import Foundation import Testing @testable import GasTankAR @MainActor struct LoginTests { @Test(arguments: ["", " ", "AB", " AB\n"]) func invalidCodeDoesNotReachService(_ code: String) async { let service = RecordingAuthenticationService() let useCase = DefaultLoginUseCase(authenticationService: service) await #expect(throws: LoginError.invalidSalesCode) { try await useCase.execute(salesCode: code) } #expect(service.receivedCodes.isEmpty) } @Test func normalizesCodeBeforeAuthentication() async throws { let service = RecordingAuthenticationService() let useCase = DefaultLoginUseCase(authenticationService: service) let session = try await useCase.execute(salesCode: " mkr\n") #expect(service.receivedCodes == ["MKR"]) #expect(session.salesCode == "MKR") } @Test func serviceFailurePropagates() async { let useCase = DefaultLoginUseCase( authenticationService: StubAuthenticationService(simulatedError: .rejected, delay: .zero) ) await #expect(throws: LoginError.rejected) { try await useCase.execute(salesCode: "MKR") } } @Test func successfulLoginReturnsSessionAndResetsLoading() async { let model = LoginViewModel(loginUseCase: StubLoginUseCase(error: nil)) model.salesCode = "MKR" let session = await model.signIn() #expect(session == LoginSession(salesCode: "MKR")) #expect(!model.isLoading) #expect(model.errorMessage == nil) } @Test func failureIsDisplayedAndEditingClearsIt() async { let model = LoginViewModel(loginUseCase: StubLoginUseCase(error: .rejected)) model.salesCode = "MKR" let session = await model.signIn() #expect(session == nil) #expect(model.errorMessage != nil) #expect(!model.isLoading) model.salesCode = "ABC" #expect(model.errorMessage == nil) #expect(model.canSubmit) } @Test func concurrentSubmissionIsIgnored() async { let useCase = SuspendedLoginUseCase() let model = LoginViewModel(loginUseCase: useCase) model.salesCode = "MKR" let first = Task { await model.signIn() } // Wait for a deterministic suspension point instead of a timed delay. while useCase.continuation == nil { await Task.yield() } #expect(model.isLoading) #expect(!model.canSubmit) let duplicate = await model.signIn() #expect(duplicate == nil) #expect(useCase.callCount == 1) useCase.continuation?.resume(returning: LoginSession(salesCode: "MKR")) let result = await first.value #expect(result != nil) #expect(!model.isLoading) } @Test func cancelledLoginDoesNotReturnSessionOrShowError() async { let useCase = SuspendedLoginUseCase() let model = LoginViewModel(loginUseCase: useCase) model.salesCode = "MKR" let task = Task { await model.signIn() } while useCase.continuation == nil { await Task.yield() } task.cancel() useCase.continuation?.resume(returning: LoginSession(salesCode: "MKR")) let result = await task.value #expect(result == nil) #expect(model.errorMessage == nil) #expect(!model.isLoading) } } @MainActor private final class RecordingAuthenticationService: AuthenticationService { private(set) var receivedCodes: [String] = [] func signIn(salesCode: String) async throws -> LoginSession { receivedCodes.append(salesCode) return LoginSession(salesCode: salesCode) } } private struct StubLoginUseCase: LoginUseCase { let error: LoginError? func execute(salesCode: String) async throws -> LoginSession { if let error { throw error } return LoginSession(salesCode: salesCode) } } @MainActor private final class SuspendedLoginUseCase: LoginUseCase { var continuation: CheckedContinuation? private(set) var callCount = 0 func execute(salesCode: String) async throws -> LoginSession { callCount += 1 return await withCheckedContinuation { continuation = $0 } } }