diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dda3696..c2b5a28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,8 @@ name: Test on: push: - branches: main + branches: + - main pull_request: jobs: @@ -26,5 +27,7 @@ jobs: .build/repositories key: google-cloud-swift_build_${{ runner.os }}_${{ hashFiles('Package.resolved') }} - run: swift build - - name: swift test + - if: matrix.swift == '6.2' # swift 6.0 does not support test scoping yet + name: Run test run: ./run_test.sh + diff --git a/Package.swift b/Package.swift index 0cbbd33..c8c1fa5 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( .library(name: "GoogleCloud", targets: ["GoogleCloud"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "5.0.0"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.24.0"), .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.1.1"), ], diff --git a/Sources/FirebaseAdmin/Auth/Auth.swift b/Sources/FirebaseAdmin/Auth/Auth.swift index 2a4a80a..07a996e 100644 --- a/Sources/FirebaseAdmin/Auth/Auth.swift +++ b/Sources/FirebaseAdmin/Auth/Auth.swift @@ -51,7 +51,7 @@ public struct Auth: Sendable { projectID paramProjectID: String? = nil ) throws { var credential = client.credential - var baseURL: URL + let baseURL: URL guard let projectID = paramProjectID ?? Self.projectID(from: credential) else { throw AuthError(message: "projectID must be provided if the credential doesn't have it.") } @@ -137,7 +137,7 @@ public struct Auth: Sendable { private func user( request: GetUserRequest, - logger: Logger? + logger: Logger? = nil ) async throws -> UserRecord? { let path = "/accounts:lookup" let res = try await baseClient.post( @@ -234,4 +234,8 @@ public struct Auth: Sendable { responseType: ListUserResult.self ) } + + public func customToken(uid: String) async throws -> String { + fatalError("TODO") + } } diff --git a/Sources/FirebaseAdmin/Auth/HTTPKeySource.swift b/Sources/FirebaseAdmin/Auth/HTTPKeySource.swift index 6186379..53ce388 100644 --- a/Sources/FirebaseAdmin/Auth/HTTPKeySource.swift +++ b/Sources/FirebaseAdmin/Auth/HTTPKeySource.swift @@ -36,7 +36,10 @@ struct HTTPKeySource { } } - init(client: HTTPClient, clock: some Clock = .continuous) { + init( + client: HTTPClient = .shared, + clock: some Clock = .continuous + ) { self.client = client let willRefreshKeys = NIOLockedValueBox<(@Sendable () -> ())?>(nil) self.willRefreshKeys = willRefreshKeys diff --git a/Sources/GoogleCloud/BigQuery/BigQuery.swift b/Sources/GoogleCloud/BigQuery/BigQuery.swift index 10d425d..2d4c0a4 100644 --- a/Sources/GoogleCloud/BigQuery/BigQuery.swift +++ b/Sources/GoogleCloud/BigQuery/BigQuery.swift @@ -47,7 +47,7 @@ public struct BigQuery: Sendable { public var useLegacySql: Bool = false } - public func query( + public func query( _ query: BigQueryQueryString, options: QueryOptions = QueryOptions(), decoding rowType: Row.Type, @@ -58,7 +58,7 @@ public struct BigQuery: Sendable { .reduce(into: [], { $0.append(contentsOf: $1) }) } - public func queryStream( + public func queryStream( _ query: BigQueryQueryString, options: QueryOptions = QueryOptions(), decoding rowType: Row.Type, diff --git a/Sources/GoogleCloudBase/Util/SendableMetatype.swift b/Sources/GoogleCloudBase/Util/SendableMetatype.swift new file mode 100644 index 0000000..8752afb --- /dev/null +++ b/Sources/GoogleCloudBase/Util/SendableMetatype.swift @@ -0,0 +1,5 @@ +#if swift(>=6.2) +public typealias GCSSendableMetatype = SendableMetatype +#else +public typealias GCSSendableMetatype = Any +#endif diff --git a/Tests/SharedTests/Auth/AuthTest.swift b/Tests/SharedTests/Auth/AuthTest.swift index 413ec4e..1240ce8 100644 --- a/Tests/SharedTests/Auth/AuthTest.swift +++ b/Tests/SharedTests/Auth/AuthTest.swift @@ -1,55 +1,33 @@ +import AsyncHTTPClient @testable import FirebaseAdmin import NIOPosix -import XCTest +import Testing import Logging private let testingProjectID = "testing-project-id" -final class AuthTest: XCTestCase { - private static let client = try! GCPClient(credentialFactory: .custom { _ in - MockCredential() - }) - - private static nonisolated(unsafe) var emulatorURL: URL? = Auth.emulatorBaseURL() - - override class func setUp() { - super.setUp() - initLogger() - - if let url = Self.emulatorURL { +@Suite( + .gcpClient, + .serialized, + .enabled(if: Auth.emulatorBaseURL() != nil, "AuthTest uses Firebase Auth Emulator.") +) struct AuthTest { + init() async throws { + if let url = Auth.emulatorBaseURL() { let endpoint = Auth.emulatorAPIBaseURL(url: url)!.appendingPathComponent("projects/\(testingProjectID)/accounts") - do { - let request = try HTTPClient.Request(url: endpoint, method: .DELETE) - _ = try client.httpClient.execute(request: request).wait() - } catch { - XCTFail("\(error)") - } - } - } - - override class func tearDown() { - do { - try client.syncShutdown() - } catch { - XCTFail("\(error)") + var request = HTTPClientRequest(url: endpoint.absoluteString) + request.method = .DELETE + _ = try await HTTPClient.shared.execute(request, timeout: .seconds(1)) } - - super.tearDown() - } - - override func setUpWithError() throws { - try super.setUpWithError() - try XCTSkipIf(Self.emulatorURL == nil, "AuthTest uses Firebase Auth Emulator.") } private func makeAuth() throws -> Auth { try Auth( - client: Self.client, + client: .mockCredentialClient, projectID: testingProjectID ) } - func testCreateUser() async throws { + @Test func createUser() async throws { let auth = try makeAuth() do { @@ -57,27 +35,25 @@ final class AuthTest: XCTestCase { email: "testCreateUser@example.com", password: "012345" )).get() - XCTAssertTrue(!uid.isEmpty) + #expect(!uid.isEmpty) } catch { dump(error) - XCTFail("\(error)") + Issue.record("\(error)") } } - func testCreateUserWithID() async throws { + @Test func createUserWithID() async throws { let auth = try makeAuth() - await XCTAssertNoThrow({ - let uid = try await auth.createUser(UserToCreate( - localId: "tama", - email: "testCreateUserWithID@example.com", - password: "012345" - )).get() - XCTAssertEqual(uid, "tama") - }) + let uid = try await auth.createUser(UserToCreate( + localId: "tama", + email: "testCreateUserWithID@example.com", + password: "012345" + )).get() + #expect(uid == "tama") } - func testCreateUserErrorEmailExists() async throws { + @Test func createUserErrorEmailExists() async throws { let auth = try makeAuth() let email = "testCreateUserErrorEmailExists@example.com" @@ -91,39 +67,32 @@ final class AuthTest: XCTestCase { email: email, password: "123456" )) - let error = try XCTUnwrap(ret.failure) - XCTAssertEqual(error.code, .emailExists) + let error = try #require(ret.failure) + #expect(error.code == .emailExists) } - func testGetUser() async throws { + @Test func getUser() async throws { let auth = try makeAuth() - let uid = try await XCTAssertNoThrow { - try await auth.createUser(UserToCreate( - email: "testGetUser@example.com", - password: "111111" - )).get() - } + let uid = try await auth.createUser(UserToCreate( + email: "testGetUser@example.com", + password: "111111" + )).get() - let result = try await XCTUnwrap { - try await auth.user(for: uid) - } - XCTAssertEqual(result.uid, uid) - XCTAssertEqual(result.email, "testGetUser@example.com".lowercased()) - XCTAssertEqual(result.providers.first?.providerID, "password") - XCTAssertTrue(try XCTUnwrap(result.passwordHash).hasSuffix("password=111111")) + let result = try #require(try await auth.user(for: uid)) + #expect(result.uid == uid) + #expect(result.email == "testGetUser@example.com".lowercased()) + #expect(result.providers.first?.providerID == "password") + #expect(try #require(result.passwordHash).hasSuffix("password=111111")) } - func testGetUserNotFound() async throws { + @Test func getUserNotFound() async throws { let auth = try makeAuth() - let result = try await XCTAssertNoThrow { - try await auth.user(for: "aaaaaaaaaaaaaaaaaaaaa") - } - XCTAssertNil(result) + #expect(try await auth.user(for: "aaaaaaaaaaaaaaaaaaaaa") == nil) } - func testGetUserEmail() async throws { + @Test func getUserEmail() async throws { let auth = try makeAuth() _ = try await auth.createUser(UserToCreate( @@ -132,17 +101,17 @@ final class AuthTest: XCTestCase { )) let user = try await auth.user(byEmail: "testGetUserEmail@example.com".lowercased()) - XCTAssertNotNil(user) + #expect(user != nil) } - func testGetUserEmailNotFound() async throws { + @Test func getUserEmailNotFound() async throws { let auth = try makeAuth() let user = try await auth.user(byEmail: "xxxxxx@example.com") - XCTAssertNil(user) + #expect(user == nil) } - func testGetUsers() async throws { + @Test func getUsers() async throws { let auth = try makeAuth() let ids = [ @@ -179,34 +148,33 @@ final class AuthTest: XCTestCase { .email("testGetUsers.3@example.com".lowercased()) ]) - XCTAssertEqual( - Set(users.map { $0.uid }), - [ids[0], ids[1], ids[3]] + #expect( + Set(users.map { $0.uid }) == [ids[0], ids[1], ids[3]] ) } - func testGetUsersEmpty() async throws { + @Test func getUsersEmpty() async throws { let auth = try makeAuth() let users = try await auth.users(for: []) - XCTAssertEqual(users.count, 0) + #expect(users.count == 0) } - func testUpdateUserConsistentID() async throws { + @Test func updateUserConsistentID() async throws { let auth = try makeAuth() let uid = try await auth.createUser( UserToCreate(email: "testUpdateUserID@example.com", password: "123456") ).get() let user0o = try await auth.user(for: uid) - let user0 = try XCTUnwrap(user0o) - XCTAssertFalse(user0.disabled) + let user0 = try #require(user0o) + #expect(!user0.disabled) try await auth.updateUser(.init(disabled: true), for: uid).get() let user1o = try await auth.user(for: uid) - let user1 = try XCTUnwrap(user1o) - XCTAssertEqual(user1.uid, uid) - XCTAssertTrue(user1.disabled) + let user1 = try #require(user1o) + #expect(user1.uid == uid) + #expect(user1.disabled) } private func runUpdateUser( @@ -228,49 +196,48 @@ final class AuthTest: XCTestCase { case .success: break } let usero = try await auth.user(for: uid0) - return .success(try XCTUnwrap(usero)) + return .success(try #require(usero)) } - func testUpdateUserDisplayName() async throws { + @Test func updateUserDisplayName() async throws { let u = try await runUpdateUser( properties: .init(displayName: .set("dog")) ).get() - XCTAssertEqual(u.displayName, "dog") + #expect(u.displayName == "dog") } - func testUpdateUserDeleteDisplayName() async throws { + @Test func updateUserDeleteDisplayName() async throws { let u = try await runUpdateUser( properties: .init(displayName: .delete) ).get() - XCTAssertEqual(u.displayName, nil) + #expect(u.displayName == nil) } - func testUpdateUserErrorEmptyDisplayName() async throws { - let error = try await XCTUnwrap { - try await runUpdateUser( - properties: .init(displayName: .set("")) - ).failure - } - XCTAssertEqual(error.code, .invalidDisplayName) - XCTAssertEqual(error.message, "display name must be a non-empty string") + @Test func updateUserErrorEmptyDisplayName() async throws { + let result = try await runUpdateUser( + properties: .init(displayName: .set("")) + ) + let error = try #require(result.failure) + #expect(error.code == .invalidDisplayName) + #expect(error.message == "display name must be a non-empty string") } - func testUpdateUserEmail() async throws { + @Test func updateUserEmail() async throws { let u = try await runUpdateUser( properties: .init(email: "testUpdateUserEmail.updated@example.com") ).get() - XCTAssertEqual(u.email, "testUpdateUserEmail.updated@example.com".lowercased()) + #expect(u.email == "testUpdateUserEmail.updated@example.com".lowercased()) } - func testUpdateUserDeletePhoneNumber() async throws { + @Test func updateUserDeletePhoneNumber() async throws { let u = try await runUpdateUser( create: { $0.phoneNumber = "+81-090-1234-1234" }, properties: .init(phoneNumber: .delete) ).get() - XCTAssertEqual(u.phoneNumber, nil) + #expect(u.phoneNumber == nil) } - func testUpdateUserPassword() async throws { + @Test func updateUserPassword() async throws { let u = try await runUpdateUser( properties: .init(password: "987654") ).get() @@ -278,29 +245,28 @@ final class AuthTest: XCTestCase { _ = u } - func testUpdateUserPhotoURL() async throws { + @Test func updateUserPhotoURL() async throws { let u = try await runUpdateUser( properties: .init(photoURL: .set("https://example.com/cat.jpeg")) ).get() - XCTAssertEqual(u.photoURL, "https://example.com/cat.jpeg") + #expect(u.photoURL == "https://example.com/cat.jpeg") } - func testUpdateUserErrorInvalidEmail() async throws { + @Test func updateUserErrorInvalidEmail() async throws { let auth = try makeAuth() let id = try await auth.createUser(.init( email: "testUpdateUserErrorInvalidEmail@example.com", password: "123456" )).get() - let error = try await XCTUnwrap { - try await auth.updateUser( - .init(email: "a"), for: id - ).failure - } - XCTAssertEqual(error.code, .invalidEmail) - XCTAssertEqual(error.message, "malformed email string: a") + let result = try await auth.updateUser( + .init(email: "a"), for: id + ) + let error = try #require(result.failure) + #expect(error.code == .invalidEmail) + #expect(error.message == "malformed email string: a") } - func testUpdateUserErrorEmailExists() async throws { + @Test func updateUserErrorEmailExists() async throws { let auth = try makeAuth() let email0 = "testUpdateUserErrorEmailExists.0@example.com" @@ -310,45 +276,41 @@ final class AuthTest: XCTestCase { let id = try await auth.createUser(.init(email: email1, password: "123456")).get() let result = try await auth.updateUser(.init(email: email0), for: id) - let error: UpdateUserError = try XCTUnwrap(result.failure) - XCTAssertEqual(error.code, .emailExists) - XCTAssertNil(error.message) + let error: UpdateUserError = try #require(result.failure) + #expect(error.code == .emailExists) + #expect(error.message == nil) } - func testUpdateUserErrorInvalidPhoneNumber() async throws { + @Test func updateUserErrorInvalidPhoneNumber() async throws { let auth = try makeAuth() - let id = try await auth.createUser( - .init( - email: "testUpdateUserErrorInvalidPhoneNumber@example.com", - password: "123456" - ) - ).get() - let error = try await XCTUnwrap { - try await auth.updateUser( - .init(phoneNumber: .set("aaa")), for: id - ).failure - } - XCTAssertEqual(error.code, .invalidPhoneNumber) - XCTAssertEqual(error.message, "phone number must be a valid, E.164 compliant identifier") + let id = try await auth.createUser(.init( + email: "testUpdateUserErrorInvalidPhoneNumber@example.com", + password: "123456" + )).get() + let result = try await auth.updateUser( + .init(phoneNumber: .set("aaa")), for: id + ) + let error = try #require(result.failure) + #expect(error.code == .invalidPhoneNumber) + #expect(error.message == "phone number must be a valid, E.164 compliant identifier") } - func testUpdateUserErrorEmptyPhotoURL() async throws { + @Test func updateUserErrorEmptyPhotoURL() async throws { let auth = try makeAuth() let id = try await auth.createUser(.init( email: "testUpdateUserErrorInvalidPhotoURL@example.com", password: "123456" )).get() - let error = try await XCTUnwrap { - try await auth.updateUser( - .init(photoURL: .set("")), for: id - ).failure - } - XCTAssertEqual(error.code, .invalidPhotoURL) - XCTAssertEqual(error.message, "photoURL must be a non-empty string") + let result = try await auth.updateUser( + .init(photoURL: .set("")), for: id + ) + let error = try #require(result.failure) + #expect(error.code == .invalidPhotoURL) + #expect(error.message == "photoURL must be a non-empty string") } - func testUpdateUserErrorWeakPassword() async throws { + @Test func updateUserErrorWeakPassword() async throws { let auth = try makeAuth() let id = try await auth.createUser(.init( @@ -356,50 +318,42 @@ final class AuthTest: XCTestCase { password: "123456" )).get() let result = try await auth.updateUser(.init(password: "123"), for: id) - let error: UpdateUserError = try XCTUnwrap(result.failure) - XCTAssertEqual(error.code, .weakPassword) - XCTAssertEqual(error.message, "password must be a string at least 6 characters long") + let error: UpdateUserError = try #require(result.failure) + #expect(error.code == .weakPassword) + #expect(error.message == "password must be a string at least 6 characters long") } - func testSetCustomClaims() async throws { + @Test func setCustomClaims() async throws { let auth = try makeAuth() - let uid = try await XCTAssertNoThrow { - try await auth.createUser(UserToCreate( - email: "testSetCustomClaims@example.com", - password: "012345" - )).get() - } + let uid = try await auth.createUser(UserToCreate( + email: "testSetCustomClaims@example.com", + password: "012345" + )).get() - await XCTAssertNoThrow { - try await auth.setCustomUserClaims([ - "key1": "value1", - "key2": "value2", - ], for: uid) - } + try await auth.setCustomUserClaims([ + "key1": "value1", + "key2": "value2", + ], for: uid) - let result = try await XCTUnwrap { - try await auth.user(for: uid) - } - XCTAssertEqual(result.customClaims["key1"], "value1") - XCTAssertEqual(result.customClaims["key2"], "value2") + let result = try #require(try await auth.user(for: uid)) + #expect(result.customClaims["key1"] == "value1") + #expect(result.customClaims["key2"] == "value2") } - func testDeleteUser() async throws { + @Test func deleteUser() async throws { let auth = try makeAuth() let uid = try await auth.createUser(UserToCreate( email: "testDeleteUser_\(#line)@example.com", password: "012345" )).get() - let userBeforeRemoved = try await XCTAssertNoThrow { try await auth.user(for: uid) } - XCTAssertNotNil(userBeforeRemoved) + let userBeforeRemoved = try await auth.user(for: uid) + #expect(userBeforeRemoved != nil) - await XCTAssertNoThrow { - try await auth.deleteUser(for: uid) - } + try await auth.deleteUser(for: uid) - let userRemoved = try await XCTAssertNoThrow { try await auth.user(for: uid) } - XCTAssertNil(userRemoved) + let userRemoved = try await auth.user(for: uid) + #expect(userRemoved == nil) } } diff --git a/Tests/SharedTests/Auth/Entity/FirebaseAuthTokenTest.swift b/Tests/SharedTests/Auth/Entity/FirebaseAuthTokenTest.swift index 84d06e5..076061e 100644 --- a/Tests/SharedTests/Auth/Entity/FirebaseAuthTokenTest.swift +++ b/Tests/SharedTests/Auth/Entity/FirebaseAuthTokenTest.swift @@ -1,14 +1,15 @@ @testable import FirebaseAdmin -import XCTest +import Foundation +import Testing -final class FirebaseAuthTokenTest: XCTestCase { +@Suite struct FirebaseAuthTokenTest { func makeDecoder() -> JSONDecoder { let d = JSONDecoder() d.dateDecodingStrategy = .secondsSince1970 return d } - func testCustomClaim() throws { + @Test func customClaim() throws { let json = """ { "name": "Kenta Okamura", @@ -36,8 +37,8 @@ final class FirebaseAuthTokenTest: XCTestCase { let decoded = try makeDecoder().decode(FirebaseAuthToken.self, from: json) - XCTAssertEqual(decoded.name, "Kenta Okamura") - XCTAssertEqual(decoded.claims["mykey"], "custom claim") - XCTAssertTrue(decoded.isEmailVerified) + #expect(decoded.name == "Kenta Okamura") + #expect(decoded.claims["mykey"] == "custom claim") + #expect(decoded.isEmailVerified) } } diff --git a/Tests/SharedTests/Auth/Entity/UserRecordTest.swift b/Tests/SharedTests/Auth/Entity/UserRecordTest.swift index 20f5582..aee9529 100644 --- a/Tests/SharedTests/Auth/Entity/UserRecordTest.swift +++ b/Tests/SharedTests/Auth/Entity/UserRecordTest.swift @@ -1,14 +1,15 @@ @testable import FirebaseAdmin -import XCTest +import Foundation +import Testing -final class UserRecordTest: XCTestCase { +@Suite struct UserRecordTest { func makeDecoder() -> JSONDecoder { let d = JSONDecoder() d.dateDecodingStrategy = .secondsSince1970 return d } - func testGetAccountInfoResponse() throws { + @Test func getAccountInfoResponse() throws { struct Response: Decodable { var users: [UserRecord] } @@ -40,13 +41,12 @@ final class UserRecordTest: XCTestCase { """.data(using: .utf8)! let decoded = try makeDecoder().decode(Response.self, from: json) - let user = try XCTUnwrap(decoded.users.first) - XCTAssertEqual(user.providers.first?.providerID, "password") - XCTAssertGreaterThan(user.createdAt, Date(timeIntervalSince1970: 1638245305)) + let user = try #require(decoded.users.first) + #expect(user.providers.first?.providerID == "password") + #expect(user.createdAt > Date(timeIntervalSince1970: 1638245305)) } - - func testManyAttributes() throws { + @Test func manyAttributes() throws { let json = """ { "localId": "yE2sJxMG6JDacAccEOoDyIgYPyzG", @@ -83,11 +83,10 @@ final class UserRecordTest: XCTestCase { let user = try makeDecoder().decode(UserRecord.self, from: json) - XCTAssertEqual(user.providers.first?.providerID, "password") - XCTAssertTrue(user.emailVerified) - XCTAssertGreaterThan(user.createdAt, Date(timeIntervalSince1970: 1638245305)) - XCTAssertEqual(user.customClaims["custom"], "value") - XCTAssertGreaterThan(try XCTUnwrap(user.lastRefreshAt), Date(timeIntervalSince1970: 1638245305)) - + #expect(user.providers.first?.providerID == "password") + #expect(user.emailVerified) + #expect(user.createdAt > Date(timeIntervalSince1970: 1638245305)) + #expect(user.customClaims["custom"] == "value") + #expect(try #require(user.lastRefreshAt) > Date(timeIntervalSince1970: 1638245305)) } } diff --git a/Tests/SharedTests/Auth/Entity/UserToCreateTest.swift b/Tests/SharedTests/Auth/Entity/UserToCreateTest.swift index 1761142..ef7bf34 100644 --- a/Tests/SharedTests/Auth/Entity/UserToCreateTest.swift +++ b/Tests/SharedTests/Auth/Entity/UserToCreateTest.swift @@ -1,64 +1,64 @@ @testable import FirebaseAdmin -import XCTest +import Testing -final class UserToCreateTest: XCTestCase { - func testPasswordValidation() throws { +@Suite struct UserToCreateTest { + @Test func passwordValidation() throws { let c1 = UserToCreate(password: "012345") - XCTAssertNil(c1.validatedRequest().failure) + #expect(c1.validatedRequest().failure == nil) let c2 = UserToCreate(password: "short") - let error = try XCTUnwrap(c2.validatedRequest().failure) - XCTAssertEqual(error.code, .weakPassword) - XCTAssertEqual(error.message, "password must be a string at least 6 characters long") + let error = try #require(c2.validatedRequest().failure) + #expect(error.code == .weakPassword) + #expect(error.message == "password must be a string at least 6 characters long") } - func testPhoneNumberValidation() throws { + @Test func phoneNumberValidation() throws { let c1 = UserToCreate(phoneNumber: "+15555550100") - XCTAssertNil(c1.validatedRequest().failure) + #expect(c1.validatedRequest().failure == nil) let c2 = UserToCreate(phoneNumber: "15555550100") - var error = try XCTUnwrap(c2.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidPhoneNumber) - XCTAssertEqual(error.message, "phone number must be a valid, E.164 compliant identifier") + var error = try #require(c2.validatedRequest().failure) + #expect(error.code == .invalidPhoneNumber) + #expect(error.message == "phone number must be a valid, E.164 compliant identifier") let c3 = UserToCreate(phoneNumber: "+_!@#$") - error = try XCTUnwrap(c3.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidPhoneNumber) - XCTAssertEqual(error.message, "phone number must be a valid, E.164 compliant identifier") + error = try #require(c3.validatedRequest().failure) + #expect(error.code == .invalidPhoneNumber) + #expect(error.message == "phone number must be a valid, E.164 compliant identifier") let c4 = UserToCreate(phoneNumber: "") - error = try XCTUnwrap(c4.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidPhoneNumber) - XCTAssertEqual(error.message, "phone number must be a non-empty string") + error = try #require(c4.validatedRequest().failure) + #expect(error.code == .invalidPhoneNumber) + #expect(error.message == "phone number must be a non-empty string") } - func testEmailValidation() throws { + @Test func emailValidation() throws { let c1 = UserToCreate(email: "a@a.com") - XCTAssertNil(c1.validatedRequest().failure) + #expect(c1.validatedRequest().failure == nil) let c2 = UserToCreate(email: "") - var error = try XCTUnwrap(c2.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidEmail) - XCTAssertEqual(error.message, "email must be a non-empty string") + var error = try #require(c2.validatedRequest().failure) + #expect(error.code == .invalidEmail) + #expect(error.message == "email must be a non-empty string") let c3 = UserToCreate(email: "a") - error = try XCTUnwrap(c3.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidEmail) - XCTAssertEqual(error.message, "malformed email string: a") + error = try #require(c3.validatedRequest().failure) + #expect(error.code == .invalidEmail) + #expect(error.message == "malformed email string: a") let c4 = UserToCreate(email: "a@") - error = try XCTUnwrap(c4.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidEmail) - XCTAssertEqual(error.message, "malformed email string: a@") + error = try #require(c4.validatedRequest().failure) + #expect(error.code == .invalidEmail) + #expect(error.message == "malformed email string: a@") let c5 = UserToCreate(email: "@a.") - error = try XCTUnwrap(c5.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidEmail) - XCTAssertEqual(error.message, "malformed email string: @a.") + error = try #require(c5.validatedRequest().failure) + #expect(error.code == .invalidEmail) + #expect(error.message == "malformed email string: @a.") let c6 = UserToCreate(email: "a@a@a") - error = try XCTUnwrap(c6.validatedRequest().failure) - XCTAssertEqual(error.code, .invalidEmail) - XCTAssertEqual(error.message, "malformed email string: a@a@a") + error = try #require(c6.validatedRequest().failure) + #expect(error.code == .invalidEmail) + #expect(error.message == "malformed email string: a@a@a") } } diff --git a/Tests/SharedTests/Auth/HTTPKeySourceTest.swift b/Tests/SharedTests/Auth/HTTPKeySourceTest.swift index 0f48026..12cc272 100644 --- a/Tests/SharedTests/Auth/HTTPKeySourceTest.swift +++ b/Tests/SharedTests/Auth/HTTPKeySourceTest.swift @@ -1,28 +1,24 @@ import AsyncHTTPClient import Atomics +import Foundation @testable import FirebaseAdmin -import XCTest +import Testing -final class HTTPKeySourceTest: XCTestCase { +@Suite struct HTTPKeySourceTest { private let mockClock: MockClock = .init() private func makeKeySource() -> HTTPKeySource { - let client = HTTPClient(eventLoopGroupProvider: .singleton) - addTeardownBlock { - try! client.syncShutdown() - } - return HTTPKeySource(client: client, clock: mockClock) + return HTTPKeySource(clock: mockClock) } - func testFetchKeys() async throws { + @Test func fetchKeys() async throws { let source = makeKeySource() let keys = try await source.publicKeys() - let key = try await keys.getKey() - XCTAssertNotNil(key) + _ = try await keys.getKey() } - func testRefreshKeys() async throws { + @Test func refreshKeys() async throws { mockClock.nowValue = Date() let source = makeKeySource() @@ -32,36 +28,31 @@ final class HTTPKeySourceTest: XCTestCase { } do { - let keys = try await source.publicKeys() - XCTAssertNotNil(keys) - XCTAssertEqual(refreshCalled.load(ordering: .relaxed), 1) + let _ = try await source.publicKeys() + #expect(refreshCalled.load(ordering: .relaxed) == 1) } do { - let keys = try await source.publicKeys() - XCTAssertNotNil(keys) - XCTAssertEqual(refreshCalled.load(ordering: .relaxed), 1) + let _ = try await source.publicKeys() + #expect(refreshCalled.load(ordering: .relaxed) == 1) } do { - let keys = try await source.publicKeys() - XCTAssertNotNil(keys) - XCTAssertEqual(refreshCalled.load(ordering: .relaxed), 1) + let _ = try await source.publicKeys() + #expect(refreshCalled.load(ordering: .relaxed) == 1) } mockClock.nowValue?.addTimeInterval(60 * 60 * 24) do { - let keys = try await source.publicKeys() - XCTAssertNotNil(keys) - XCTAssertEqual(refreshCalled.load(ordering: .relaxed), 2) + let _ = try await source.publicKeys() + #expect(refreshCalled.load(ordering: .relaxed) == 2) } do { - let keys = try await source.publicKeys() - XCTAssertNotNil(keys) - XCTAssertEqual(refreshCalled.load(ordering: .relaxed), 2) + let _ = try await source.publicKeys() + #expect(refreshCalled.load(ordering: .relaxed) == 2) } } - func testParallelAccess() async throws { + @Test func parallelAccess() async throws { mockClock.nowValue = Date() let source = makeKeySource() @@ -80,6 +71,6 @@ final class HTTPKeySourceTest: XCTestCase { try await g.waitForAll() } - XCTAssertEqual(refreshCalled.load(ordering: .relaxed), 1) + #expect(refreshCalled.load(ordering: .relaxed) == 1) } } diff --git a/Tests/SharedTests/Auth/ListUserTest.swift b/Tests/SharedTests/Auth/ListUserTest.swift index d8f2143..c197eba 100644 --- a/Tests/SharedTests/Auth/ListUserTest.swift +++ b/Tests/SharedTests/Auth/ListUserTest.swift @@ -1,55 +1,24 @@ +import AsyncHTTPClient @testable import FirebaseAdmin import NIOPosix -import XCTest +import Testing import Logging private let testingProjectID = "testing-project-id" -final class ListUserTest: XCTestCase { - private static let client = try! GCPClient(credentialFactory: .custom { _ in - MockCredential() - }) - - private static nonisolated(unsafe) var emulatorURL: URL? = Auth.emulatorBaseURL() - - override class func setUp() { - super.setUp() - initLogger() - - if let url = Self.emulatorURL { - let endpoint = Auth.emulatorAPIBaseURL(url: url)!.appendingPathComponent("projects/\(testingProjectID)/accounts") - do { - let request = try HTTPClient.Request(url: endpoint, method: .DELETE) - _ = try client.httpClient.execute(request: request).wait() - } catch { - XCTFail("\(error)") - } - } - } - - override class func tearDown() { - do { - try client.syncShutdown() - } catch { - XCTFail("\(error)") - } - - super.tearDown() - } - - override func setUpWithError() throws { - try super.setUpWithError() - try XCTSkipIf(Self.emulatorURL == nil, "ListUserTest uses Firebase Auth Emulator.") - } +extension AuthTest { + struct ListUserTest {} +} +extension AuthTest.ListUserTest { private func makeAuth() throws -> Auth { try Auth( - client: Self.client, + client: .mockCredentialClient, projectID: testingProjectID ) } - func testListUser() async throws { + @Test func listUser() async throws { let auth = try makeAuth() for index in 0..<90 { @@ -69,13 +38,14 @@ final class ListUserTest: XCTestCase { callCount += 1 let result = try await auth.listUsers(pageSize: 20, pageToken: pageToken).get() if callCount == 5 { - XCTAssertEqual(result.users.count, 10) + #expect(result.users.count == 10) } else { - XCTAssertEqual(result.users.count, 20) + #expect(result.users.count == 20) } for user in result.users { - let index = Int(user.displayName!)! - XCTAssertEqual(user.email, "\(index)@firebase.com") + let displayName = try #require(user.displayName) + let index = try #require(Int(displayName)) + #expect(user.email == "\(displayName)@firebase.com") founds[index] = user } guard let nextPageToken = result.nextPageToken else { @@ -84,9 +54,9 @@ final class ListUserTest: XCTestCase { pageToken = nextPageToken } - XCTAssertEqual(callCount, 5) + #expect(callCount == 5) let foundIDs = Set(founds.keys) - XCTAssertEqual(foundIDs, Set(0..<90)) + #expect(foundIDs == Set(0..<90)) } } diff --git a/Tests/SharedTests/Base/AutoRotatingValueTest.swift b/Tests/SharedTests/Base/AutoRotatingValueTest.swift index 1210daa..c80a47b 100644 --- a/Tests/SharedTests/Base/AutoRotatingValueTest.swift +++ b/Tests/SharedTests/Base/AutoRotatingValueTest.swift @@ -1,23 +1,24 @@ import Atomics +import Foundation @testable import GoogleCloudBase import NIOPosix -import XCTest +import Testing -final class AutoRotatingValueTest: XCTestCase { +@Suite struct AutoRotatingValueTest { private let mockClock = MockClock() - func testGetValue() async throws { + @Test func getValue() async throws { let counter = ManagedAtomic(0) let store = AutoRotatingValue { counter.wrappingIncrement(ordering: .relaxed) return ("hello", .seconds(1)) } let token = try await store.getValue() - XCTAssertEqual(token, "hello") - XCTAssertEqual(counter.load(ordering: .relaxed), 1) + #expect(token == "hello") + #expect(counter.load(ordering: .relaxed) == 1) } - func testRefreshValue() async throws { + @Test func refreshValue() async throws { let counter = ManagedAtomic(0) let store = AutoRotatingValue(clock: mockClock) { counter.wrappingIncrement(ordering: .relaxed) @@ -26,17 +27,17 @@ final class AutoRotatingValueTest: XCTestCase { mockClock.nowValue = Date() _ = try await store.getValue() - XCTAssertEqual(counter.load(ordering: .relaxed), 1) + #expect(counter.load(ordering: .relaxed) == 1) _ = try await store.getValue() - XCTAssertEqual(counter.load(ordering: .relaxed), 1) + #expect(counter.load(ordering: .relaxed) == 1) mockClock.nowValue?.addTimeInterval(2) _ = try await store.getValue() - XCTAssertEqual(counter.load(ordering: .relaxed), 2) + #expect(counter.load(ordering: .relaxed) == 2) } - func testParallelAccess() async throws { + @Test func parallelAccess() async throws { let counter = ManagedAtomic(0) let store = AutoRotatingValue(clock: mockClock) { counter.wrappingIncrement(ordering: .relaxed) @@ -54,6 +55,6 @@ final class AutoRotatingValueTest: XCTestCase { try await g.waitForAll() } - XCTAssertEqual(counter.load(ordering: .relaxed), 1) + #expect(counter.load(ordering: .relaxed) == 1) } } diff --git a/Tests/SharedTests/BigQuery/BigQueryTest.swift b/Tests/SharedTests/BigQuery/BigQueryTest.swift index dd9f3f4..d53670a 100644 --- a/Tests/SharedTests/BigQuery/BigQueryTest.swift +++ b/Tests/SharedTests/BigQuery/BigQueryTest.swift @@ -1,40 +1,20 @@ +import Foundation @testable import GoogleCloud import NIOPosix -import XCTest - -final class BigQueryTest: XCTestCase { - private static let client = try! GCPClient(credentialFactory: .custom { _ in - MockCredential() - }) - - override class func setUp() { - super.setUp() - initLogger() - } - - override class func tearDown() { - do { - try client.syncShutdown() - } catch { - XCTFail("\(error)") - } - - super.tearDown() - } - - override func setUpWithError() throws { - try super.setUpWithError() - try XCTSkipIf(ProcessInfo.processInfo.environment[bigqueryEmulatorHostEnvVar] == nil, "BigQueryTest uses BigQuery Emulator.") - } +import Testing +@Suite( + .gcpClient, + .enabled(if: ProcessInfo.processInfo.environment[bigqueryEmulatorHostEnvVar] != nil, "BigQueryTest uses BigQuery Emulator.") +) struct BigQueryTest { private func makeBigQuery() -> BigQuery { BigQuery( - client: Self.client, + client: .mockCredentialClient, projectID: "testing-project-id" ) } - func testBigQueryQueryResponseDecode() throws { + @Test func bigQueryQueryResponseDecode() throws { let data = #""" {"jobReference":{"jobId":"coyNAgTg6wfkrPHVDRqlyhov5UQ","projectId":"testing-project-id"},"schema":{"fields":[{"name":"id","type":"INTEGER"},{"name":"name","type":"STRING"},{"fields":[{"name":"key","type":"STRING"},{"name":"value","type":"JSON"}],"mode":"REPEATED","name":"structarr","type":"RECORD"},{"name":"birthday","type":"DATE"},{"name":"skillNum","type":"NUMERIC"},{"name":"created_at","type":"TIMESTAMP"}]},"rows":[{"f":[{"v":"1"},{"v":"alice"},{"v":[{"v":{"f":[{"v":"profile"},{"v":"{\"age\": 10}"}]}}]},{"v":"2012-01-01"},{"v":"3"},{"v":"1641038400.0"}]},{"f":[{"v":"2"},{"v":"bob"},{"v":[{"v":{"f":[{"v":"profile"},{"v":"{\"age\": 15}"}]}}]},{"v":"2007-02-01"},{"v":null},{"v":"1641146400.0"}]}],"totalRows":"2","jobComplete":true} """#.data(using: .utf8)! @@ -42,7 +22,7 @@ final class BigQueryTest: XCTestCase { let _ = try JSONDecoder().decode(BigQueryQueryResponse.self, from: data) } - func testBasic() async throws { + @Test func basic() async throws { let bigQuery = makeBigQuery() struct Row: Decodable { @@ -56,15 +36,15 @@ final class BigQueryTest: XCTestCase { decoding: Row.self ) - XCTAssertEqual(rows.count, 2) - XCTAssertEqual(rows[0].id, 1) - XCTAssertEqual(rows[0].name, "alice") - XCTAssertEqual(rows[0].skillNum, 3) - XCTAssertEqual(rows[0].created_at, Date(timeIntervalSince1970: 1641038400)) - XCTAssertEqual(rows[1].skillNum, nil) + #expect(rows.count == 2) + #expect(rows[0].id == 1) + #expect(rows[0].name == "alice") + #expect(rows[0].skillNum == 3) + #expect(rows[0].created_at == Date(timeIntervalSince1970: 1641038400)) + #expect(rows[1].skillNum == nil) } - func testDecodePrimitives() async throws { + @Test func decodePrimitives() async throws { let bigQuery = makeBigQuery() struct Row: Decodable { @@ -88,17 +68,17 @@ final class BigQueryTest: XCTestCase { , CURRENT_DATETIME() AS `datetime` """, decoding: Row.self) - XCTAssertEqual(rows.count, 1) - XCTAssertEqual(rows[0].bool, true) - XCTAssertEqual(rows[0].int, 1) - XCTAssertEqual(rows[0].float, 0.5) - XCTAssertEqual(rows[0].string, "hello") - XCTAssertEqual(rows[0].optional, nil) - XCTAssertEqual(rows[0].timestamp.timeIntervalSince1970, now.timeIntervalSince1970, accuracy: 1) - XCTAssertEqual(rows[0].datetime.timeIntervalSince1970, now.timeIntervalSince1970, accuracy: 1) + #expect(rows.count == 1) + #expect(rows[0].bool == true) + #expect(rows[0].int == 1) + #expect(rows[0].float == 0.5) + #expect(rows[0].string == "hello") + #expect(rows[0].optional == nil) + #expect(abs(rows[0].timestamp.timeIntervalSince1970 - now.timeIntervalSince1970) < 1) + #expect(abs(rows[0].datetime.timeIntervalSince1970 - now.timeIntervalSince1970) < 1) } - func testParameter() async throws { + @Test func parameter() async throws { let bigQuery = makeBigQuery() struct Row: Decodable { @@ -112,7 +92,7 @@ final class BigQueryTest: XCTestCase { decoding: Row.self ) - XCTAssertEqual(rows.count, 1) - XCTAssertEqual(rows[0].id, 1) + #expect(rows.count == 1) + #expect(rows[0].id == 1) } } diff --git a/Tests/SharedTests/Core/GCPClientTest.swift b/Tests/SharedTests/Core/GCPClientTest.swift index 09aceda..3c97e5e 100644 --- a/Tests/SharedTests/Core/GCPClientTest.swift +++ b/Tests/SharedTests/Core/GCPClientTest.swift @@ -1,10 +1,10 @@ import GoogleCloudBase -func testGCPClientOverload() throws { +func gcpClientOverload() throws { _ = try GCPClient(credentialFactory: .selector(.environment, .configFile)) } -func testGCPClientOverloadAsync() async throws { +func gcpClientOverloadAsync() async throws { _ = try await GCPClient() _ = try await GCPClient(credentialFactory: .applicationDefault) } diff --git a/Tests/SharedTests/Storage/BucketTest.swift b/Tests/SharedTests/Storage/BucketTest.swift index d22266d..774eed9 100644 --- a/Tests/SharedTests/Storage/BucketTest.swift +++ b/Tests/SharedTests/Storage/BucketTest.swift @@ -1,54 +1,34 @@ +import Foundation @testable import GoogleCloud import NIOPosix -import XCTest - -final class BucketTest: XCTestCase { - private static let client = try! GCPClient(credentialFactory: .custom { _ in - MockCredential() - }) - - override class func setUp() { - super.setUp() - initLogger() - } - - override class func tearDown() { - do { - try client.syncShutdown() - } catch { - XCTFail("\(error)") - } - - super.tearDown() - } - - override func setUpWithError() throws { - try super.setUpWithError() - try XCTSkipIf(ProcessInfo.processInfo.environment[storageEmulatorHostEnvVar] == nil, "BucketTest uses Cloud Storage Emulator.") - } +import Testing +@Suite( + .gcpClient, + .enabled(if: ProcessInfo.processInfo.environment[storageEmulatorHostEnvVar] != nil, "BucketTest uses Cloud Storage Emulator.") +) struct BucketTest: ~Copyable { private func makeBucket() -> Bucket { - Storage(client: Self.client) + Storage(client: .mockCredentialClient) .bucket(name: "test-bucket") } - func testFiles() async throws { + @Test func files() async throws { let bucket = makeBucket() let response = try await bucket.files(prefix: "testFiles/") - XCTAssertEqual(Set(response.map(\.name)), ["testFiles/bar.png", "testFiles/baz.jpg"]) + #expect(Set(response.map(\.name)) == ["testFiles/bar.png", "testFiles/baz.jpg"]) } - func testUpload() async throws { + @Test func upload() async throws { let bucket = makeBucket() let name = "testUpload/foo??bar.txt" let data = Data(name.utf8) let response = try await bucket.uploadSimple(name: name, media: data) - XCTAssertEqual(response.name, name) + #expect(response.name == name) } - func testDelete() async throws { + @Test func delete() async throws { let bucket = makeBucket() let names = [ @@ -60,11 +40,11 @@ final class BucketTest: XCTestCase { let data = Data(name.utf8) _ = try await bucket.uploadSimple(name: name, media: data) let files = try await bucket.files(prefix: name) - XCTAssertFalse(files.isEmpty) + #expect(!files.isEmpty) try await bucket.delete(name: name) let filesAfter = try await bucket.files(prefix: name) - XCTAssertTrue(filesAfter.isEmpty) + #expect(filesAfter.isEmpty) } } } diff --git a/Tests/SharedTests/Util/GCPClientTrait.swift b/Tests/SharedTests/Util/GCPClientTrait.swift new file mode 100644 index 0000000..3c3bfd4 --- /dev/null +++ b/Tests/SharedTests/Util/GCPClientTrait.swift @@ -0,0 +1,36 @@ +import GoogleCloudBase +import Testing + +extension SuiteTrait where Self == GCPClientTrait { + static var gcpClient: Self { .init() } +} + +extension GCPClient { + @TaskLocal static var mockCredentialClient: GCPClient! +} + +struct GCPClientTrait: SuiteTrait, TestScoping { + func provideScope( + for test: Test, + testCase: Test.Case?, + performing function: @Sendable () async throws -> Void + ) async throws { + initLogger() + + let client = try GCPClient(credentialFactory: .custom { _ in + MockCredential() + }) + + var didShutdown = false + defer { + if !didShutdown { + Task { + try await client.shutdown() + } + } + } + try await GCPClient.$mockCredentialClient.withValue(client, operation: function) + didShutdown = true + try await client.shutdown() + } +} diff --git a/Tests/SharedTests/Util/Result+Extension.swift b/Tests/SharedTests/Util/Result+Extension.swift new file mode 100644 index 0000000..8f369a1 --- /dev/null +++ b/Tests/SharedTests/Util/Result+Extension.swift @@ -0,0 +1,31 @@ +extension Result where Failure == any Error { + init(catching body: () async throws -> Success) async { + do { + self = .success(try await body()) + } catch { + self = .failure(error) + } + } +} + +extension Result where Failure == Never { + init(catching body: () async -> Success) async { + self = .success(await body()) + } +} + +extension Result { + var success: Success? { + switch self { + case .success(let x): return x + default: return nil + } + } + + var failure: Failure? { + switch self { + case .failure(let x): return x + default: return nil + } + } +} diff --git a/Tests/SharedTests/Util/XCTest+Extension.swift b/Tests/SharedTests/Util/XCTest+Extension.swift deleted file mode 100644 index 9590e47..0000000 --- a/Tests/SharedTests/Util/XCTest+Extension.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation -import XCTest - -func XCTUnwrap(_ expression: () async throws -> T?, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) async throws -> T { - let v = await Result { try await expression() } - return try XCTUnwrap(v.get(), message(), file: file, line: line) -} - -extension Result where Failure == any Error { - init(catching body: () async throws -> Success) async { - do { - self = .success(try await body()) - } catch { - self = .failure(error) - } - } -} - -extension Result where Failure == Never { - init(catching body: () async -> Success) async { - self = .success(await body()) - } -} - -extension Result { - var success: Success? { - switch self { - case .success(let x): return x - default: return nil - } - } - - var failure: Failure? { - switch self { - case .failure(let x): return x - default: return nil - } - } -} - -func XCTAssertThrowsError(_ expression: @Sendable () async throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, _ errorHandler: (_ error: any Error) -> Void = { _ in }) async { - let v = await Result { try await expression() } - XCTAssertThrowsError(try v.get(), message(), file: file, line: line, errorHandler) -} - -func XCTAssertNoThrow(_ expression: @Sendable () async throws -> Void, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) async { - let v = await Result { try await expression() } - XCTAssertNoThrow(try v.get(), message(), file: file, line: line) -} - -func XCTAssertNoThrow(_ expression: @Sendable () async throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) async throws -> T { - let v = await Result { try await expression() } - XCTAssertNoThrow(try v.get(), message(), file: file, line: line) - return try v.get() -} - diff --git a/docker-compose.yml b/docker-compose.yml index d5c2274..d6b08cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ services: cloudstorage: image: fsouza/fake-gcs-server - ports: + ports: - "4443:4443" - volumes: + volumes: - ./fixtures/storage:/data command: -scheme http firebase: @@ -15,6 +15,7 @@ services: command: firebase emulators:start --only auth -P testing-project-id bigquery: image: ghcr.io/goccy/bigquery-emulator:latest + platform: linux/amd64 ports: - 9050:9050 volumes: diff --git a/run_test.sh b/run_test.sh index 19b794c..4cbc94b 100755 --- a/run_test.sh +++ b/run_test.sh @@ -3,4 +3,4 @@ export STORAGE_EMULATOR_HOST=127.0.0.1:4443 export FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 export BIGQUERY_EMULATOR_HOST=127.0.0.1:9050 -swift test +swift test "$@"