diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 402548b1..a36e6791 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,20 +30,10 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: List available devices
run: xcrun simctl list devices available
- - name: Cache derived data
- uses: actions/cache@v3
- with:
- path: |
- ~/.derivedData
- key: |
- deriveddata-xcodebuild-${{ matrix.platform }}-${{ matrix.xcode }}-${{ matrix.command }}-${{ hashFiles('**/Sources/**/*.swift', '**/Tests/**/*.swift') }}
- restore-keys: |
- deriveddata-xcodebuild-${{ matrix.platform }}-${{ matrix.xcode }}-${{ matrix.command }}-
- name: Set IgnoreFileSystemDeviceInodeChanges flag
run: defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES
- name: Update mtime for incremental builds
uses: chetan/git-restore-mtime-action@v2
- - run: make dot-env
- name: Debug
run: make XCODEBUILD_ARGUMENT="${{ matrix.command }}" CONFIG=Debug PLATFORM="${{ matrix.platform }}" xcodebuild
- name: Release
@@ -62,7 +52,6 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: lcov.info
-
xcodebuild:
name: xcodebuild (15)
runs-on: macos-14
@@ -77,6 +66,7 @@ jobs:
- { xcode: 15.2, platform: TVOS }
- { xcode: 15.2, platform: VISIONOS }
- { xcode: 15.2, platform: WATCHOS }
+ - { command: test, platform: WATCHOS }
include:
- { command: test, skip_release: 1 }
steps:
@@ -105,7 +95,6 @@ jobs:
run: defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES
- name: Update mtime for incremental builds
uses: chetan/git-restore-mtime-action@v2
- - run: make dot-env
- name: Debug
run: make XCODEBUILD_ARGUMENT="${{ matrix.command }}" CONFIG=Debug PLATFORM="${{ matrix.platform }}" xcodebuild
- name: Release
@@ -132,7 +121,6 @@ jobs:
# build-spm-linux-${{ matrix.swift-version }}-${{ hashFiles('**/Sources/**/*.swift', '**/Tests/**/*.swift', '**/Package.resolved') }}
# restore-keys: |
# build-spm-linux-${{ matrix.swift-version }}-
- # - run: make dot-env
# - name: Run tests
# run: swift test --skip IntegrationTests
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows_disabled/integration-tests.yml
similarity index 100%
rename from .github/workflows/integration-tests.yml
rename to .github/workflows_disabled/integration-tests.yml
diff --git a/.gitignore b/.gitignore
index 3c09847f..087a8828 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,6 +99,5 @@ iOSInjectionProject/
# Environment
.env
Secrets.swift
-DotEnv.swift
lcov.info
temp_coverage
\ No newline at end of file
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Supabase.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Supabase.xcscheme
index 932de571..6a52636f 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Supabase.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Supabase.xcscheme
@@ -97,7 +97,52 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
- codeCoverageEnabled = "YES">
+ codeCoverageEnabled = "YES"
+ onlyGenerateCoverageForSpecifiedTargets = "YES">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Makefile b/Makefile
index 6e582059..dc05f17d 100644
--- a/Makefile
+++ b/Makefile
@@ -38,26 +38,13 @@ endif
TEST_RUNNER_CI = $(CI)
-export SECRETS
-define SECRETS
-enum DotEnv {
- static let SUPABASE_URL = "$(SUPABASE_URL)"
- static let SUPABASE_ANON_KEY = "$(SUPABASE_ANON_KEY)"
- static let SUPABASE_SERVICE_ROLE_KEY = "$(SUPABASE_SERVICE_ROLE_KEY)"
-}
-endef
-
xcodebuild:
$(XCODEBUILD)
-load-env:
- @. ./scripts/load_env.sh
-
-dot-env:
- @echo "$$SECRETS" > Tests/IntegrationTests/DotEnv.swift
-
-test-integration: dot-env
- $(MAKE) TEST_PLAN=Integration xcodebuild
+test-integration:
+ cd Tests/IntegrationTests && supabase start && supabase db reset
+ swift test --filter IntegrationTests
+ cd Tests/IntegrationTests && supabase stop
build-for-library-evolution:
swift build \
@@ -107,4 +94,4 @@ coverage:
define udid_for
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
-endef
+endef
\ No newline at end of file
diff --git a/Package.resolved b/Package.resolved
index 284ef0a6..7c5de6d3 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,5 +1,14 @@
{
"pins" : [
+ {
+ "identity" : "mocker",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/WeTransfer/Mocker",
+ "state" : {
+ "revision" : "95fa785c751f6bc40c49e112d433c3acf8417a97",
+ "version" : "3.0.2"
+ }
+ },
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
diff --git a/Package.swift b/Package.swift
index 0daf6b54..785900e7 100644
--- a/Package.swift
+++ b/Package.swift
@@ -24,13 +24,14 @@ let package = Package(
targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"]),
],
dependencies: [
- .package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
+ .package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
+ .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.1.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
- .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.2"),
+ .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
- .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.0"),
+ .package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.0"),
],
targets: [
.target(
@@ -39,6 +40,7 @@ let package = Package(
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "Clocks", package: "swift-clocks"),
+ .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
]
),
.testTarget(
@@ -60,11 +62,11 @@ let package = Package(
name: "AuthTests",
dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
- .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
+ .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
- "Helpers",
"Auth",
+ "Helpers",
"TestHelpers",
],
exclude: [
@@ -82,12 +84,13 @@ let package = Package(
name: "FunctionsTests",
dependencies: [
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
+ .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
"Functions",
+ "Mocker",
"TestHelpers",
- ],
- exclude: ["__Snapshots__"]
+ ]
),
.testTarget(
name: "IntegrationTests",
@@ -99,7 +102,10 @@ let package = Package(
"Supabase",
"TestHelpers",
],
- resources: [.process("Fixtures")]
+ resources: [
+ .process("Fixtures"),
+ .process("supabase"),
+ ]
),
.target(
name: "PostgREST",
@@ -111,11 +117,13 @@ let package = Package(
.testTarget(
name: "PostgRESTTests",
dependencies: [
+ .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
"Helpers",
+ "Mocker",
"PostgREST",
- ],
- exclude: ["__Snapshots__"]
+ "TestHelpers",
+ ]
),
.target(
name: "Realtime",
@@ -129,8 +137,8 @@ let package = Package(
name: "RealtimeTests",
dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
- .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
+ .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
"PostgREST",
"Realtime",
"TestHelpers",
@@ -149,6 +157,9 @@ let package = Package(
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
"Storage",
+ ],
+ resources: [
+ .copy("sadcat.jpg")
]
),
.target(
@@ -174,8 +185,10 @@ let package = Package(
name: "TestHelpers",
dependencies: [
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
+ .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
"Auth",
+ "Mocker",
]
),
]
diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift
index e9be2f71..5ee7e577 100644
--- a/Sources/Functions/FunctionsClient.swift
+++ b/Sources/Functions/FunctionsClient.swift
@@ -1,7 +1,7 @@
import ConcurrencyExtras
import Foundation
-import Helpers
import HTTPTypes
+import Helpers
#if canImport(FoundationNetworking)
import FoundationNetworking
@@ -29,6 +29,7 @@ public final class FunctionsClient: Sendable {
private let http: any HTTPClientType
private let mutableState = LockIsolated(MutableState())
+ private let sessionConfiguration: URLSessionConfiguration
var headers: HTTPFields {
mutableState.headers
@@ -49,6 +50,24 @@ public final class FunctionsClient: Sendable {
region: String? = nil,
logger: (any SupabaseLogger)? = nil,
fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }
+ ) {
+ self.init(
+ url: url,
+ headers: headers,
+ region: region,
+ logger: logger,
+ fetch: fetch,
+ sessionConfiguration: .default
+ )
+ }
+
+ convenience init(
+ url: URL,
+ headers: [String: String] = [:],
+ region: String? = nil,
+ logger: (any SupabaseLogger)? = nil,
+ fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) },
+ sessionConfiguration: URLSessionConfiguration
) {
var interceptors: [any HTTPClientInterceptor] = []
if let logger {
@@ -57,18 +76,26 @@ public final class FunctionsClient: Sendable {
let http = HTTPClient(fetch: fetch, interceptors: interceptors)
- self.init(url: url, headers: headers, region: region, http: http)
+ self.init(
+ url: url,
+ headers: headers,
+ region: region,
+ http: http,
+ sessionConfiguration: sessionConfiguration
+ )
}
init(
url: URL,
headers: [String: String],
region: String?,
- http: any HTTPClientType
+ http: any HTTPClientType,
+ sessionConfiguration: URLSessionConfiguration = .default
) {
self.url = url
self.region = region
self.http = http
+ self.sessionConfiguration = sessionConfiguration
mutableState.withValue {
$0.headers = HTTPFields(headers)
@@ -164,7 +191,7 @@ public final class FunctionsClient: Sendable {
let request = buildRequest(functionName: functionName, options: invokeOptions)
let response = try await http.send(request)
- guard 200 ..< 300 ~= response.statusCode else {
+ guard 200..<300 ~= response.statusCode else {
throw FunctionsError.httpError(code: response.statusCode, data: response.data)
}
@@ -194,7 +221,8 @@ public final class FunctionsClient: Sendable {
let (stream, continuation) = AsyncThrowingStream.makeStream()
let delegate = StreamResponseDelegate(continuation: continuation)
- let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
+ let session = URLSession(
+ configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest
@@ -211,10 +239,12 @@ public final class FunctionsClient: Sendable {
return stream
}
- private func buildRequest(functionName: String, options: FunctionInvokeOptions) -> Helpers.HTTPRequest {
+ private func buildRequest(functionName: String, options: FunctionInvokeOptions)
+ -> Helpers.HTTPRequest
+ {
var request = HTTPRequest(
url: url.appendingPathComponent(functionName),
- method: options.httpMethod ?? .post,
+ method: FunctionInvokeOptions.httpMethod(options.method) ?? .post,
query: options.query,
headers: mutableState.headers.merging(with: options.headers),
body: options.body
@@ -243,14 +273,24 @@ final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable {
continuation.finish(throwing: error)
}
- func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
+ func urlSession(
+ _: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse,
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
+ ) {
+ defer {
+ completionHandler(.allow)
+ }
+
guard let httpResponse = response as? HTTPURLResponse else {
continuation.finish(throwing: URLError(.badServerResponse))
return
}
- guard 200 ..< 300 ~= httpResponse.statusCode else {
- let error = FunctionsError.httpError(code: httpResponse.statusCode, data: Data())
+ guard 200..<300 ~= httpResponse.statusCode else {
+ let error = FunctionsError.httpError(
+ code: httpResponse.statusCode,
+ data: Data()
+ )
continuation.finish(throwing: error)
return
}
@@ -259,6 +299,5 @@ final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable {
if isRelayError {
continuation.finish(throwing: FunctionsError.relayError)
}
- completionHandler(.allow)
}
}
diff --git a/Sources/Functions/Types.swift b/Sources/Functions/Types.swift
index e409c665..e69f036e 100644
--- a/Sources/Functions/Types.swift
+++ b/Sources/Functions/Types.swift
@@ -1,6 +1,6 @@
import Foundation
-import Helpers
import HTTPTypes
+import Helpers
/// An error type representing various errors that can occur while invoking functions.
public enum FunctionsError: Error, LocalizedError {
@@ -99,7 +99,7 @@ public struct FunctionInvokeOptions: Sendable {
case delete = "DELETE"
}
- var httpMethod: HTTPTypes.HTTPRequest.Method? {
+ static func httpMethod(_ method: Method?) -> HTTPTypes.HTTPRequest.Method? {
switch method {
case .get:
.get
diff --git a/Sources/Helpers/Version.swift b/Sources/Helpers/Version.swift
index 490d7258..052341a1 100644
--- a/Sources/Helpers/Version.swift
+++ b/Sources/Helpers/Version.swift
@@ -1 +1,9 @@
-package let version = "2.24.4" // {x-release-please-version}
+import XCTestDynamicOverlay
+
+private let _version = "2.24.4" // {x-release-please-version}
+
+#if DEBUG
+ package let version = isTesting ? "0.0.0" : _version
+#else
+ package let version = _version
+#endif
diff --git a/Sources/TestHelpers/MockExtensions.swift b/Sources/TestHelpers/MockExtensions.swift
new file mode 100644
index 00000000..8b585d31
--- /dev/null
+++ b/Sources/TestHelpers/MockExtensions.swift
@@ -0,0 +1,43 @@
+//
+// MockExtensions.swift
+// Supabase
+//
+// Created by Guilherme Souza on 21/01/25.
+//
+
+import Mocker
+import Foundation
+import InlineSnapshotTesting
+
+extension Mock {
+ package func snapshotRequest(
+ message: @autoclosure () -> String = "",
+ record isRecording: Bool? = nil,
+ timeout: TimeInterval = 5,
+ syntaxDescriptor: InlineSnapshotSyntaxDescriptor = InlineSnapshotSyntaxDescriptor(),
+ matches expected: (() -> String)? = nil,
+ fileID: StaticString = #fileID,
+ file filePath: StaticString = #filePath,
+ function: StaticString = #function,
+ line: UInt = #line,
+ column: UInt = #column
+ ) -> Self {
+ var copy = self
+ copy.onRequestHandler = OnRequestHandler {
+ assertInlineSnapshot(
+ of: $0,
+ as: ._curl,
+ record: isRecording,
+ timeout: timeout,
+ syntaxDescriptor: syntaxDescriptor,
+ matches: expected,
+ fileID: fileID,
+ file: filePath,
+ function: function,
+ line: line,
+ column: column
+ )
+ }
+ return copy
+ }
+}
diff --git a/Sources/TestHelpers/URLRequestSnapshot.swift b/Sources/TestHelpers/URLRequestSnapshot.swift
new file mode 100644
index 00000000..0c17b458
--- /dev/null
+++ b/Sources/TestHelpers/URLRequestSnapshot.swift
@@ -0,0 +1,110 @@
+//
+// SnapshotStrategy.swift
+// Supabase
+//
+// Created by Guilherme Souza on 22/01/25.
+//
+
+@preconcurrency import InlineSnapshotTesting
+
+#if !os(WASI)
+ import Foundation
+
+ #if canImport(FoundationNetworking)
+ import FoundationNetworking
+ #endif
+
+ extension Snapshotting where Value == URLRequest, Format == String {
+ /// A snapshot strategy for comparing requests based on a cURL representation.
+ ///
+ // ``` swift
+ // assertSnapshot(of: request, as: .curl)
+ // ```
+ //
+ // Records:
+ //
+ // ```
+ // curl \
+ // --request POST \
+ // --header "Accept: text/html" \
+ // --data 'pricing[billing]=monthly&pricing[lane]=individual' \
+ // "https://www.pointfree.co/subscribe"
+ // ```
+ package static let _curl = SimplySnapshotting.lines.pullback { (request: URLRequest) in
+
+ var components = ["curl"]
+
+ // HTTP Method
+ let httpMethod = request.httpMethod!
+ switch httpMethod {
+ case "GET": break
+ case "HEAD": components.append("--head")
+ default: components.append("--request \(httpMethod)")
+ }
+
+ // Headers
+ if let headers = request.allHTTPHeaderFields {
+ for field in headers.keys.sorted() where field != "Cookie" {
+ let escapedValue = headers[field]!.replacingOccurrences(of: "\"", with: "\\\"")
+ components.append("--header \"\(field): \(escapedValue)\"")
+ }
+ }
+
+ // Body
+ if let httpBodyData = request.data,
+ let httpBody = String(data: httpBodyData, encoding: .utf8)
+ {
+ var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"")
+ escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"")
+
+ components.append("--data \"\(escapedBody)\"")
+ }
+
+ // Cookies
+ if let cookie = request.allHTTPHeaderFields?["Cookie"] {
+ let escapedValue = cookie.replacingOccurrences(of: "\"", with: "\\\"")
+ components.append("--cookie \"\(escapedValue)\"")
+ }
+
+ // URL
+ components.append("\"\(request.url!.sortingQueryItems()!.absoluteString)\"")
+
+ return components.joined(separator: " \\\n\t")
+ }
+ }
+
+ extension URL {
+ fileprivate func sortingQueryItems() -> URL? {
+ var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
+ let sortedQueryItems = components?.queryItems?.sorted { $0.name < $1.name }
+ components?.queryItems = sortedQueryItems
+
+ return components?.url
+ }
+ }
+
+ extension URLRequest {
+ fileprivate var data: Data? {
+ httpBody ?? httpBodyStream.map { Data(reading: $0, withBufferSize: 1024) }
+ }
+ }
+
+ extension Data {
+ fileprivate init(reading stream: InputStream, withBufferSize bufferSize: UInt = 1024) {
+ self.init()
+
+ stream.open()
+ defer { stream.close() }
+
+ let bufferSize = Int(bufferSize)
+ let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize)
+ defer { buffer.deallocate() }
+
+ while stream.hasBytesAvailable {
+ let read = stream.read(buffer, maxLength: bufferSize)
+ guard read > 0 else { return }
+ self.append(buffer, count: read)
+ }
+ }
+ }
+#endif
diff --git a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 953722c6..4a7be132 100644
--- a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -36,6 +36,15 @@
"version" : "4.1.1"
}
},
+ {
+ "identity" : "mocker",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/WeTransfer/Mocker",
+ "state" : {
+ "revision" : "95fa785c751f6bc40c49e112d433c3acf8417a97",
+ "version" : "3.0.2"
+ }
+ },
{
"identity" : "svgview",
"kind" : "remoteSourceControl",
diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift
index 1152c2d1..b48cbf24 100644
--- a/Tests/AuthTests/RequestsTests.swift
+++ b/Tests/AuthTests/RequestsTests.swift
@@ -527,7 +527,7 @@ final class RequestsTests: XCTestCase {
fetch: { request in
DispatchQueue.main.sync {
assertSnapshot(
- of: request, as: .curl, record: record, file: file, testName: testName, line: line
+ of: request, as: ._curl, record: record, file: file, testName: testName, line: line
)
}
diff --git a/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift b/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift
index 4a781007..0c050086 100644
--- a/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift
+++ b/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift
@@ -1,3 +1,4 @@
+import HTTPTypes
import XCTest
@testable import Functions
@@ -34,4 +35,18 @@ final class FunctionInvokeOptionsTests: XCTestCase {
XCTAssertEqual(options.headers[.contentType], contentType)
XCTAssertNotNil(options.body)
}
+
+ func testMethod() {
+ let testCases: [FunctionInvokeOptions.Method: HTTPTypes.HTTPRequest.Method] = [
+ .get: .get,
+ .post: .post,
+ .put: .put,
+ .patch: .patch,
+ .delete: .delete,
+ ]
+
+ for (method, expected) in testCases {
+ XCTAssertEqual(FunctionInvokeOptions.httpMethod(method), expected)
+ }
+ }
}
diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift
index e4972dce..7e2535a6 100644
--- a/Tests/FunctionsTests/FunctionsClientTests.swift
+++ b/Tests/FunctionsTests/FunctionsClientTests.swift
@@ -1,89 +1,156 @@
import ConcurrencyExtras
-@testable import Functions
-import Helpers
import HTTPTypes
+import Helpers
+import InlineSnapshotTesting
+import Mocker
import TestHelpers
import XCTest
+@testable import Functions
+
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
final class FunctionsClientTests: XCTestCase {
let url = URL(string: "http://localhost:5432/functions/v1")!
- let apiKey = "supabase.anon.key"
-
- lazy var sut = FunctionsClient(url: url, headers: ["Apikey": apiKey])
+ let apiKey =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
+
+ let sessionConfiguration: URLSessionConfiguration = {
+ let sessionConfiguration = URLSessionConfiguration.default
+ sessionConfiguration.protocolClasses = [MockingURLProtocol.self]
+ return sessionConfiguration
+ }()
+
+ lazy var session = URLSession(configuration: sessionConfiguration)
+
+ var region: String?
+
+ lazy var sut = FunctionsClient(
+ url: url,
+ headers: [
+ "apikey": apiKey
+ ],
+ region: region,
+ fetch: { request in
+ try await self.session.data(for: request)
+ },
+ sessionConfiguration: sessionConfiguration
+ )
+
+ override func setUp() {
+ super.setUp()
+ // isRecording = true
+ }
func testInit() async {
let client = FunctionsClient(
url: url,
- headers: ["Apikey": apiKey],
+ headers: ["apikey": apiKey],
region: .saEast1
)
XCTAssertEqual(client.region, "sa-east-1")
- XCTAssertEqual(client.headers[.init("Apikey")!], apiKey)
+ XCTAssertEqual(client.headers[.init("apikey")!], apiKey)
XCTAssertNotNil(client.headers[.init("X-Client-Info")!])
}
func testInvoke() async throws {
- let url = URL(string: "http://localhost:5432/functions/v1/hello_world")!
-
- let http = await HTTPClientMock()
- .when {
- $0.url.pathComponents.contains("hello_world")
- } return: { _ in
- try .stub(body: Empty())
- }
- let sut = FunctionsClient(
- url: self.url,
- headers: ["Apikey": apiKey],
- region: nil,
- http: http
+ Mock(
+ url: self.url.appendingPathComponent("hello_world"),
+ statusCode: 200,
+ data: [.post: Data()]
)
-
- let body = ["name": "Supabase"]
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "Content-Length: 19" \
+ --header "Content-Type: application/json" \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "X-Custom-Key: value" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ --data "{\"name\":\"Supabase\"}" \
+ "http://localhost:5432/functions/v1/hello_world"
+ """#
+ }
+ .register()
try await sut.invoke(
"hello_world",
- options: .init(headers: ["X-Custom-Key": "value"], body: body)
+ options: .init(headers: ["X-Custom-Key": "value"], body: ["name": "Supabase"])
)
+ }
- let request = await http.receivedRequests.last
+ func testInvokeReturningDecodable() async throws {
+ Mock(
+ url: url.appendingPathComponent("hello"),
+ statusCode: 200,
+ data: [
+ .post: #"{"message":"Hello, world!","status":"ok"}"#.data(using: .utf8)!
+ ]
+ )
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello"
+ """#
+ }
+ .register()
- XCTAssertEqual(request?.url, url)
- XCTAssertEqual(request?.method, .post)
- XCTAssertEqual(request?.headers[.init("Apikey")!], apiKey)
- XCTAssertEqual(request?.headers[.init("X-Custom-Key")!], "value")
- XCTAssertEqual(request?.headers[.init("X-Client-Info")!], "functions-swift/\(Functions.version)")
+ struct Payload: Decodable {
+ var message: String
+ var status: String
+ }
+
+ let response = try await sut.invoke("hello") as Payload
+ XCTAssertEqual(response.message, "Hello, world!")
+ XCTAssertEqual(response.status, "ok")
}
func testInvokeWithCustomMethod() async throws {
- let http = await HTTPClientMock().any { _ in try .stub(body: Empty()) }
-
- let sut = FunctionsClient(
- url: url,
- headers: ["Apikey": apiKey],
- region: nil,
- http: http
+ Mock(
+ url: url.appendingPathComponent("hello-world"),
+ statusCode: 200,
+ data: [.delete: Data()]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request DELETE \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello-world"
+ """#
+ }
+ .register()
try await sut.invoke("hello-world", options: .init(method: .delete))
-
- let request = await http.receivedRequests.last
- XCTAssertEqual(request?.method, .delete)
}
func testInvokeWithQuery() async throws {
- let http = await HTTPClientMock().any { _ in try .stub(body: Empty()) }
-
- let sut = FunctionsClient(
- url: url,
- headers: ["Apikey": apiKey],
- region: nil,
- http: http
+ Mock(
+ url: url.appendingPathComponent("hello-world"),
+ ignoreQuery: true,
+ statusCode: 200,
+ data: [
+ .post: Data()
+ ]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello-world?key=value"
+ """#
+ }
+ .register()
try await sut.invoke(
"hello-world",
@@ -91,73 +158,95 @@ final class FunctionsClientTests: XCTestCase {
query: [URLQueryItem(name: "key", value: "value")]
)
)
-
- let request = await http.receivedRequests.last
- XCTAssertEqual(request?.urlRequest.url?.query, "key=value")
}
func testInvokeWithRegionDefinedInClient() async throws {
- let http = await HTTPClientMock()
- .any { _ in try .stub(body: Empty()) }
+ region = FunctionRegion.caCentral1.rawValue
- let sut = FunctionsClient(
- url: url,
- headers: [:],
- region: FunctionRegion.caCentral1.rawValue,
- http: http
+ Mock(
+ url: url.appendingPathComponent("hello-world"),
+ statusCode: 200,
+ data: [.post: Data()]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ --header "x-region: ca-central-1" \
+ "http://localhost:5432/functions/v1/hello-world"
+ """#
+ }
+ .register()
try await sut.invoke("hello-world")
-
- let request = await http.receivedRequests.last
- XCTAssertEqual(request?.headers[.xRegion], "ca-central-1")
}
func testInvokeWithRegion() async throws {
- let http = await HTTPClientMock()
- .any { _ in try .stub(body: Empty()) }
-
- let sut = FunctionsClient(
- url: url,
- headers: [:],
- region: nil,
- http: http
+ Mock(
+ url: url.appendingPathComponent("hello-world"),
+ statusCode: 200,
+ data: [.post: Data()]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ --header "x-region: ca-central-1" \
+ "http://localhost:5432/functions/v1/hello-world"
+ """#
+ }
+ .register()
try await sut.invoke("hello-world", options: .init(region: .caCentral1))
-
- let request = await http.receivedRequests.last
- XCTAssertEqual(request?.headers[.xRegion], "ca-central-1")
}
func testInvokeWithoutRegion() async throws {
- let http = await HTTPClientMock()
- .any { _ in try .stub(body: Empty()) }
+ region = nil
- let sut = FunctionsClient(
- url: url,
- headers: [:],
- region: nil,
- http: http
+ Mock(
+ url: url.appendingPathComponent("hello-world"),
+ statusCode: 200,
+ data: [.post: Data()]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello-world"
+ """#
+ }
+ .register()
try await sut.invoke("hello-world")
-
- let request = await http.receivedRequests.last
- XCTAssertNil(request?.headers[.xRegion])
}
func testInvoke_shouldThrow_URLError_badServerResponse() async {
- let sut = await FunctionsClient(
- url: url,
- headers: ["Apikey": apiKey],
- region: nil,
- http: HTTPClientMock()
- .any { _ in throw URLError(.badServerResponse) }
+ Mock(
+ url: url.appendingPathComponent("hello_world"),
+ statusCode: 200,
+ data: [.post: Data()],
+ requestError: URLError(.badServerResponse)
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello_world"
+ """#
+ }
+ .register()
do {
try await sut.invoke("hello_world")
+ XCTFail("Invoke should fail.")
} catch let urlError as URLError {
XCTAssertEqual(urlError.code, .badServerResponse)
} catch {
@@ -166,13 +255,22 @@ final class FunctionsClientTests: XCTestCase {
}
func testInvoke_shouldThrow_FunctionsError_httpError() async {
- let sut = await FunctionsClient(
- url: url,
- headers: ["Apikey": apiKey],
- region: nil,
- http: HTTPClientMock()
- .any { _ in try .stub(body: Empty(), statusCode: 300) }
+ Mock(
+ url: url.appendingPathComponent("hello_world"),
+ statusCode: 300,
+ data: [.post: Data()]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello_world"
+ """#
+ }
+ .register()
+
do {
try await sut.invoke("hello_world")
XCTFail("Invoke should fail.")
@@ -184,17 +282,24 @@ final class FunctionsClientTests: XCTestCase {
}
func testInvoke_shouldThrow_FunctionsError_relayError() async {
- let sut = await FunctionsClient(
- url: url,
- headers: ["Apikey": apiKey],
- region: nil,
- http: HTTPClientMock().any { _ in
- try .stub(
- body: Empty(),
- headers: [.xRelayError: "true"]
- )
- }
+ Mock(
+ url: url.appendingPathComponent("hello_world"),
+ statusCode: 200,
+ data: [.post: Data()],
+ additionalHeaders: [
+ "x-relay-error": "true"
+ ]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/hello_world"
+ """#
+ }
+ .register()
do {
try await sut.invoke("hello_world")
@@ -208,27 +313,90 @@ final class FunctionsClientTests: XCTestCase {
func test_setAuth() {
sut.setAuth(token: "access.token")
XCTAssertEqual(sut.headers[.authorization], "Bearer access.token")
+
+ sut.setAuth(token: nil)
+ XCTAssertNil(sut.headers[.authorization])
}
-}
-extension Helpers.HTTPResponse {
- static func stub(
- body: any Encodable,
- statusCode: Int = 200,
- headers: HTTPFields = .init()
- ) throws -> Helpers.HTTPResponse {
- let data = try JSONEncoder().encode(body)
- let response = HTTPURLResponse(
- url: URL(string: "http://127.0.0.1")!,
- statusCode: statusCode,
- httpVersion: nil,
- headerFields: headers.dictionary
- )!
- return HTTPResponse(
- data: data,
- response: response
+ func testInvokeWithStreamedResponse() async throws {
+ Mock(
+ url: url.appendingPathComponent("stream"),
+ statusCode: 200,
+ data: [.post: Data("hello world".utf8)]
)
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/stream"
+ """#
+ }
+ .register()
+
+ let stream = sut._invokeWithStreamedResponse("stream")
+
+ for try await value in stream {
+ XCTAssertEqual(String(decoding: value, as: UTF8.self), "hello world")
+ }
}
-}
-struct Empty: Codable {}
+ func testInvokeWithStreamedResponseHTTPError() async throws {
+ Mock(
+ url: url.appendingPathComponent("stream"),
+ statusCode: 300,
+ data: [.post: Data()]
+ )
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/stream"
+ """#
+ }
+ .register()
+
+ let stream = sut._invokeWithStreamedResponse("stream")
+
+ do {
+ for try await _ in stream {
+ XCTFail("should throw error")
+ }
+ } catch let FunctionsError.httpError(code, _) {
+ XCTAssertEqual(code, 300)
+ }
+ }
+
+ func testInvokeWithStreamedResponseRelayError() async throws {
+ Mock(
+ url: url.appendingPathComponent("stream"),
+ statusCode: 200,
+ data: [.post: Data()],
+ additionalHeaders: [
+ "x-relay-error": "true"
+ ]
+ )
+ .snapshotRequest {
+ #"""
+ curl \
+ --request POST \
+ --header "X-Client-Info: functions-swift/0.0.0" \
+ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
+ "http://localhost:5432/functions/v1/stream"
+ """#
+ }
+ .register()
+
+ let stream = sut._invokeWithStreamedResponse("stream")
+
+ do {
+ for try await _ in stream {
+ XCTFail("should throw error")
+ }
+ } catch FunctionsError.relayError {
+ }
+ }
+}
diff --git a/Tests/FunctionsTests/FunctionsErrorTests.swift b/Tests/FunctionsTests/FunctionsErrorTests.swift
new file mode 100644
index 00000000..706abcf2
--- /dev/null
+++ b/Tests/FunctionsTests/FunctionsErrorTests.swift
@@ -0,0 +1,20 @@
+//
+// FunctionsErrorTests.swift
+// Supabase
+//
+// Created by Guilherme Souza on 20/01/25.
+//
+
+import Supabase
+import XCTest
+
+final class FunctionsErrorTests: XCTestCase {
+
+ func testLocalizedDescription() {
+ XCTAssertEqual(
+ FunctionsError.relayError.localizedDescription, "Relay Error invoking the Edge Function")
+ XCTAssertEqual(
+ FunctionsError.httpError(code: 412, data: Data()).localizedDescription,
+ "Edge Function returned a non-2xx status code: 412")
+ }
+}
diff --git a/Tests/IntegrationTests/supabase/.branches/_current_branch b/Tests/IntegrationTests/supabase/.branches/_current_branch
new file mode 100644
index 00000000..88d050b1
--- /dev/null
+++ b/Tests/IntegrationTests/supabase/.branches/_current_branch
@@ -0,0 +1 @@
+main
\ No newline at end of file
diff --git a/Tests/IntegrationTests/supabase/.temp/cli-latest b/Tests/IntegrationTests/supabase/.temp/cli-latest
new file mode 100644
index 00000000..e4af78e3
--- /dev/null
+++ b/Tests/IntegrationTests/supabase/.temp/cli-latest
@@ -0,0 +1 @@
+v2.6.8
\ No newline at end of file