Add lead workflows, PDF signing, AR projects and photo gallery integration
This commit is contained in:
137
GasTankARTests/LoginTests.swift
Normal file
137
GasTankARTests/LoginTests.swift
Normal file
@ -0,0 +1,137 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user