188 lines
10 KiB
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" })
|
|
}
|
|
}
|