Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"enabledPlugins": {
"NnCredentialKit@nelix-swift-tools": true,
"NnTesting@nelix-swift-tools": true
}
}
21 changes: 21 additions & 0 deletions Tests/NnCredentialKitTests/Shared/CredentialType+TestHelpers.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
22 changes: 11 additions & 11 deletions Tests/NnCredentialKitTests/UnitTests/AccountDeleterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -44,17 +44,17 @@ 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)
}
}


// 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) {
Expand Down
142 changes: 85 additions & 57 deletions Tests/NnCredentialKitTests/UnitTests/AccountLinkViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,95 +5,132 @@ 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)
#expect(delegate.providerType == nil)
#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)
Expand All @@ -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))
}
Expand All @@ -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)
}
Expand All @@ -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?

Expand Down Expand Up @@ -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"
}
}
}
Loading
Loading