Persistenter Zähler in Meta + onDecryptFailure-Hook + stats()-Felder; decryptPulledPayload wirft auch bei NoOp-gegen-verschlüsselt (kein enc:1-Ciphertext mehr als 'Plaintext' in der Projektion). Parität zu @mana/event-sync 0.6.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
67 lines
3.2 KiB
Swift
67 lines
3.2 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import ManaEventSync
|
|
|
|
/// Decrypt-Failure-Sichtbarkeit: nicht-entschlüsselbare Pull-Payloads
|
|
/// werfen (statt Ciphertext in die Projektion zu lassen) und der Verlust
|
|
/// ist über die Meta-Keys persistent zählbar.
|
|
final class DecryptFailureTests: XCTestCase {
|
|
private func makeKey(_ seed: UInt8 = 0) -> Data {
|
|
Data((0 ..< 32).map { UInt8($0) &+ seed })
|
|
}
|
|
|
|
func testDecryptPulledPayloadPassesGoodCiphertext() async throws {
|
|
let provider = try MasterKeyCryptoProvider(masterKeyBytes: makeKey(), providerId: "test")
|
|
let payload = JSONValue.object(["v": .int(1)])
|
|
let wire = try await provider.encryptPayload(payload)
|
|
let out = try await decryptPulledPayload(wire, eventId: "evt-1", crypto: provider)
|
|
XCTAssertEqual(out, payload)
|
|
}
|
|
|
|
func testDecryptPulledPayloadPassesPlaintextThrough() async throws {
|
|
// Mixed-Log-Toleranz: Klartext-Payloads (Guest-Phase) bleiben gültig.
|
|
let plain = JSONValue.object(["text": .string("guest")])
|
|
let out = try await decryptPulledPayload(plain, eventId: "evt-2", crypto: NoOpCryptoProvider())
|
|
XCTAssertEqual(out, plain)
|
|
}
|
|
|
|
func testWrongKeyThrows() async throws {
|
|
let providerA = try MasterKeyCryptoProvider(masterKeyBytes: makeKey(0), providerId: "a")
|
|
let providerB = try MasterKeyCryptoProvider(masterKeyBytes: makeKey(7), providerId: "b")
|
|
let wire = try await providerA.encryptPayload(.object(["secret": .bool(true)]))
|
|
do {
|
|
_ = try await decryptPulledPayload(wire, eventId: "evt-3", crypto: providerB)
|
|
XCTFail("erwartete Decrypt-Fehler bei falschem Key")
|
|
} catch {
|
|
// erwartet — CryptoKit-Fehler (Tag-Mismatch)
|
|
}
|
|
}
|
|
|
|
func testNoOpAgainstEncryptedLogThrowsInsteadOfLeakingCiphertext() async throws {
|
|
// Der nutriphi-Footgun-Fall: Client ohne Key bekommt enc:1-Events.
|
|
// Vorher: Ciphertext-String landete als „Plaintext" in der Projektion
|
|
// bzw. das Event verschwand still. Jetzt: expliziter Fehler.
|
|
let provider = try MasterKeyCryptoProvider(masterKeyBytes: makeKey(), providerId: "test")
|
|
let wire = try await provider.encryptPayload(.object(["secret": .bool(true)]))
|
|
do {
|
|
_ = try await decryptPulledPayload(wire, eventId: "evt-4", crypto: NoOpCryptoProvider())
|
|
XCTFail("erwartete decryptFailed")
|
|
} catch let error as EventSyncError {
|
|
XCTAssertEqual(error, .decryptFailed(eventId: "evt-4"))
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func testFailureCounterRoundTripInMeta() throws {
|
|
let storage = try EventStorage(
|
|
storeName: "test-\(UUID().uuidString)",
|
|
appGroupIdentifier: nil,
|
|
inMemory: true
|
|
)
|
|
XCTAssertNil(try storage.metaGet(EventStorageKey.decryptFailureCount))
|
|
try storage.metaSet(EventStorageKey.decryptFailureCount, "3")
|
|
try storage.metaSet(EventStorageKey.decryptFailureLastEvent, "evt-x")
|
|
XCTAssertEqual(try storage.metaGet(EventStorageKey.decryptFailureCount), "3")
|
|
XCTAssertEqual(try storage.metaGet(EventStorageKey.decryptFailureLastEvent), "evt-x")
|
|
}
|
|
}
|