138 lines
4.2 KiB
Swift
138 lines
4.2 KiB
Swift
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<LoginSession, Never>?
|
|
private(set) var callCount = 0
|
|
|
|
func execute(salesCode: String) async throws -> LoginSession {
|
|
callCount += 1
|
|
return await withCheckedContinuation { continuation = $0 }
|
|
}
|
|
}
|