Files
bitchat/bitchatTests/Services/BLEPrivateMediaReceiptStoreTests.swift
T
jackandClaude Opus 4.8 52709addc1 Quarantine corrupt private-media receipts per record
A single unreadable or undecodable receipt record used to abort the
whole index install, leaving every lookup .unavailable and silently
dropping ALL inbound private media with no recovery path. Move the
damaged file aside to a .corrupt name (skipped by future scans, bytes
preserved), fail only that ID closed, and let the rest of the ledger
load. Directory-level create/enumerate failures stay globally
fail-closed and retryable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 13:15:32 +02:00

292 lines
12 KiB
Swift

import Foundation
import Testing
@testable import bitchat
struct BLEPrivateMediaReceiptStoreTests {
private struct TestError: Error {}
private let messageID = "media-00112233445566778899aabbccddeeff"
@Test
func acceptedReceiptPersistsAcrossStoreInstances() throws {
let root = makeRoot("persist")
defer { try? FileManager.default.removeItem(at: root) }
let payload = try makePayload(in: root)
let first = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(first.commitAccepted(messageID: messageID, storedURL: payload))
#expect(first.state(for: messageID) == .accepted(payload))
let relaunched = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(relaunched.state(for: messageID) == .accepted(payload))
}
@Test
func directoryEnumerationFailureIsUnavailableAndRetriesWithoutCachingEmpty() throws {
let root = makeRoot("list-failure")
defer { try? FileManager.default.removeItem(at: root) }
let payload = try makePayload(in: root)
#expect(BLEPrivateMediaReceiptStore(baseDirectory: root).commitAccepted(
messageID: messageID,
storedURL: payload
))
let record = receiptRecord(in: root)
#expect(FileManager.default.fileExists(atPath: record.path))
var shouldFail = true
let store = BLEPrivateMediaReceiptStore(
baseDirectory: root,
directoryReader: { directory in
if shouldFail {
shouldFail = false
throw TestError()
}
return try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil
)
}
)
#expect(store.state(for: messageID) == .unavailable)
#expect(FileManager.default.fileExists(atPath: record.path))
#expect(store.state(for: messageID) == .accepted(payload))
}
@Test
func recordReadFailureQuarantinesOnlyThatRecord() throws {
let root = makeRoot("read-failure")
defer { try? FileManager.default.removeItem(at: root) }
let payload = try makePayload(in: root)
#expect(BLEPrivateMediaReceiptStore(baseDirectory: root).commitAccepted(
messageID: messageID,
storedURL: payload
))
let record = receiptRecord(in: root)
let durableBytes = try Data(contentsOf: record)
let store = BLEPrivateMediaReceiptStore(
baseDirectory: root,
dataReader: { url in
if url.lastPathComponent.hasPrefix(self.messageID) {
throw TestError()
}
return try Data(contentsOf: url)
}
)
#expect(store.state(for: messageID) == .unavailable)
// The record was moved aside, bytes intact, not deleted.
#expect(!FileManager.default.fileExists(atPath: record.path))
let quarantined = quarantinedRecord(in: root)
#expect(FileManager.default.fileExists(atPath: quarantined.path))
#expect(try Data(contentsOf: quarantined) == durableBytes)
// The quarantine is sticky for this ID; no absent/accepted flapping.
#expect(store.state(for: messageID) == .unavailable)
}
@Test
func decodeFailureQuarantinesRecordWithoutRetryOrDeletion() throws {
let root = makeRoot("decode-failure")
defer { try? FileManager.default.removeItem(at: root) }
let payload = try makePayload(in: root)
#expect(BLEPrivateMediaReceiptStore(baseDirectory: root).commitAccepted(
messageID: messageID,
storedURL: payload
))
let record = receiptRecord(in: root)
let corruptBytes = Data("{not-json".utf8)
try corruptBytes.write(to: record, options: .atomic)
let store = BLEPrivateMediaReceiptStore(baseDirectory: root)
// Only this ID fails closed; it cannot be re-recorded past the
// quarantine either.
#expect(store.state(for: messageID) == .unavailable)
#expect(!store.commitAccepted(messageID: messageID, storedURL: payload))
#expect(!store.recordDeleted(messageID: messageID))
#expect(store.state(for: messageID) == .unavailable)
// The unreadable bytes were preserved at the quarantine name.
let quarantined = quarantinedRecord(in: root)
#expect(!FileManager.default.fileExists(atPath: record.path))
#expect(FileManager.default.fileExists(atPath: quarantined.path))
#expect(try Data(contentsOf: quarantined) == corruptBytes)
// A relaunch stays fail-closed for this ID without ever re-reading
// the quarantined file.
let readURLs = ReadTracker()
let relaunched = BLEPrivateMediaReceiptStore(
baseDirectory: root,
dataReader: { url in
readURLs.append(url)
return try Data(contentsOf: url)
}
)
#expect(relaunched.state(for: messageID) == .unavailable)
#expect(!readURLs.urls.contains {
$0.lastPathComponent == quarantined.lastPathComponent
})
}
@Test
func corruptRecordDoesNotBlockAnotherSendersMedia() throws {
let root = makeRoot("quarantine-isolation")
defer { try? FileManager.default.removeItem(at: root) }
let otherMessageID = "media-ffeeddccbbaa99887766554433221100"
let corruptPayload = try makePayload(in: root, name: "corrupt.jpg")
let healthyPayload = try makePayload(in: root, name: "healthy.jpg")
let seed = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(seed.commitAccepted(
messageID: messageID,
storedURL: corruptPayload
))
#expect(seed.commitAccepted(
messageID: otherMessageID,
storedURL: healthyPayload
))
try Data("{not-json".utf8).write(
to: receiptRecord(in: root),
options: .atomic
)
let store = BLEPrivateMediaReceiptStore(baseDirectory: root)
// Only the damaged record fails closed; the other sender's ledger
// entry keeps serving and new decisions still commit durably.
#expect(store.state(for: messageID) == .unavailable)
#expect(store.state(for: otherMessageID) == .accepted(healthyPayload))
let freshMessageID = "media-0102030405060708090a0b0c0d0e0f10"
let freshPayload = try makePayload(in: root, name: "fresh.jpg")
#expect(store.commitAccepted(
messageID: freshMessageID,
storedURL: freshPayload
))
#expect(store.state(for: freshMessageID) == .accepted(freshPayload))
}
@Test
func unreadableTombstoneNeverBecomesAbsentOrGetsDeleted() throws {
let root = makeRoot("tombstone-decode")
defer { try? FileManager.default.removeItem(at: root) }
let payload = try makePayload(in: root)
let seed = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(seed.commitAccepted(messageID: messageID, storedURL: payload))
#expect(seed.recordDeleted(messageID: messageID))
#expect(!FileManager.default.fileExists(atPath: payload.path))
let record = receiptRecord(in: root)
let corruptBytes = Data([0xFF, 0x00, 0x7B])
try corruptBytes.write(to: record, options: .atomic)
let relaunched = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(relaunched.state(for: messageID) == .unavailable)
// The quarantined tombstone can never flip to absent — a sender retry
// must not resurrect explicitly deleted media even when the payload
// bytes arrive again.
try Data([0xFF, 0xD8, 0xFF, 0xD9]).write(to: payload)
#expect(!relaunched.commitAccepted(
messageID: messageID,
storedURL: payload
))
let quarantined = quarantinedRecord(in: root)
#expect(FileManager.default.fileExists(atPath: quarantined.path))
#expect(try Data(contentsOf: quarantined) == corruptBytes)
#expect(
BLEPrivateMediaReceiptStore(baseDirectory: root)
.state(for: messageID) == .unavailable
)
}
@Test
func failedTombstonePersistenceDoesNotPoisonVolatileState() throws {
let root = makeRoot("failed-tombstone-write")
defer { try? FileManager.default.removeItem(at: root) }
let store = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(store.state(for: messageID) == .absent)
// Force the atomic record write itself to fail after the store has
// successfully loaded its empty index.
let record = receiptRecord(in: root)
try FileManager.default.createDirectory(
at: record,
withIntermediateDirectories: true
)
#expect(!store.recordDeleted(messageID: messageID))
try FileManager.default.removeItem(at: record)
// The UI must be able to report the deletion failure without a
// process-lifetime tombstone silently hiding a later retry.
#expect(store.state(for: messageID) == .absent)
}
@Test
func unreleasedAggregateLedgerIsIgnoredAndLeftUntouched() throws {
let root = makeRoot("no-legacy-migration")
defer { try? FileManager.default.removeItem(at: root) }
let files = root.appendingPathComponent("files", isDirectory: true)
try FileManager.default.createDirectory(
at: files,
withIntermediateDirectories: true
)
let legacy = files.appendingPathComponent(
".private-media-receipts.json",
isDirectory: false
)
let bytes = Data(
#"{"entries":{"media-00112233445566778899aabbccddeeff":{"relativePath":"images/incoming/old.jpg","acceptedAt":0}}}"#
.utf8
)
try bytes.write(to: legacy, options: .atomic)
let store = BLEPrivateMediaReceiptStore(baseDirectory: root)
#expect(store.state(for: messageID) == .absent)
#expect(try Data(contentsOf: legacy) == bytes)
}
private func makeRoot(_ label: String) -> URL {
FileManager.default.temporaryDirectory.appendingPathComponent(
"private-media-receipt-\(label)-\(UUID().uuidString)",
isDirectory: true
)
}
private func makePayload(
in root: URL,
name: String = "image.jpg"
) throws -> URL {
let directory = root.appendingPathComponent(
"files/images/incoming",
isDirectory: true
)
try FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
let payload = directory.appendingPathComponent(name)
try Data([0xFF, 0xD8, 0xFF, 0xD9]).write(to: payload)
return payload
}
private func receiptRecord(in root: URL) -> URL {
root
.appendingPathComponent(
"files/.private-media-receipts",
isDirectory: true
)
.appendingPathComponent(messageID)
.appendingPathExtension("json")
}
private func quarantinedRecord(in root: URL) -> URL {
receiptRecord(in: root).appendingPathExtension("corrupt")
}
}
/// Collects the URLs a store's data reader touched. A reference type so the
/// `@Sendable`-shaped reader closure can record without mutating captures.
private final class ReadTracker: @unchecked Sendable {
private(set) var urls: [URL] = []
func append(_ url: URL) { urls.append(url) }
}