diff --git a/Package.swift b/Package.swift index bed7ee94e..77f4dae58 100644 --- a/Package.swift +++ b/Package.swift @@ -181,6 +181,7 @@ let package = Package( name: "SupabaseTests", dependencies: [ .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), "Supabase", ] ), diff --git a/Sources/Helpers/Version.swift b/Sources/Helpers/Version.swift index a39236e0f..3d57f26d5 100644 --- a/Sources/Helpers/Version.swift +++ b/Sources/Helpers/Version.swift @@ -1,3 +1,4 @@ +import Foundation import XCTestDynamicOverlay private let _version = "2.27.0" // {x-release-please-version} @@ -7,3 +8,61 @@ private let _version = "2.27.0" // {x-release-please-version} #else package let version = _version #endif + +private let _platform: String? = { + #if os(macOS) + return "macOS" + #elseif os(visionOS) + return "visionOS" + #elseif os(iOS) + #if targetEnvironment(macCatalyst) + return "macCatalyst" + #else + if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac { + return "iOSAppOnMac" + } + return "iOS" + #endif + #elseif os(watchOS) + return "watchOS" + #elseif os(tvOS) + return "tvOS" + #elseif os(Android) + return "Android" + #elseif os(Linux) + return "Linux" + #elseif os(Windows) + return "Windows" + #else + return nil + #endif +}() + +private let _platformVersion: String? = { + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Windows) + let majorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion + let minorVersion = ProcessInfo.processInfo.operatingSystemVersion.minorVersion + let patchVersion = ProcessInfo.processInfo.operatingSystemVersion.patchVersion + return "\(majorVersion).\(minorVersion).\(patchVersion)" + #elseif os(Linux) || os(Android) + if let version = try? String(contentsOfFile: "/proc/version") { + return version.trimmingCharacters(in: .whitespacesAndNewlines) + } else { + return nil + } + #else + nil + #endif +}() + +#if DEBUG + package let platform = isTesting ? "macOS" : _platform +#else + package let platform = _platform +#endif + +#if DEBUG + package let platformVersion = isTesting ? "0.0.0" : _platformVersion +#else + package let platformVersion = _platformVersion +#endif diff --git a/Sources/Supabase/Constants.swift b/Sources/Supabase/Constants.swift new file mode 100644 index 000000000..cb4ac4e5c --- /dev/null +++ b/Sources/Supabase/Constants.swift @@ -0,0 +1,25 @@ +// +// Constants.swift +// Supabase +// +// Created by Guilherme Souza on 06/03/25. +// + +import Foundation +import Helpers + +let defaultHeaders: [String: String] = { + var headers = [ + "X-Client-Info": "supabase-swift/\(version)" + ] + + if let platform { + headers["X-Supabase-Client-Platform"] = platform + } + + if let platformVersion { + headers["X-Supabase-Client-Platform-Version"] = platformVersion + } + + return headers +}() diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 4d28d4696..849cd3ae7 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -17,8 +17,6 @@ public typealias SupabaseLogger = Helpers.SupabaseLogger public typealias SupabaseLogLevel = Helpers.SupabaseLogLevel public typealias SupabaseLogMessage = Helpers.SupabaseLogMessage -let version = Helpers.version - /// Supabase Client. public final class SupabaseClient: Sendable { let options: SupabaseClientOptions @@ -163,12 +161,16 @@ public final class SupabaseClient: Sendable { databaseURL = supabaseURL.appendingPathComponent("/rest/v1") functionsURL = supabaseURL.appendingPathComponent("/functions/v1") - _headers = HTTPFields([ - "X-Client-Info": "supabase-swift/\(version)", - "Authorization": "Bearer \(supabaseKey)", - "Apikey": supabaseKey, - ]) - .merging(with: HTTPFields(options.global.headers)) + _headers = HTTPFields(defaultHeaders) + .merging( + with: HTTPFields( + [ + "Authorization": "Bearer \(supabaseKey)", + "Apikey": supabaseKey, + ] + ) + ) + .merging(with: HTTPFields(options.global.headers)) // default storage key uses the supabase project ref as a namespace let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token" diff --git a/Tests/SupabaseTests/SupabaseClientTests.swift b/Tests/SupabaseTests/SupabaseClientTests.swift index 8e25ebd69..437353cd6 100644 --- a/Tests/SupabaseTests/SupabaseClientTests.swift +++ b/Tests/SupabaseTests/SupabaseClientTests.swift @@ -1,10 +1,13 @@ -@testable import Auth import CustomDump -@testable import Functions +import InlineSnapshotTesting import IssueReporting +import SnapshotTestingCustomDump +import XCTest + +@testable import Auth +@testable import Functions @testable import Realtime @testable import Supabase -import XCTest final class AuthLocalStorageMock: AuthLocalStorage { func store(key _: String, value _: Data) throws {} @@ -61,16 +64,22 @@ final class SupabaseClientTests: XCTestCase { "https://project-ref.supabase.co/functions/v1" ) - XCTAssertEqual( - client.headers, + assertInlineSnapshot(of: client.headers, as: .customDump) { + """ [ - "X-Client-Info": "supabase-swift/\(Supabase.version)", "Apikey": "ANON_KEY", - "header_field": "header_value", "Authorization": "Bearer ANON_KEY", + "X-Client-Info": "supabase-swift/0.0.0", + "X-Supabase-Client-Platform": "macOS", + "X-Supabase-Client-Platform-Version": "0.0.0", + "header_field": "header_value" ] - ) - expectNoDifference(client._headers.dictionary, client.headers) + """ + } + expectNoDifference(client.headers, client.auth.configuration.headers) + expectNoDifference(client.headers, client.functions.headers.dictionary) + expectNoDifference(client.headers, client.storage.configuration.headers) + expectNoDifference(client.headers, client.rest.configuration.headers) XCTAssertEqual(client.functions.region, "ap-northeast-1") @@ -79,7 +88,8 @@ final class SupabaseClientTests: XCTestCase { let realtimeOptions = client.realtimeV2.options let expectedRealtimeHeader = client._headers.merging(with: [ - .init("custom_realtime_header_key")!: "custom_realtime_header_value"] + .init("custom_realtime_header_key")!: "custom_realtime_header_value" + ] ) expectNoDifference(realtimeOptions.headers, expectedRealtimeHeader) XCTAssertIdentical(realtimeOptions.logger as? Logger, logger)