Add lead workflows, PDF signing, AR projects and photo gallery integration

This commit is contained in:
Dennis Nemec
2026-11-08 17:49:39 +01:00
parent a343bde741
commit 8d18873412
82 changed files with 6401 additions and 34 deletions

View File

@ -0,0 +1,146 @@
import Foundation
import Testing
import PDFKit
@testable import GasTankAR
@MainActor
struct LeadDetailDownloadTests {
private func fixture() async throws -> (DefaultLeadDetailRepository, StubLeadDetailRemoteService, LeadDetail) {
let service = StubLeadDetailRemoteService(delay: .zero)
let repository = DefaultLeadDetailRepository(
remoteService: service,
cacheDirectory: FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
)
let detail = try await repository.detail(id: "L-240318", salesCode: "MKR")
return (repository, service, detail)
}
@Test
func metadataDoesNotDownloadPDFsAndProgressMatchesDocuments() async throws {
let (_, service, detail) = try await fixture()
#expect(service.downloadCount == 0)
#expect(detail.offers.count == 3)
#expect(detail.offers[0].documents.count == 6)
#expect(detail.offers[0].signedCount == 2)
#expect(detail.offers[1].documents.count == 5)
#expect(detail.offers[2].isComplete)
#expect(detail.photos.count == 10)
}
@Test
func downloadsActualPDFAndReusesCachedFile() async throws {
let (repository, service, detail) = try await fixture()
let document = try #require(detail.offers.first?.documents.first)
let useCase = DownloadLeadDocumentUseCase(repository: repository)
let first = try await useCase.execute(document, leadID: detail.lead.id, salesCode: "MKR")
let pdf = try #require(PDFDocument(url: first))
#expect(pdf.pageCount == document.pageCount)
#expect(pdf.string?.contains("Beispieldokument") == true)
let second = try await useCase.execute(document, leadID: detail.lead.id, salesCode: "MKR")
#expect(first == second)
#expect(service.downloadCount == 1)
try FileManager.default.removeItem(at: first)
let third = try await useCase.execute(document, leadID: detail.lead.id, salesCode: "MKR")
#expect(FileManager.default.fileExists(atPath: third.path))
#expect(service.downloadCount == 2)
}
@Test
func concurrentDownloadsShareOneRequest() async throws {
let (repository, service, detail) = try await fixture()
service.delay = .milliseconds(50)
let file = try #require(detail.offers.first?.documents.first?.file)
let first = Task { try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR") }
let second = Task { try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR") }
let firstURL = try await first.value
let secondURL = try await second.value
#expect(firstURL == secondURL)
#expect(service.downloadCount == 1)
}
@Test
func cacheIsScopedToUserAndDocumentVersion() async throws {
let (repository, service, detail) = try await fixture()
let file = try #require(detail.offers.first?.documents.first?.file)
let first = try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR")
let otherUser = try await repository.file(file, leadID: detail.lead.id, salesCode: "ABC")
#expect(first != otherUser)
let nextVersion = LeadFileReference(id: file.id, version: "2", kind: .pdf)
await #expect(throws: LeadFileError.notFound) {
try await repository.file(nextVersion, leadID: detail.lead.id, salesCode: "MKR")
}
#expect(service.downloadCount == 3)
}
@Test
func invalidPDFIsNotCachedAndRetrySucceeds() async throws {
let (repository, service, detail) = try await fixture()
let file = try #require(detail.offers.first?.documents.first?.file)
service.fileScenario = .invalidFile
await #expect(throws: LeadFileError.invalidFile) {
try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR")
}
service.fileScenario = .success
let url = try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR")
#expect(PDFDocument(url: url) != nil)
#expect(service.downloadCount == 2)
}
@Test
func fileErrorsPreserveLoadedDetailAndMapCorrectly() async throws {
let (repository, service, detail) = try await fixture()
let file = try #require(detail.offers.first?.documents.first?.file)
for (scenario, expected): (StubLeadDetailRemoteService.Scenario, LeadError) in [
(.offline, .offline), (.timeout, .timeout), (.http(500), .serviceUnavailable),
(.http(401), .unauthorized), (.http(403), .forbidden)
] {
service.fileScenario = scenario
await #expect(throws: expected) {
try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR")
}
}
service.fileScenario = .http(404)
await #expect(throws: LeadFileError.notFound) {
try await repository.file(file, leadID: detail.lead.id, salesCode: "MKR")
}
#expect(detail.lead.id == "L-240318")
}
@Test
func treeExpandsCollapsesAndKeepsStateWhenSelectingPDF() async throws {
let (repository, _, _) = try await fixture()
let model = LeadDetailViewModel(leadID: "L-240318", salesCode: "MKR", repository: repository)
await model.load()
let detail = try #require(model.detail)
#expect(model.expandedOffers == [detail.offers[0].id])
model.toggleAll()
#expect(model.isAllExpanded)
model.selectedDocument = detail.offers[0].documents[0]
model.selectedDocument = nil
#expect(model.isAllExpanded)
model.toggleAll()
#expect(model.expandedOffers.isEmpty)
#expect(model.expandedGroups.isEmpty)
model.toggleOffer(detail.offers[1].id)
#expect(model.expandedOffers == [detail.offers[1].id])
}
@Test
func anotherLeadKeepsItsOwnIdentityAndEmptyOffers() async throws {
let (repository, _, _) = try await fixture()
let detail = try await repository.detail(id: "L-240326", salesCode: "MKR")
#expect(detail.lead.companyName == "Schreinerei Vogt")
#expect(detail.offers.isEmpty)
#expect(detail.email == nil)
}
@Test
func photoDownloadProducesAnImage() async throws {
let (repository, service, detail) = try await fixture()
let photo = try #require(detail.photos.first)
let url = try await LoadLeadPhotoUseCase(repository: repository)
.execute(photo, leadID: detail.lead.id, salesCode: "MKR")
#expect(FileManager.default.fileExists(atPath: url.path))
#expect(service.downloadCount == 1)
}
}