Files
Termind/FrameworkTests/CloudSyncRecordCodingTests.swift
T
2026-09-17 21:22:21 +08:00

156 lines
7.0 KiB
Swift

import CloudKit
import Foundation
import Testing
@testable import TermindInfrastructure
struct CloudSyncRecordCodingTests {
@Test(arguments: [0, 699_999, 700_000, 700_001])
func payloadBoundaryPreservesBytesAndDefersAssetCreation(size: Int) throws {
let record = makeRecord()
let payload = Data(repeating: 0xAB, count: size)
let url = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: url) }
var requestedAsset = false
func assetURL() -> URL {
requestedAsset = true
return url
}
try CloudSyncRecordCoding.setPayload(payload, on: record, assetURL: assetURL())
#expect(requestedAsset == (size > 700_000))
#expect((record["payloadAsset"] is CKAsset) == (size > 700_000))
#expect((record["payload"] is Data) == (size <= 700_000))
#expect(
try CloudSyncRecordCoding.validatedPayloadData(from: record, invalidEnvelope: CodingError.invalidPayload)
== payload)
}
@Test
func payloadHashUsesTheExistingLowercaseSHA256WireEncoding() throws {
let record = makeRecord()
try CloudSyncRecordCoding.setPayload(
Data("abc".utf8), on: record, assetURL: FileManager.default.temporaryDirectory)
#expect(record["payloadHash"] as? String == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
}
@Test
func changingStorageRepresentationClearsThePreviousField() throws {
let record = makeRecord()
let url = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: url) }
record["payload"] = Data("old inline bytes".utf8) as NSData
try CloudSyncRecordCoding.setPayload(Data(repeating: 1, count: 700_001), on: record, assetURL: url)
#expect(record["payload"] == nil)
#expect(record["payloadAsset"] is CKAsset)
let inline = Data("new inline bytes".utf8)
try CloudSyncRecordCoding.setPayload(inline, on: record, assetURL: url)
#expect(record["payloadAsset"] == nil)
#expect(
try CloudSyncRecordCoding.validatedPayloadData(from: record, invalidEnvelope: CodingError.invalidPayload)
== inline)
}
@Test(arguments: [nil, "incorrect"] as [String?])
func invalidHashRejectsTheEnvelopeButKeepsRawBytesForQuarantine(hash: String?) throws {
let record = makeRecord()
let payload = Data("quarantined bytes".utf8)
record["payload"] = payload as NSData
record["payloadHash"] = hash.map { $0 as NSString }
#expect(throws: CodingError.invalidPayload) {
try CloudSyncRecordCoding.validatedPayloadData(from: record, invalidEnvelope: CodingError.invalidPayload)
}
#expect(
try CloudSyncRecordCoding.payloadData(from: record, invalidEnvelope: CodingError.invalidPayload) == payload)
}
@Test
func missingPayloadUsesTheCallersError() {
let record = makeRecord()
#expect(throws: CodingError.invalidPayload) {
try CloudSyncRecordCoding.payloadData(from: record, invalidEnvelope: CodingError.invalidPayload)
}
#expect(throws: CodingError.invalidPayload) {
try CloudSyncRecordCoding.validatedPayloadData(from: record, invalidEnvelope: CodingError.invalidPayload)
}
}
@Test
func inlinePayloadTakesPrecedenceAndAssetReadErrorsAreNotRemapped() throws {
let record = makeRecord()
let url = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try Data("asset".utf8).write(to: url)
defer { try? FileManager.default.removeItem(at: url) }
record["payloadAsset"] = CKAsset(fileURL: url)
try FileManager.default.removeItem(at: url)
let inline = Data("inline".utf8)
record["payload"] = inline as NSData
#expect(
try CloudSyncRecordCoding.payloadData(from: record, invalidEnvelope: CodingError.invalidPayload) == inline)
record["payload"] = nil
#expect(throws: CocoaError.self) {
try CloudSyncRecordCoding.payloadData(from: record, invalidEnvelope: CodingError.invalidPayload)
}
}
@Test
func systemFieldsRestoreRecordIdentityInsteadOfUsingTheFallback() throws {
let original = makeRecord()
let data = try CloudSyncRecordCoding.systemFields(of: original)
let fallback = CKRecord.ID(recordName: UUID().uuidString)
let restored = try CloudSyncRecordCoding.makeRecord(type: "Fallback", id: fallback, systemFields: data)
#expect(restored.recordID == original.recordID)
#expect(restored.recordType == original.recordType)
let fresh = try CloudSyncRecordCoding.makeRecord(type: "Fallback", id: fallback, systemFields: nil)
#expect(fresh.recordID == fallback)
#expect(fresh.recordType == "Fallback")
#expect(throws: (any Error).self) {
try CloudSyncRecordCoding.makeRecord(type: "Fallback", id: fallback, systemFields: Data([0xFF]))
}
}
@Test
func assetNamespacesDoNotOverwriteEachOtherForTheSameRecordName() throws {
let root = "TermindTests/CloudSyncRecordCoding/\(UUID().uuidString)"
let first = makeRecord()
let second = CKRecord(recordType: first.recordType, recordID: first.recordID)
let firstURL = try CloudSyncRecordCoding.assetURL(
recordName: first.recordID.recordName, directoryName: "\(root)/first")
defer {
try? FileManager.default.removeItem(at: firstURL.deletingLastPathComponent().deletingLastPathComponent())
}
let secondURL = try CloudSyncRecordCoding.assetURL(
recordName: second.recordID.recordName, directoryName: "\(root)/second")
#expect(firstURL != secondURL)
#expect(firstURL.lastPathComponent == first.recordID.recordName.replacingOccurrences(of: "/", with: "-"))
let firstPayload = Data(repeating: 1, count: 700_001)
let secondPayload = Data(repeating: 2, count: 700_001)
try CloudSyncRecordCoding.setPayload(firstPayload, on: first, assetURL: firstURL)
try CloudSyncRecordCoding.setPayload(secondPayload, on: second, assetURL: secondURL)
#expect(
try CloudSyncRecordCoding.validatedPayloadData(from: first, invalidEnvelope: CodingError.invalidPayload)
== firstPayload)
#expect(
try CloudSyncRecordCoding.validatedPayloadData(from: second, invalidEnvelope: CodingError.invalidPayload)
== secondPayload)
}
private func makeRecord() -> CKRecord {
CKRecord(
recordType: "CodingTestItem",
recordID: CKRecord.ID(
recordName: "kind/\(UUID().uuidString)",
zoneID: CKRecordZone.ID(zoneName: "CodingTestZone", ownerName: CKCurrentUserDefaultName)))
}
private enum CodingError: Error, Equatable {
case invalidPayload
}
}