Files
Gas-Tank-AR-App/GasTankARTests/ARProjectsTests.swift

188 lines
10 KiB
Swift

import Foundation
import SwiftData
import Testing
@testable import GasTankAR
@MainActor
struct ARProjectsTests {
private func detail(_ id: String = "L-240318") throws -> LeadDetail {
let lead = try #require(LeadSampleData.records.first { $0.id == id }).toDomain()
return LeadDetailSampleData.make(for: lead)
}
private func container() throws -> ModelContainer {
try ModelContainer(for: Project.self, LeadARProjectLink.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true))
}
@Test func suppliedProjectsAreIdempotentAndExcludeServiceOffers() throws {
let container = try container()
let detail = try detail()
let service = LeadARProjectsService(detail: detail, container: container)
let first = try service.loadProjects()
let second = try LeadARProjectsService(detail: detail, container: container).loadProjects()
#expect(first.count == 2)
#expect(Set(first.map(\.id)) == Set(second.map(\.id)))
#expect(first.allSatisfy { $0.source == .supplied })
#expect(try ModelContext(container).fetchCount(FetchDescriptor<Project>()) == 2)
#expect(try ModelContext(container).fetchCount(FetchDescriptor<LeadARProjectLink>()) == 2)
}
@Test func customProjectSurvivesReloadAndIsScopedToItsLead() throws {
let container = try container()
let detail = try detail()
let service = LeadARProjectsService(detail: detail, container: container)
let project = try service.createProject(name: " Hinter dem Haus ", tank: Tank())
let reopened = LeadARProjectsService(detail: detail, container: container)
#expect(try reopened.loadProjects().contains { $0.id == project.id && $0.name == "Hinter dem Haus" })
#expect(try reopened.resolveProject(id: project.id).tank == Tank())
let otherDetail = try self.detail("L-240330")
let other = LeadARProjectsService(detail: otherDetail, container: container)
#expect(try !other.loadProjects().contains { $0.id == project.id })
#expect(throws: LeadARProjectsService.ProjectError.self) { try other.resolveProject(id: project.id) }
}
@Test func legacySessionIsLinkedWithoutLosingARData() throws {
let container = try container()
let detail = try detail()
let context = ModelContext(container)
let legacy = Project(leadID: detail.lead.id, customerName: detail.lead.displayName,
creationDate: .now, tank: Tank(), worldMapData: Data([1, 2, 3]),
tankTransform: [1, 2, 3], measurementsData: Data([4, 5]))
context.insert(legacy)
try context.save()
let service = LeadARProjectsService(detail: detail, container: container)
let projects = try service.loadProjects()
#expect(projects.contains { $0.id == legacy.projectID && $0.source == .supplied })
let restored = try service.resolveProject(id: legacy.projectID)
#expect(restored.worldMapData == Data([1, 2, 3]))
#expect(restored.measurementsData == Data([4, 5]))
#expect(restored.tankTransform == [1, 2, 3])
#expect(try context.fetchCount(FetchDescriptor<Project>()) == 2)
}
@Test func invalidCreationDoesNotLeaveProjectsOrLinks() throws {
let container = try container()
let service = LeadARProjectsService(detail: try detail(), container: container)
#expect(throws: LeadARProjectsService.ProjectError.self) {
try service.createProject(name: " ", tank: Tank())
}
#expect(throws: LeadARProjectsService.ProjectError.self) {
try service.createProject(name: "Invalid", tank: Tank(color: .white, placement: .belowGround))
}
#expect(try ModelContext(container).fetchCount(FetchDescriptor<Project>()) == 0)
#expect(try ModelContext(container).fetchCount(FetchDescriptor<LeadARProjectLink>()) == 0)
}
@Test func deletingCustomProjectRemovesAssociationAndSurvivesReload() throws {
let container = try container()
let detail = try detail()
let service = LeadARProjectsService(detail: detail, container: container)
let project = try service.createProject(name: "Löschen", tank: Tank())
try service.deleteProject(id: project.id)
let reloaded = LeadARProjectsService(detail: detail, container: container)
#expect(try !reloaded.loadProjects().contains { $0.id == project.id })
#expect(try ModelContext(container).fetchCount(FetchDescriptor<Project>()) == 2)
#expect(try ModelContext(container).fetchCount(FetchDescriptor<LeadARProjectLink>()) == 2)
}
@Test func suppliedProjectsCannotBeDeleted() throws {
let container = try container()
let service = LeadARProjectsService(detail: try detail(), container: container)
let project = try #require(service.loadProjects().first)
#expect(throws: LeadARProjectsService.ProjectError.self) {
try service.deleteProject(id: project.id)
}
#expect(try service.loadProjects().contains { $0.id == project.id })
}
@Test func editingUpdatesExistingProjectAndPreservesSession() throws {
let container = try container()
let detail = try detail()
let service = LeadARProjectsService(detail: detail, container: container)
let original = try service.createProject(name: "Alt", tank: Tank())
let stored = try service.resolveProject(id: original.id)
stored.worldMapData = Data([1, 2, 3])
stored.tankTransform = [1, 2, 3]
try stored.modelContext?.save()
let tank = Tank(size: .t2_1, color: .olive, placement: .belowGround)
let updated = try service.updateProject(id: original.id, name: " Neu ", tank: tank)
#expect(updated.id == original.id)
#expect(updated.createdAt == original.createdAt)
let reopened = LeadARProjectsService(detail: detail, container: container)
let project = try reopened.resolveProject(id: original.id)
#expect(project.projectName == "Neu")
#expect(project.tank == tank)
#expect(project.worldMapData == Data([1, 2, 3]))
#expect(project.tankTransform == [1, 2, 3])
#expect(try ModelContext(container).fetchCount(FetchDescriptor<Project>()) == 1)
}
@Test func editingRejectsSuppliedForeignAndInvalidProjects() throws {
let container = try container()
let service = LeadARProjectsService(detail: try detail(), container: container)
let supplied = try #require(service.loadProjects().first)
#expect(throws: LeadARProjectsService.ProjectError.self) {
try service.updateProject(id: supplied.id, name: "Changed", tank: Tank())
}
let own = try service.createProject(name: "Original", tank: Tank())
#expect(throws: LeadARProjectsService.ProjectError.self) {
try service.updateProject(id: own.id, name: " ", tank: Tank())
}
#expect(throws: LeadARProjectsService.ProjectError.self) {
try service.updateProject(id: own.id, name: "Invalid", tank: Tank(color: .white, placement: .belowGround))
}
let other = LeadARProjectsService(detail: try detail("L-240330"), container: container)
#expect(throws: LeadARProjectsService.ProjectError.self) {
try other.updateProject(id: own.id, name: "Foreign", tank: Tank())
}
#expect(try service.resolveProject(id: own.id).projectName == "Original")
}
@Test func galleryLoadsNewARPhotosWithoutDuplicatesAndExcludesOtherLeads() throws {
let container = try container()
let detail = try detail()
let service = LeadARProjectsService(detail: detail, container: container)
let own = try service.createProject(name: "Garten", tank: Tank())
let ownProject = try service.resolveProject(id: own.id)
let context = try #require(ownProject.modelContext)
let adapter = ProjectPersistenceAdapter(project: ownProject, modelContext: context)
adapter.savePhoto(Data([1]), date: Date(timeIntervalSince1970: 1), coordinate: nil)
let supplied = try #require(service.loadProjects().first { $0.source == .supplied })
let suppliedProject = try service.resolveProject(id: supplied.id)
ProjectPersistenceAdapter(project: suppliedProject, modelContext: context)
.savePhoto(Data([2]), date: Date(timeIntervalSince1970: 2), coordinate: nil)
let other = LeadARProjectsService(detail: try self.detail("L-240330"), container: container)
let otherValue = try other.createProject(name: "Anderer Kunde", tank: Tank())
let otherProject = try other.resolveProject(id: otherValue.id)
ProjectPersistenceAdapter(project: otherProject, modelContext: try #require(otherProject.modelContext))
.savePhoto(Data([9]), date: .now, coordinate: nil)
let initial = try LeadARPhotoStore(container: container).photos(for: detail.lead.id)
#expect(initial.map(\.imageData) == [Data([2]), Data([1])])
adapter.savePhoto(Data([3]), date: Date(timeIntervalSince1970: 3), coordinate: nil)
let refreshed = try LeadARPhotoStore(container: container).photos(for: detail.lead.id)
#expect(refreshed.map(\.imageData) == [Data([3]), Data([2]), Data([1])])
try service.deleteProject(id: own.id)
#expect(try LeadARPhotoStore(container: container).photos(for: detail.lead.id).count == 1)
}
@Test func associationPersistsWhenDiskStoreReopens() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let url = directory.appendingPathComponent("projects.store")
let detail = try detail()
let projectID: UUID
do {
let store = try ModelContainer(for: Project.self, LeadARProjectLink.self,
configurations: ModelConfiguration(url: url))
projectID = try LeadARProjectsService(detail: detail, container: store)
.createProject(name: "Persistiert", tank: Tank()).id
}
let reopened = try ModelContainer(for: Project.self, LeadARProjectLink.self,
configurations: ModelConfiguration(url: url))
let projects = try LeadARProjectsService(detail: detail, container: reopened).loadProjects()
#expect(projects.contains { $0.id == projectID && $0.name == "Persistiert" })
}
}