267 lines
12 KiB
Swift
267 lines
12 KiB
Swift
import Testing
|
|
import Foundation
|
|
import PDFKit
|
|
import PencilKit
|
|
import UIKit
|
|
@testable import GasTankAR
|
|
|
|
@MainActor
|
|
struct PDFSigningTests {
|
|
private func handwriting() -> PKDrawing {
|
|
let locations = [CGPoint(x: 20, y: 20), CGPoint(x: 50, y: 65), CGPoint(x: 100, y: 25)]
|
|
let points = locations.enumerated().map { index, location in
|
|
PKStrokePoint(location: location, timeOffset: Double(index) * 0.1,
|
|
size: CGSize(width: 2, height: 2), opacity: 1, force: 1,
|
|
azimuth: 0, altitude: .pi / 2)
|
|
}
|
|
return PKDrawing(strokes: [
|
|
PKStroke(ink: PKInk(.pen, color: .black),
|
|
path: PKStrokePath(controlPoints: points, creationDate: Date()))
|
|
])
|
|
}
|
|
|
|
private func directory() throws -> URL {
|
|
let url = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
return url
|
|
}
|
|
|
|
private func source(in directory: URL) throws -> URL {
|
|
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 595, height: 842))
|
|
let data = renderer.pdfData { context in
|
|
for number in 1...2 {
|
|
context.beginPage()
|
|
("Original page \(number)" as NSString).draw(
|
|
at: CGPoint(x: 40, y: 40), withAttributes: [.font: UIFont.systemFont(ofSize: 16)]
|
|
)
|
|
}
|
|
}
|
|
let url = directory.appending(path: "original.pdf")
|
|
try data.write(to: url)
|
|
return url
|
|
}
|
|
|
|
@Test
|
|
func signatureSurvivesReopeningAndPreservesOriginalPages() async throws {
|
|
let folder = try directory()
|
|
defer { try? FileManager.default.removeItem(at: folder) }
|
|
let original = try source(in: folder)
|
|
let originalData = try Data(contentsOf: original)
|
|
let bounds = CGRect(x: 310, y: 76, width: 237, height: 60)
|
|
let url = try await DefaultPDFSigningService(outputDirectory: folder).sign(
|
|
source: original, drawing: handwriting(),
|
|
placement: PDFSignaturePlacement(pageIndex: 1, bounds: bounds)
|
|
)
|
|
let result = try #require(PDFDocument(url: url))
|
|
#expect(result.pageCount == 2)
|
|
#expect(result.page(at: 0)?.string?.contains("Original page 1") == true)
|
|
#expect(result.page(at: 1)?.string?.contains("Original page 2") == true)
|
|
#expect(result.page(at: 0)?.annotations.isEmpty == true)
|
|
let annotation = try #require(result.page(at: 1)?.annotations.first)
|
|
#expect(annotation.type == "Ink")
|
|
#expect(annotation.bounds == bounds)
|
|
let paths = try #require(annotation.paths)
|
|
#expect(!paths.isEmpty)
|
|
for path in paths {
|
|
#expect(bounds.contains(path.bounds))
|
|
}
|
|
#expect(try Data(contentsOf: original) == originalData)
|
|
#expect(url != original)
|
|
}
|
|
|
|
@Test
|
|
func rejectsEmptyDrawingAndOutOfPagePlacement() async throws {
|
|
let folder = try directory()
|
|
defer { try? FileManager.default.removeItem(at: folder) }
|
|
let original = try source(in: folder)
|
|
let service = DefaultPDFSigningService(outputDirectory: folder)
|
|
await #expect(throws: PDFSigningError.self) {
|
|
try await service.sign(source: original, drawing: PKDrawing(),
|
|
placement: PDFSignaturePlacement(pageIndex: 0, bounds: .zero))
|
|
}
|
|
await #expect(throws: PDFSigningError.self) {
|
|
try await service.sign(source: original, drawing: handwriting(),
|
|
placement: PDFSignaturePlacement(pageIndex: 8,
|
|
bounds: CGRect(x: 0, y: 0, width: 200, height: 50)))
|
|
}
|
|
await #expect(throws: PDFSigningError.self) {
|
|
try await service.sign(source: original, drawing: handwriting(),
|
|
placement: PDFSignaturePlacement(pageIndex: 0,
|
|
bounds: CGRect(x: 590, y: 0, width: 200, height: 50)))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func failedSigningKeepsHandwritingAndCurrentDocument() async throws {
|
|
let folder = try directory()
|
|
defer { try? FileManager.default.removeItem(at: folder) }
|
|
let repository = DefaultLeadDetailRepository(remoteService: StubLeadDetailRemoteService(delay: .zero))
|
|
let detail = try await repository.detail(id: "L-240318", salesCode: "MKR")
|
|
let offer = try #require(detail.offers.first)
|
|
let documents = offer.documents.filter { $0.status == .pending }
|
|
let service = FailingSigningService()
|
|
let model = LeadSigningViewModel(
|
|
request: LeadSigningRequest(offerID: offer.id, signerName: "Klaus Hoffmann", documents: documents),
|
|
leadID: detail.lead.id, salesCode: "MKR", repository: repository,
|
|
signingService: service, store: LeadSignedDocumentStore(directory: folder)
|
|
)
|
|
await model.loadCurrentDocument()
|
|
model.drawing = handwriting()
|
|
let input = model.drawing
|
|
var callbackCount = 0
|
|
await model.signAndAdvance { _, _ in callbackCount += 1 }
|
|
#expect(service.calls == 1)
|
|
#expect(callbackCount == 0)
|
|
#expect(model.currentIndex == 0)
|
|
#expect(model.drawing == input)
|
|
#expect(model.sourceURL != nil)
|
|
#expect(model.message != nil)
|
|
#expect(model.canSign)
|
|
}
|
|
|
|
@Test
|
|
func listsAllDocumentsButSignsAndUploadsOnlyPendingDocuments() async throws {
|
|
let folder = try directory()
|
|
defer { try? FileManager.default.removeItem(at: folder) }
|
|
let repository = DefaultLeadDetailRepository(remoteService: StubLeadDetailRemoteService(delay: .zero))
|
|
let detail = try await repository.detail(id: "L-240318", salesCode: "MKR")
|
|
var offer = try #require(detail.offers.first)
|
|
var documents = Array(offer.documents.prefix(5))
|
|
for index in [0, 2, 4] { documents[index].status = .signed }
|
|
offer.groups = [LeadDocumentGroup(id: "mixed", title: "Dokumente", documents: documents)]
|
|
let detailModel = LeadDetailViewModel(leadID: detail.lead.id, salesCode: "MKR", repository: repository)
|
|
detailModel.startSigning(offer: offer)
|
|
let request = try #require(detailModel.signingRequest)
|
|
#expect(request.documents == documents)
|
|
detailModel.startSigning(offer: offer, group: offer.groups.first)
|
|
#expect(detailModel.signingRequest?.documents == documents)
|
|
|
|
let upload = RecordingPDFUploadService()
|
|
upload.shouldFail = false
|
|
let model = LeadSigningViewModel(
|
|
request: request, leadID: detail.lead.id, salesCode: "MKR", repository: repository,
|
|
signingService: DefaultPDFSigningService(outputDirectory: folder),
|
|
store: LeadSignedDocumentStore(directory: folder), uploadService: upload
|
|
)
|
|
#expect(model.pendingDocuments.count == 2)
|
|
#expect(model.currentDocument?.id == documents[1].id)
|
|
#expect(model.currentDocumentNumber == 2)
|
|
#expect(!model.isLastPendingDocument)
|
|
await model.loadCurrentDocument()
|
|
model.drawing = handwriting()
|
|
var signedIDs: [String] = []
|
|
await model.signAndAdvance { document, _ in signedIDs.append(document.id) }
|
|
#expect(model.currentDocument?.id == documents[3].id)
|
|
#expect(model.currentDocumentNumber == 4)
|
|
#expect(model.isLastPendingDocument)
|
|
#expect(!model.isComplete)
|
|
model.drawing = handwriting()
|
|
await model.signAndAdvance { document, _ in signedIDs.append(document.id) }
|
|
#expect(model.isComplete)
|
|
#expect(model.canFinish)
|
|
#expect(signedIDs == [documents[1].id, documents[3].id])
|
|
#expect(upload.calls.count == 1)
|
|
#expect(upload.calls.first?.count == 2)
|
|
#expect(model.request.documents.count == 5)
|
|
}
|
|
|
|
@Test
|
|
func uploadFailureCanBeRetriedWithoutSigningAgain() async throws {
|
|
let folder = try directory()
|
|
defer { try? FileManager.default.removeItem(at: folder) }
|
|
let repository = DefaultLeadDetailRepository(remoteService: StubLeadDetailRemoteService(delay: .zero))
|
|
let detail = try await repository.detail(id: "L-240318", salesCode: "MKR")
|
|
let offer = try #require(detail.offers.first)
|
|
let document = try #require(offer.documents.first)
|
|
let upload = RecordingPDFUploadService()
|
|
let model = LeadSigningViewModel(
|
|
request: LeadSigningRequest(offerID: offer.id, signerName: "Klaus Hoffmann", documents: [document]),
|
|
leadID: detail.lead.id, salesCode: "MKR", repository: repository,
|
|
signingService: DefaultPDFSigningService(outputDirectory: folder),
|
|
store: LeadSignedDocumentStore(directory: folder), uploadService: upload
|
|
)
|
|
upload.onUpload = { [weak model] in
|
|
#expect(model?.isUploading == true)
|
|
#expect(model?.canFinish == false)
|
|
}
|
|
await model.loadCurrentDocument()
|
|
model.drawing = handwriting()
|
|
var signedCount = 0
|
|
await model.signAndAdvance { _, _ in signedCount += 1 }
|
|
#expect(model.isComplete)
|
|
#expect(model.uploadError != nil)
|
|
#expect(!model.canFinish)
|
|
let signedURLs = model.signedURLs
|
|
upload.shouldFail = false
|
|
await model.uploadSignedDocuments()
|
|
#expect(model.canFinish)
|
|
#expect(model.uploadError == nil)
|
|
#expect(model.signedURLs == signedURLs)
|
|
#expect(signedCount == 1)
|
|
#expect(upload.calls.count == 2)
|
|
#expect(upload.calls.first == upload.calls.last)
|
|
await model.uploadSignedDocuments()
|
|
#expect(upload.calls.count == 2)
|
|
}
|
|
|
|
@Test
|
|
func successfulSigningAdvancesAndRestoresOnlyMatchingDocumentVersion() async throws {
|
|
let folder = try directory()
|
|
defer { try? FileManager.default.removeItem(at: folder) }
|
|
let repository = DefaultLeadDetailRepository(remoteService: StubLeadDetailRemoteService(delay: .zero))
|
|
let detail = try await repository.detail(id: "L-240318", salesCode: "MKR")
|
|
let offer = try #require(detail.offers.first)
|
|
let documents = Array(offer.documents.filter { $0.status == .pending }.prefix(2))
|
|
let store = LeadSignedDocumentStore(directory: folder)
|
|
let model = LeadSigningViewModel(
|
|
request: LeadSigningRequest(offerID: offer.id, signerName: "Klaus Hoffmann", documents: documents),
|
|
leadID: detail.lead.id, salesCode: "MKR", repository: repository,
|
|
signingService: DefaultPDFSigningService(outputDirectory: folder), store: store,
|
|
uploadService: StubLeadSignedPDFUploadService(delay: .zero)
|
|
)
|
|
await model.loadCurrentDocument()
|
|
#expect(!model.canSign)
|
|
model.drawing = handwriting()
|
|
await model.signAndAdvance { _, _ in }
|
|
#expect(model.currentIndex == 1)
|
|
#expect(model.drawing.strokes.isEmpty)
|
|
#expect(!model.isComplete)
|
|
model.drawing = handwriting()
|
|
await model.signAndAdvance { _, _ in }
|
|
#expect(model.isComplete)
|
|
#expect(!model.canSign)
|
|
#expect(store.savedURLs(documents: documents, leadID: detail.lead.id, salesCode: "MKR").count == 2)
|
|
#expect(store.savedURLs(documents: documents, leadID: detail.lead.id, salesCode: "OTHER").isEmpty)
|
|
let first = try #require(documents.first)
|
|
let updated = LeadDocument(id: first.id, title: first.title, pageCount: first.pageCount,
|
|
status: .pending,
|
|
file: LeadFileReference(id: first.file.id, version: "2", kind: .pdf))
|
|
#expect(store.savedURLs(documents: [updated], leadID: detail.lead.id, salesCode: "MKR").isEmpty)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class RecordingPDFUploadService: LeadSignedPDFUploadService {
|
|
let isSimulation = false
|
|
var calls: [[URL]] = []
|
|
var shouldFail = true
|
|
var onUpload: (() -> Void)?
|
|
|
|
func upload(_ files: [URL], leadID: String, offerID: String, salesCode: String) async throws -> LeadPDFUploadResult {
|
|
calls.append(files)
|
|
onUpload?()
|
|
if shouldFail { throw URLError(.notConnectedToInternet) }
|
|
return .uploaded
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class FailingSigningService: PDFSigningService {
|
|
var calls = 0
|
|
|
|
func sign(source: URL, drawing: PKDrawing, placement: PDFSignaturePlacement) async throws -> URL {
|
|
calls += 1
|
|
throw PDFSigningError.saveFailed
|
|
}
|
|
}
|