diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f906730 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "enabledPlugins": { + "NnCredentialKit@nelix-swift-tools": true, + "NnTesting@nelix-swift-tools": true + } +} diff --git a/Tests/NnCredentialKitTests/Shared/CredentialType+TestHelpers.swift b/Tests/NnCredentialKitTests/Shared/CredentialType+TestHelpers.swift new file mode 100644 index 0000000..c70ffbb --- /dev/null +++ b/Tests/NnCredentialKitTests/Shared/CredentialType+TestHelpers.swift @@ -0,0 +1,21 @@ +// +// CredentialType+TestHelpers.swift +// NnCredentialKit +// +// Created by Nikolai Nobadi on 6/6/26. +// + +@testable import NnCredentialKit + +extension CredentialType { + var id: String { + switch self { + case .apple: + return "apple" + case .google: + return "google" + case .emailPassword: + return "emailPassword" + } + } +} diff --git a/Tests/NnCredentialKitTests/UnitTests/AccountDeleterTests.swift b/Tests/NnCredentialKitTests/UnitTests/AccountDeleterTests.swift index 26863df..8615812 100644 --- a/Tests/NnCredentialKitTests/UnitTests/AccountDeleterTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/AccountDeleterTests.swift @@ -10,28 +10,28 @@ import Testing @MainActor struct AccountDeleterTests { - @Test("Throws error when delete account fails") - func throwsErrorOnDeleteAccountFail() async throws { + @Test + func `Throws error when delete account fails`() async throws { let error = TestError.network let sut = makeSUT(firstResult: .failure(error)) - await #expect(throws: TestError.network) { + await #expect(throws: error) { try await sut.deleteAccount() } } - @Test("Throws error after successful reauthentication attempt") - func throwsErrorAfterSuccessfulReauth() async throws { + @Test + func `Throws error after successful reauthentication attempt`() async throws { let error = TestError.postReauthorizationAction let sut = makeSUT(firstResult: .reauthRequired, secondResult: .failure(error)) - await #expect(throws: TestError.postReauthorizationAction) { + await #expect(throws: error) { try await sut.deleteAccount() } } - @Test("Throws error when reauthentication fails") - func throwsErrorWhenReauthenticationFails() async throws { + @Test + func `Throws error when reauthentication fails`() async throws { let sut = makeSUT(firstResult: .reauthRequired, secondResult: .failure(TestError.network), throwReauthError: true) await #expect(throws: TestError.reauth) { @@ -44,7 +44,7 @@ struct AccountDeleterTests { // MARK: - SUT private extension AccountDeleterTests { func makeSUT(firstResult: AccountCredentialResult = .success, secondResult: AccountCredentialResult = .success, throwReauthError: Bool = false) -> AccountDeleter { - let delegate = StubDelegate(firstResult: firstResult, secondResult: secondResult) + let delegate = MockDelegate(firstResult: firstResult, secondResult: secondResult) let auth = MockReauthenticator(throwError: throwReauthError) return AccountDeleter(delegate: delegate, reauthenticator: auth) @@ -52,9 +52,9 @@ private extension AccountDeleterTests { } -// MARK: - Helper Classes +// MARK: - Mocks private extension AccountDeleterTests { - final class StubDelegate: DeleteAccountDelegate, @unchecked Sendable { + final class MockDelegate: DeleteAccountDelegate, @unchecked Sendable { private let store: StubResultStore init(firstResult: AccountCredentialResult, secondResult: AccountCredentialResult) { diff --git a/Tests/NnCredentialKitTests/UnitTests/AccountLinkViewModelTests.swift b/Tests/NnCredentialKitTests/UnitTests/AccountLinkViewModelTests.swift index dfd36e8..2ae5d49 100644 --- a/Tests/NnCredentialKitTests/UnitTests/AccountLinkViewModelTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/AccountLinkViewModelTests.swift @@ -5,8 +5,8 @@ import Testing @MainActor struct AccountLinkViewModelTests { - @Test("SUT starts with empty values") - func startsWithEmptyValues() { + @Test + func `SUT starts with empty values`() { let (sut, delegate) = makeSUT() #expect(sut.providers.isEmpty) @@ -14,86 +14,123 @@ struct AccountLinkViewModelTests { #expect(delegate.credentialType == nil) } - @Test("Links account when provider is not linked") - func linksAccountIfNotLinked() async throws { + @Test + func `Returns success when linking an unlinked provider`() async throws { let provider = makeAuthProvider(.emailPassword) let credential = makeEmailPasswordCredential() - let (sut, delegate) = makeSUT(credentialType: credential) + let sut = makeSUT(credentialType: credential).sut let result = try await sut.linkAction(provider) + #expect(result == .success) + } + + @Test + func `Sends credential to delegate without unlinking when provider is not linked`() async throws { + let provider = makeAuthProvider(.emailPassword) + let credential = makeEmailPasswordCredential() + let (sut, delegate) = makeSUT(credentialType: credential) + + _ = try await sut.linkAction(provider) + let credentialType = try #require(delegate.credentialType) - #expect(result == .success) #expect(delegate.providerType == nil) - #expect(credentialType.id == CredentialType.emailPassword(email: "", password: "").id) + #expect(credentialType.id == credential.id) } - @Test("Throws error when credential provider fails") - func throwsIfCredentialProviderFails() async { + @Test + func `Throws error when credential provider fails`() async { let provider = makeAuthProvider(.emailPassword) - let (sut, delegate) = makeSUT(throwProviderError: true) + let sut = makeSUT(throwProviderError: true).sut await #expect(throws: TestError.credentialTypeProvider) { _ = try await sut.linkAction(provider) } + } + + @Test + func `Does not contact delegate when credential provider fails`() async { + let provider = makeAuthProvider(.emailPassword) + let (sut, delegate) = makeSUT(throwProviderError: true) + + _ = try? await sut.linkAction(provider) #expect(delegate.providerType == nil) #expect(delegate.credentialType == nil) } - @Test("Returns canceled result when credential is nil") - func returnsCanceledWhenCredentialIsNil() async throws { + @Test + func `Returns canceled result when credential is nil`() async throws { let provider = makeAuthProvider(.emailPassword) - let (sut, delegate) = makeSUT() + let sut = makeSUT().sut let result = try await sut.linkAction(provider) #expect(result == .canceled) + } + + @Test + func `Does not contact delegate when credential is nil`() async throws { + let provider = makeAuthProvider(.emailPassword) + let (sut, delegate) = makeSUT() + + _ = try await sut.linkAction(provider) + #expect(delegate.providerType == nil) #expect(delegate.credentialType == nil) } - @Test("Throws error when reauthentication fails") - func throwsWhenReauthenticationFails() async { + @Test + func `Throws error when reauthentication fails`() async { let error = TestError.reauth let provider = makeAuthProvider(.emailPassword) let credential = makeEmailPasswordCredential() - let (sut, _) = makeSUT(credentialType: credential, firstResult: .failure(error)) + let sut = makeSUT(credentialType: credential, firstResult: .failure(error)).sut await #expect(throws: error) { _ = try await sut.linkAction(provider) } } - @Test("Retries linking after successful reauthentication") - func retriesAfterReauth() async { + @Test + func `Retries linking after successful reauthentication`() async { let error = TestError.postReauthorizationAction let provider = makeAuthProvider(.emailPassword) let credential = makeEmailPasswordCredential() - let (sut, _) = makeSUT(credentialType: credential, firstResult: .reauthRequired, secondResult: .failure(error)) + let sut = makeSUT(credentialType: credential, firstResult: .reauthRequired, secondResult: .failure(error)).sut await #expect(throws: error) { _ = try await sut.linkAction(provider) } } - @Test("Unlinks provider when more than one is linked") - func unlinksWhenMultipleProvidersExist() async throws { + @Test + func `Returns success when unlinking with multiple linked providers`() async throws { let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") let secondProvider = makeAuthProvider(.apple, email: "tester@apple.com") - let (sut, delegate) = makeSUT(providers: [provider, secondProvider]) + let sut = makeSUT(providers: [provider, secondProvider]).sut let result = try await sut.linkAction(provider) #expect(result == .success) + } + + @Test + func `Sends provider type to delegate when unlinking with multiple linked providers`() async throws { + let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") + let secondProvider = makeAuthProvider(.apple, email: "tester@apple.com") + let (sut, delegate) = makeSUT(providers: [provider, secondProvider]) + + _ = try await sut.linkAction(provider) + #expect(delegate.providerType == provider.type) } - @Test("Throws when trying to unlink the only linked provider") - func throwsIfUnlinkingOnlyLinkedProvider() async { + @Test + func `Throws when trying to unlink the only linked provider`() async { let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") - let (sut, _) = makeSUT(providers: [provider]) + let sut = makeSUT(providers: [provider]).sut await #expect(throws: CredentialError.cannotUnlinkOnlyProvider) { _ = try await sut.linkAction(provider) @@ -104,43 +141,43 @@ struct AccountLinkViewModelTests { // MARK: - Button Display Tests extension AccountLinkViewModelTests { - @Test("Shows button when prevent flag disabled") - func showsButtonWhenPreventFlagDisabled() { + @Test + func `Shows button when prevent flag disabled`() { let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") - let (sut, _) = makeSUT(providers: [provider], preventUnlinkingLastProvider: false) + let sut = makeSUT(providers: [provider], preventUnlinkingLastProvider: false).sut #expect(sut.shouldShowButton(for: provider)) } - @Test("Shows button for unlinked provider when prevent flag enabled") - func showsButtonForUnlinkedProviderWhenPreventFlagEnabled() { + @Test + func `Shows button for unlinked provider when prevent flag enabled`() { let provider = makeAuthProvider(.emailPassword) - let (sut, _) = makeSUT(providers: [provider], preventUnlinkingLastProvider: true) + let sut = makeSUT(providers: [provider], preventUnlinkingLastProvider: true).sut #expect(sut.shouldShowButton(for: provider)) } - @Test("Shows button for linked provider when multiple providers linked and prevent flag enabled") - func showsButtonForLinkedProviderWhenMultipleProvidersLinkedAndPreventFlagEnabled() { + @Test + func `Shows button for linked provider when multiple providers linked and prevent flag enabled`() { let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") let secondProvider = makeAuthProvider(.apple, email: "tester@apple.com") - let (sut, _) = makeSUT(providers: [provider, secondProvider], preventUnlinkingLastProvider: true) + let sut = makeSUT(providers: [provider, secondProvider], preventUnlinkingLastProvider: true).sut #expect(sut.shouldShowButton(for: provider)) } - @Test("Hides button for linked provider when only provider linked and prevent flag enabled") - func hidesButtonForLinkedProviderWhenOnlyProviderLinkedAndPreventFlagEnabled() { + @Test + func `Hides button for linked provider when only provider linked and prevent flag enabled`() { let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") - let (sut, _) = makeSUT(providers: [provider], preventUnlinkingLastProvider: true) + let sut = makeSUT(providers: [provider], preventUnlinkingLastProvider: true).sut #expect(!sut.shouldShowButton(for: provider)) } - @Test("Shows button for linked provider when only provider linked and prevent flag disabled") - func showsButtonForLinkedProviderWhenOnlyProviderLinkedAndPreventFlagDisabled() { + @Test + func `Shows button for linked provider when only provider linked and prevent flag disabled`() { let provider = makeAuthProvider(.emailPassword, email: "tester@gmail.com") - let (sut, _) = makeSUT(providers: [provider], preventUnlinkingLastProvider: false) + let sut = makeSUT(providers: [provider], preventUnlinkingLastProvider: false).sut #expect(sut.shouldShowButton(for: provider)) } @@ -152,12 +189,16 @@ private extension AccountLinkViewModelTests { func makeSUT(providers: [AuthProvider] = [], credentialType: CredentialType? = nil, firstResult: AccountCredentialResult = .success, secondResult: AccountCredentialResult = .success, throwProviderError: Bool = false, throwReauthError: Bool = false, preventUnlinkingLastProvider: Bool = false) -> (sut: AccountLinkViewModel, delegate: MockDelegate) { let delegate = MockDelegate(firstResult: firstResult, secondResult: secondResult, supportedProviders: providers) let auth = MockReauthenticator(throwError: throwReauthError) - let provider = StubProvider(credentialType: credentialType, throwError: throwProviderError) + let provider = MockCredentialProvider(credentialType: credentialType, throwError: throwProviderError) let sut = AccountLinkViewModel(providers: providers, delegate: delegate, reauthenticator: auth, credentialProvider: provider, preventUnlinkingLastProvider: preventUnlinkingLastProvider) return (sut, delegate) } - +} + + +// MARK: - Helpers +private extension AccountLinkViewModelTests { func makeAuthProvider(_ type: AuthProviderType, email: String = "") -> AuthProvider { return .init(linkedEmail: email, type: type) } @@ -168,9 +209,9 @@ private extension AccountLinkViewModelTests { } -// MARK: - Helper Classes +// MARK: - Mocks private extension AccountLinkViewModelTests { - final class StubProvider: CredentialTypeProvider { + final class MockCredentialProvider: CredentialTypeProvider { private let throwError: Bool private let credentialType: CredentialType? @@ -218,16 +259,3 @@ private extension AccountLinkViewModelTests { func reauthenticate(with credientialType: CredentialType) async throws { } } } - -extension CredentialType { - var id: String { - switch self { - case .apple: - return "apple" - case .google: - return "google" - case .emailPassword: - return "emailPassword" - } - } -} diff --git a/Tests/NnCredentialKitTests/UnitTests/AppleCredentialConverterTests.swift b/Tests/NnCredentialKitTests/UnitTests/AppleCredentialConverterTests.swift index 738cbd6..bb2a2c4 100644 --- a/Tests/NnCredentialKitTests/UnitTests/AppleCredentialConverterTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/AppleCredentialConverterTests.swift @@ -10,8 +10,8 @@ import Foundation @testable import NnCredentialKit struct AppleCredentialConverterTests { - @Test("Converts raw credential to Apple credential info with all fields") - func convertsRawCredentialToAppleCredentialInfoWithAllFields() throws { + @Test + func `Converts raw credential to Apple credential info with all fields`() throws { let email = "test@apple.com" let givenName = "John" let familyName = "Doe" @@ -30,8 +30,8 @@ struct AppleCredentialConverterTests { #expect(result.nonce == nonce) } - @Test("Converts raw credential with minimal fields") - func convertsRawCredentialWithMinimalFields() throws { + @Test + func `Converts raw credential with minimal fields`() throws { let nonce = "test-nonce" let tokenString = "minimal-token" let tokenData = tokenString.data(using: .utf8) @@ -46,8 +46,8 @@ struct AppleCredentialConverterTests { #expect(result.nonce == nonce) } - @Test("Throws error when nonce is nil") - func throwsErrorWhenNonceIsNil() { + @Test + func `Throws error when nonce is nil`() { let raw = makeRawCredential() let sut = makeSUT() @@ -56,8 +56,8 @@ struct AppleCredentialConverterTests { } } - @Test("Throws error when token data cannot be serialized") - func throwsErrorWhenTokenDataCannotBeSerialized() { + @Test + func `Throws error when token data cannot be serialized`() { let nonce = "test-nonce" let raw = makeRawCredential(idTokenData: nil) let sut = makeSUT() @@ -67,8 +67,8 @@ struct AppleCredentialConverterTests { } } - @Test("Throws error when token data is invalid UTF-8") - func throwsErrorWhenTokenDataIsInvalidUTF8() { + @Test + func `Throws error when token data is invalid UTF-8`() { let nonce = "test-nonce" let invalidData = Data([0xFF, 0xFE, 0xFD]) let raw = makeRawCredential(idTokenData: invalidData) @@ -82,8 +82,8 @@ struct AppleCredentialConverterTests { // MARK: - Display Name Tests extension AppleCredentialConverterTests { - @Test("Creates display name from full name with both components") - func createsDisplayNameFromFullNameWithBothComponents() { + @Test + func `Creates display name from full name with both components`() { let givenName = "Jane" let familyName = "Smith" let fullName = makePersonNameComponents(givenName: givenName, familyName: familyName) @@ -93,8 +93,8 @@ extension AppleCredentialConverterTests { #expect(displayName == "\(givenName) \(familyName)") } - @Test("Creates display name with only given name") - func createsDisplayNameWithOnlyGivenName() { + @Test + func `Creates display name with only given name`() { let givenName = "Jane" let fullName = makePersonNameComponents(givenName: givenName, familyName: nil) @@ -103,8 +103,8 @@ extension AppleCredentialConverterTests { #expect(displayName == givenName) } - @Test("Creates display name with only family name") - func createsDisplayNameWithOnlyFamilyName() { + @Test + func `Creates display name with only family name`() { let familyName = "Smith" let fullName = makePersonNameComponents(givenName: nil, familyName: familyName) @@ -113,15 +113,15 @@ extension AppleCredentialConverterTests { #expect(displayName == familyName) } - @Test("Returns empty string when full name is nil") - func returnsEmptyStringWhenFullNameIsNil() { + @Test + func `Returns empty string when full name is nil`() { let displayName = AppleCredentialConverter.displayName(from: nil) #expect(displayName.isEmpty) } - @Test("Trims whitespace from display name") - func trimsWhitespaceFromDisplayName() { + @Test + func `Trims whitespace from display name`() { let fullName = makePersonNameComponents(givenName: " John ", familyName: " Doe ") let displayName = AppleCredentialConverter.displayName(from: fullName) @@ -132,8 +132,8 @@ extension AppleCredentialConverterTests { // MARK: - Token Serialization Tests extension AppleCredentialConverterTests { - @Test("Serializes valid token data to string") - func serializesValidTokenDataToString() throws { + @Test + func `Serializes valid token data to string`() throws { let tokenString = "valid-token-string-12345" let tokenData = tokenString.data(using: .utf8) @@ -142,15 +142,15 @@ extension AppleCredentialConverterTests { #expect(result == tokenString) } - @Test("Throws error when token data is nil") - func throwsErrorWhenTokenDataIsNil() { + @Test + func `Throws error when token data is nil`() { #expect(throws: AppleSignInError.unableToSerializeToken) { try AppleCredentialConverter.serializeToken(nil) } } - @Test("Serializes complex UTF-8 string correctly") - func serializesComplexUTF8StringCorrectly() throws { + @Test + func `Serializes complex UTF-8 string correctly`() throws { let complexString = "Token-with-特殊字符-🔐-and-numbers-123" let tokenData = complexString.data(using: .utf8) @@ -165,7 +165,10 @@ private extension AppleCredentialConverterTests { func makeSUT() -> AppleCredentialConverter { return AppleCredentialConverter() } +} +// MARK: - Helpers +private extension AppleCredentialConverterTests { func makeRawCredential( email: String? = "default@test.com", fullName: PersonNameComponents? = nil, diff --git a/Tests/NnCredentialKitTests/UnitTests/AppleSignInServiceTests.swift b/Tests/NnCredentialKitTests/UnitTests/AppleSignInServiceTests.swift index e0120b1..e4e7bc9 100644 --- a/Tests/NnCredentialKitTests/UnitTests/AppleSignInServiceTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/AppleSignInServiceTests.swift @@ -11,8 +11,8 @@ import AuthenticationServices @MainActor struct AppleSignInServiceTests { - @Test("Uses default scopes and hashed nonce when starting session") - func usesDefaultScopesAndHashedNonce() async throws { + @Test + func `Uses default scopes and hashed nonce when starting session`() async throws { let nonce = "nonce-123" let (sut, session) = makeSUT(nonce: nonce) @@ -23,8 +23,8 @@ struct AppleSignInServiceTests { #expect(captured.nonce == "HASH(\(nonce))") } - @Test("Uses custom scopes and hashed nonce when provided") - func usesCustomScopes() async throws { + @Test + func `Uses custom scopes and hashed nonce when provided`() async throws { let nonce = "abc" let customScopes: [ASAuthorization.Scope] = [.fullName] let (sut, session) = makeSUT(nonce: nonce) @@ -36,24 +36,24 @@ struct AppleSignInServiceTests { #expect(captured.nonce == "HASH(\(nonce))") } - @Test("Returns nil on user cancel from ASAuthorizationError") - func returnsNilOnSystemCancel() async throws { + @Test + func `Returns nil on user cancel from ASAuthorizationError`() async throws { let sut = makeSUT().sut let result = try await sut.createAppleTokenInfo() #expect(result == nil) } - @Test("Returns nil on user cancel from AppleSignInError") - func returnsNilOnCustomCancel() async throws { + @Test + func `Returns nil on user cancel from AppleSignInError`() async throws { let sut = makeSUT(result: .failure(AppleSignInError.canceled)).sut let result = try await sut.createAppleTokenInfo() #expect(result == nil) } - @Test("Throws on non cancel errors") - func throwsOnOtherErrors() async { + @Test + func `Throws on non cancel errors`() async { enum E: Error { case boom } let sut = makeSUT(result: .failure(E.boom)).sut @@ -62,8 +62,8 @@ struct AppleSignInServiceTests { } } - @Test("Returns token info with email and full name from credential") - func returnsTokenInfoWithEmailAndFullNameFromCredential() async throws { + @Test + func `Returns token info with email and full name from credential`() async throws { let email = "tester@gmail.com" let firstName = "mr" let lastName = "sir" @@ -86,7 +86,11 @@ private extension AppleSignInServiceTests { return (sut, session) } +} + +// MARK: - Helpers +private extension AppleSignInServiceTests { func makeRawCredential(email: String? = nil, firstName: String? = nil, lastName: String? = nil, idTokenData: Data? = nil) -> AppleAuthRawCredential { return .init(email: email, fullName: .init(givenName: firstName, familyName: lastName), idTokenData: idTokenData) } diff --git a/Tests/NnCredentialKitTests/UnitTests/CredentialManagerTests.swift b/Tests/NnCredentialKitTests/UnitTests/CredentialManagerTests.swift index 4ae923a..4c6cc8e 100644 --- a/Tests/NnCredentialKitTests/UnitTests/CredentialManagerTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/CredentialManagerTests.swift @@ -10,34 +10,34 @@ import Testing @MainActor struct CredentialManagerTests { - @Test("Returns nil when user cancels sign-in", arguments: AuthProviderType.allCases) - func returnsNilOnUserCancel(providerType: AuthProviderType) async throws { + @Test(arguments: AuthProviderType.allCases) + func `Returns nil when user cancels sign-in`(providerType: AuthProviderType) async throws { let sut = makeSUT() let result = try await sut.loadCredential(providerType) #expect(result == nil) } - @Test("Loads Apple credential for .apple") - func loadsAppleCredential() async throws { + @Test + func `Loads Apple credential for .apple`() async throws { let credential = makeAppleCredential() let sut = makeSUT(appleCredential: credential) let result = try #require(try await sut.loadCredential(.apple)) - #expect(result.id == CredentialType.apple(.init(email: nil, displayName: nil, idTokenString: "", nonce: "")).id) + #expect(result.id == CredentialType.apple(credential).id) } - @Test("Loads Google credential for .google") - func loadsGoogleCredential() async throws { + @Test + func `Loads Google credential for .google`() async throws { let credential = makeGoogleCredential() let sut = makeSUT(googleCredential: credential) let result = try #require(try await sut.loadCredential(.google)) - #expect(result.id == CredentialType.google(makeGoogleCredential()).id) + #expect(result.id == CredentialType.google(credential).id) } - @Test("Loads email credential for .emailPassword") - func loadsEmailCredential() async throws { + @Test + func `Loads email credential for .emailPassword`() async throws { let info = makeEmailInfo() let sut = makeSUT(info: info) let result = try #require(try await sut.loadCredential(.emailPassword)) @@ -45,8 +45,8 @@ struct CredentialManagerTests { #expect(result.id == CredentialType.emailPassword(email: info.email, password: info.password).id) } - @Test("Throws if email password confirm does not match") - func throwsWhenPasswordsDoNotMatch() async { + @Test + func `Throws if email password confirm does not match`() async { let sut = makeSUT(info: makeEmailInfo(password: "one", confirm: "two")) await #expect(throws: CredentialError.passwordsMustMatch) { @@ -54,46 +54,47 @@ struct CredentialManagerTests { } } - @Test("Returns nil on reauthentication cancel") - func returnsNilOnReauthCancel() async throws { + @Test(arguments: AuthProviderType.allCases) + func `Returns nil on reauthentication cancel`(providerType: AuthProviderType) async throws { let linked = makeLinkedProviders() - for type in AuthProviderType.allCases { - let sut = makeSUT(selectedProvider: .init(linkedEmail: "", type: type)) - let result = try await sut.loadReauthCredential(linkedProviders: linked) - #expect(result == nil) - } + let sut = makeSUT(selectedProvider: makeAuthProvider(providerType)) + let result = try await sut.loadReauthCredential(linkedProviders: linked) + + #expect(result == nil) } - @Test("Throws when no linked providers exist") - func throwsWhenNoLinkedProviders() async { + @Test + func `Throws when no linked providers exist`() async { let sut = makeSUT() await #expect(throws: CredentialError.emptyAuthProviders) { _ = try await sut.loadReauthCredential(linkedProviders: []) } } - @Test("Loads Apple credential on reauth") - func loadsAppleOnReauth() async throws { + @Test + func `Loads Apple credential on reauth`() async throws { let linked = makeLinkedProviders() let selected = makeAuthProvider(.apple) - let sut = makeSUT(selectedProvider: selected, appleCredential: makeAppleCredential()) + let credential = makeAppleCredential() + let sut = makeSUT(selectedProvider: selected, appleCredential: credential) let result = try #require(try await sut.loadReauthCredential(linkedProviders: linked)) - #expect(result.id == CredentialType.apple(makeAppleCredential()).id) + #expect(result.id == CredentialType.apple(credential).id) } - @Test("Loads Google credential on reauth") - func loadsGoogleOnReauth() async throws { + @Test + func `Loads Google credential on reauth`() async throws { let linked = makeLinkedProviders() let selected = makeAuthProvider(.google) - let sut = makeSUT(selectedProvider: selected, googleCredential: makeGoogleCredential()) + let credential = makeGoogleCredential() + let sut = makeSUT(selectedProvider: selected, googleCredential: credential) let result = try #require(try await sut.loadReauthCredential(linkedProviders: linked)) - #expect(result.id == CredentialType.google(makeGoogleCredential()).id) + #expect(result.id == CredentialType.google(credential).id) } - @Test("Loads email credential on reauth") - func loadsEmailOnReauth() async throws { + @Test + func `Loads email credential on reauth`() async throws { let linked = makeLinkedProviders() let selected = makeAuthProvider(.emailPassword) let info = makeEmailInfo() @@ -105,7 +106,7 @@ struct CredentialManagerTests { } -// MARK: - Helpers +// MARK: - SUT private extension CredentialManagerTests { func makeSUT( info: EmailSignUpInfo? = nil, @@ -115,15 +116,19 @@ private extension CredentialManagerTests { googleCredential: GoogleCredentialInfo? = nil, throwProviderError: Bool = false ) -> CredentialManager { - let alerts = StubAlerts(info: info, password: password, selectedProvider: selectedProvider) - let provider = StubProvider( + let alerts = MockAlertHandler(info: info, password: password, selectedProvider: selectedProvider) + let provider = MockSocialProvider( throwError: throwProviderError, appleCredential: appleCredential, googleCredential: googleCredential ) return CredentialManager(alertHandler: alerts, socialCredentialProvider: provider) } +} + +// MARK: - Helpers +private extension CredentialManagerTests { func makeAppleCredential() -> AppleCredentialInfo { .init(email: "", displayName: "", idTokenString: "", nonce: "") } @@ -150,9 +155,9 @@ private extension CredentialManagerTests { } -// MARK: - Stubs +// MARK: - Mocks private extension CredentialManagerTests { - final class StubAlerts: CredentialAlerts { + final class MockAlertHandler: CredentialAlerts { private let info: EmailSignUpInfo? private let password: String? private let selectedProvider: AuthProvider? @@ -170,7 +175,7 @@ private extension CredentialManagerTests { } } - final class StubProvider: SocialCredentialProvider { + final class MockSocialProvider: SocialCredentialProvider { private let throwError: Bool private let appleCredential: AppleCredentialInfo? private let googleCredential: GoogleCredentialInfo? diff --git a/Tests/NnCredentialKitTests/UnitTests/GoogleSignInServiceTests.swift b/Tests/NnCredentialKitTests/UnitTests/GoogleSignInServiceTests.swift index 377e4dc..eee439a 100644 --- a/Tests/NnCredentialKitTests/UnitTests/GoogleSignInServiceTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/GoogleSignInServiceTests.swift @@ -10,32 +10,32 @@ import Testing @MainActor struct GoogleSignInServiceTests { - @Test("Returns nil when client returns nil") - func returnsNilWhenClientReturnsNil() async throws { - let sut = makeSUT(clientResult: nil).sut + @Test + func `Returns nil when client returns nil`() async throws { + let sut = makeSUT(clientResult: nil) let result = try await sut.signIn() #expect(result == nil) } - @Test("Returns nil when sign-in result missing id token") - func returnsNilWhenSignInResultMissingIdToken() async throws { + @Test + func `Returns nil when sign-in result missing id token`() async throws { let signInResult = makeSignInResult(idTokenString: nil) - let sut = makeSUT(clientResult: signInResult).sut + let sut = makeSUT(clientResult: signInResult) let result = try await sut.signIn() #expect(result == nil) } - @Test("Returns credential info with email and token when sign-in succeeds") - func returnsCredentialInfoWithEmailAndTokenWhenSignInSucceeds() async throws { + @Test + func `Returns credential info with email and token when sign-in succeeds`() async throws { let email = "tester@gmail.com" let idToken = "id-token-123" let accessToken = "access-token-456" let signInResult = makeSignInResult(idTokenString: idToken, accessTokenString: accessToken, email: email) - let sut = makeSUT(clientResult: signInResult).sut + let sut = makeSUT(clientResult: signInResult) let result = try #require(try await sut.signIn()) @@ -44,56 +44,56 @@ struct GoogleSignInServiceTests { #expect(result.accessTokenId == accessToken) } - @Test("Combines given name and family name into display name") - func combinesGivenNameAndFamilyNameIntoDisplayName() async throws { + @Test + func `Combines given name and family name into display name`() async throws { let givenName = "John" let familyName = "Doe" let signInResult = makeSignInResult(givenName: givenName, familyName: familyName) - let sut = makeSUT(clientResult: signInResult).sut + let sut = makeSUT(clientResult: signInResult) let result = try #require(try await sut.signIn()) #expect(result.displayName == "\(givenName) \(familyName)") } - @Test("Handles partial name with only given name") - func handlesPartialNameWithOnlyGivenName() async throws { + @Test + func `Handles partial name with only given name`() async throws { let givenName = "John" let signInResult = makeSignInResult(givenName: givenName, familyName: nil) - let sut = makeSUT(clientResult: signInResult).sut + let sut = makeSUT(clientResult: signInResult) let result = try #require(try await sut.signIn()) #expect(result.displayName == givenName) } - @Test("Handles partial name with only family name") - func handlesPartialNameWithOnlyFamilyName() async throws { + @Test + func `Handles partial name with only family name`() async throws { let familyName = "Doe" let signInResult = makeSignInResult(givenName: nil, familyName: familyName) - let sut = makeSUT(clientResult: signInResult).sut + let sut = makeSUT(clientResult: signInResult) let result = try #require(try await sut.signIn()) #expect(result.displayName == familyName) } - @Test("Handles empty display name when both names are nil") - func handlesEmptyDisplayNameWhenBothNamesAreNil() async throws { + @Test + func `Handles empty display name when both names are nil`() async throws { let signInResult = makeSignInResult(givenName: nil, familyName: nil) - let sut = makeSUT(clientResult: signInResult).sut + let sut = makeSUT(clientResult: signInResult) let displayName = try #require(try await sut.signIn()?.displayName) #expect(displayName.isEmpty) } - @Test("Propagates errors from client") - func propagatesErrorsFromClient() async { - enum TestError: Error { case signInFailed } - let sut = makeSUT(clientResult: nil, clientError: TestError.signInFailed).sut + @Test + func `Propagates errors from client`() async { + let error = TestError.network + let sut = makeSUT(clientResult: nil, clientError: error) - await #expect(throws: TestError.signInFailed) { + await #expect(throws: error) { _ = try await sut.signIn() } } @@ -102,13 +102,16 @@ struct GoogleSignInServiceTests { // MARK: - SUT private extension GoogleSignInServiceTests { - func makeSUT(clientResult: GoogleSignInResult?, clientError: (any Error)? = nil) -> (sut: GoogleSignInService, client: MockClient) { + func makeSUT(clientResult: GoogleSignInResult?, clientError: (any Error)? = nil) -> GoogleSignInService { let client = MockClient(result: clientResult, error: clientError) - let sut = GoogleSignInService(client: client) - return (sut, client) + return GoogleSignInService(client: client) } +} + +// MARK: - Helpers +private extension GoogleSignInServiceTests { func makeSignInResult(idTokenString: String? = "id-token", accessTokenString: String = "access-token", email: String? = "test@gmail.com", givenName: String? = "Test", familyName: String? = "User") -> GoogleSignInResult { return .init(idTokenString: idTokenString, accessTokenString: accessTokenString, email: email, givenName: givenName, familyName: familyName) } diff --git a/Tests/NnCredentialKitTests/UnitTests/NonceFactoryTests.swift b/Tests/NnCredentialKitTests/UnitTests/NonceFactoryTests.swift index f52aec8..344f6ee 100644 --- a/Tests/NnCredentialKitTests/UnitTests/NonceFactoryTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/NonceFactoryTests.swift @@ -9,23 +9,23 @@ import Testing @testable import NnCredentialKit struct NonceFactoryTests { - @Test("Generates nonce with default length of 32 characters") - func generatesNonceWithDefaultLength() { + @Test + func `Generates nonce with default length of 32 characters`() { let nonce = NonceFactory.randomNonceString() #expect(nonce.count == 32) } - @Test("Generates nonce with custom specified length") - func generatesNonceWithCustomLength() { + @Test + func `Generates nonce with custom specified length`() { let customLength = 64 let nonce = NonceFactory.randomNonceString(length: customLength) #expect(nonce.count == customLength) } - @Test("Generates nonce using only allowed character set") - func generatesNonceUsingOnlyAllowedCharacterSet() { + @Test + func `Generates nonce using only allowed character set`() { let allowedCharacters = Set("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") let nonce = NonceFactory.randomNonceString(length: 100) let nonceCharacters = Set(nonce) @@ -33,8 +33,8 @@ struct NonceFactoryTests { #expect(nonceCharacters.isSubset(of: allowedCharacters)) } - @Test("Generates different nonces on consecutive calls") - func generatesDifferentNoncesOnConsecutiveCalls() { + @Test + func `Generates different nonces on consecutive calls`() { let nonce1 = NonceFactory.randomNonceString() let nonce2 = NonceFactory.randomNonceString() let nonce3 = NonceFactory.randomNonceString() @@ -43,9 +43,13 @@ struct NonceFactoryTests { #expect(nonce2 != nonce3) #expect(nonce1 != nonce3) } +} + - @Test("Produces consistent SHA256 hash for same input") - func producesConsistentSHA256HashForSameInput() { +// MARK: - SHA256 Hashing +extension NonceFactoryTests { + @Test + func `Produces consistent SHA256 hash for same input`() { let input = "test-input-string" let hash1 = NonceFactory.sha256(input) let hash2 = NonceFactory.sha256(input) @@ -53,8 +57,8 @@ struct NonceFactoryTests { #expect(hash1 == hash2) } - @Test("Produces different SHA256 hashes for different inputs") - func producesDifferentSHA256HashesForDifferentInputs() { + @Test + func `Produces different SHA256 hashes for different inputs`() { let input1 = "first-input" let input2 = "second-input" let hash1 = NonceFactory.sha256(input1) @@ -63,19 +67,19 @@ struct NonceFactoryTests { #expect(hash1 != hash2) } - @Test("Returns SHA256 hash as lowercase hexadecimal string") - func returnsSHA256HashAsLowercaseHexadecimalString() { + @Test + func `Returns SHA256 hash as lowercase hexadecimal string`() { let input = "test" let hash = NonceFactory.sha256(input) let hexCharacters = Set("0123456789abcdef") let hashCharacters = Set(hash) - #expect(hash.count == 64) // SHA256 produces 32 bytes = 64 hex characters + #expect(hash.count == 64) #expect(hashCharacters.isSubset(of: hexCharacters)) } - @Test("Produces expected SHA256 hash for known input") - func producesExpectedSHA256HashForKnownInput() { + @Test + func `Produces expected SHA256 hash for known input`() { let input = "hello" let expectedHash = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" let hash = NonceFactory.sha256(input) diff --git a/Tests/NnCredentialKitTests/UnitTests/ReauthenticationManagerTests.swift b/Tests/NnCredentialKitTests/UnitTests/ReauthenticationManagerTests.swift index 5f761b5..be1cdf1 100644 --- a/Tests/NnCredentialKitTests/UnitTests/ReauthenticationManagerTests.swift +++ b/Tests/NnCredentialKitTests/UnitTests/ReauthenticationManagerTests.swift @@ -10,8 +10,8 @@ import Testing @MainActor struct ReauthenticationManagerTests { - @Test("Throws if no linked providers exist") - func throwsIfNoLinkedProvidersExist() async { + @Test + func `Throws if no linked providers exist`() async { let sut = makeSUT().sut await #expect(throws: CredentialError.emptyAuthProviders) { @@ -19,8 +19,8 @@ struct ReauthenticationManagerTests { } } - @Test("Throws if reauthentication is cancelled") - func throwsIfReauthIsCancelled() async { + @Test + func `Throws if reauthentication is cancelled`() async { let linked = makeLinkedProviders() let sut = makeSUT(linkedProviders: linked).sut @@ -29,23 +29,25 @@ struct ReauthenticationManagerTests { } } - @Test("Uses selected credential for reauthentication") - func usesSelectedCredentialForReauth() async throws { + @Test + func `Uses selected credential for reauthentication`() async throws { let linked = makeLinkedProviders() let credential = makeEmailPasswordCredential() let (sut, delegate) = makeSUT(linkedProviders: linked, credentialType: credential) try await sut.start(actionAfterReauth: { }) - #expect(delegate.credentialType?.id == credential.id) + let credentialType = try #require(delegate.credentialType) + + #expect(credentialType.id == credential.id) } - @Test("Performs action after successful reauth") - func performsActionAfterReauth() async throws { + @Test + func `Performs action after successful reauth`() async throws { var called = false let linked = makeLinkedProviders() let credential = makeEmailPasswordCredential() - let (sut, _) = makeSUT(linkedProviders: linked, credentialType: credential) + let sut = makeSUT(linkedProviders: linked, credentialType: credential).sut try await sut.start { called = true @@ -54,12 +56,12 @@ struct ReauthenticationManagerTests { #expect(called) } - @Test("Skips action if reauth fails") - func skipsActionIfReauthFails() async throws { + @Test + func `Skips action if reauth fails`() async throws { var called = false let linked = makeLinkedProviders() let credential = makeEmailPasswordCredential() - let (sut, _) = makeSUT(linkedProviders: linked, credentialType: credential, throwDelegateError: true) + let sut = makeSUT(linkedProviders: linked, credentialType: credential, throwDelegateError: true).sut do { try await sut.start { @@ -72,7 +74,7 @@ struct ReauthenticationManagerTests { } -// MARK: - Helpers +// MARK: - SUT private extension ReauthenticationManagerTests { func makeSUT( linkedProviders: [AuthProvider] = [], @@ -81,11 +83,15 @@ private extension ReauthenticationManagerTests { throwProviderError: Bool = false ) -> (sut: ReauthenticationManager, delegate: MockDelegate) { let delegate = MockDelegate(throwError: throwDelegateError, linkedProviders: linkedProviders) - let provider = StubProvider(throwError: throwProviderError, credentialType: credentialType) + let provider = MockCredentialProvider(throwError: throwProviderError, credentialType: credentialType) let sut = ReauthenticationManager(delegate: delegate, credentialProvider: provider) return (sut, delegate) } +} + +// MARK: - Helpers +private extension ReauthenticationManagerTests { func makeLinkedProviders(types: [AuthProviderType] = AuthProviderType.allCases) -> [AuthProvider] { types.map { .init(linkedEmail: "linked@\($0.rawValue).com", type: $0) } } @@ -99,7 +105,7 @@ private extension ReauthenticationManagerTests { } -// MARK: - Stubs +// MARK: - Mocks private extension ReauthenticationManagerTests { final class MockDelegate: ReauthenticationDelegate, @unchecked Sendable { private let throwError: Bool @@ -119,7 +125,7 @@ private extension ReauthenticationManagerTests { } } - final class StubProvider: CredentialReauthenticationProvider { + final class MockCredentialProvider: CredentialReauthenticationProvider { private let throwError: Bool private let credentialType: CredentialType?