import XCTest import CryptoKit @testable import Closer final class AnswerCryptoTests: XCTestCase { private let coupleId = "couple-test-456" private let userId = "user-test-789" private let questionId = "question-test-abc" private var key: CoupleKeyMaterial { CoupleKeyMaterial(rawBytes: Data(repeating: 0xAB, count: 32)) } func testEncryptDecryptRoundTrip() throws { let plaintext = "This is my private answer." let payload = try AnswerCrypto.encrypt( answerPlaintext: plaintext, userId: userId, questionId: questionId, coupleId: coupleId, key: key ) XCTAssertEqual(payload.schemaVersion, AnswerCrypto.schemaVersion) XCTAssertTrue(payload.ciphertext.hasPrefix(FieldEncryptor.prefix)) let recovered = try AnswerCrypto.decrypt(payload, key: key) XCTAssertEqual(recovered, plaintext) } func testEncodeDecodeRoundTrip() throws { let plaintext = "Round-trip via outer wrapper" let payload = try AnswerCrypto.encrypt( answerPlaintext: plaintext, userId: userId, questionId: questionId, coupleId: coupleId, key: key ) let encoded = try AnswerCrypto.encode(payload) XCTAssertTrue(encoded.hasPrefix(FieldEncryptor.prefix)) let decoded = try AnswerCrypto.decode(encoded) XCTAssertEqual(decoded.userId, userId) XCTAssertEqual(decoded.questionId, questionId) XCTAssertEqual(decoded.coupleId, coupleId) XCTAssertEqual(decoded.schemaVersion, AnswerCrypto.schemaVersion) let recovered = try AnswerCrypto.decrypt(decoded, key: key) XCTAssertEqual(recovered, plaintext) } func testAADMismatchRejects() throws { let plaintext = "AAD-bound answer" let payload = try AnswerCrypto.encrypt( answerPlaintext: plaintext, userId: userId, questionId: questionId, coupleId: coupleId, key: key ) // Tamper the questionId in the payload so the AAD would differ. var tampered = payload tampered = SecureAnswerPayload( schemaVersion: payload.schemaVersion, coupleId: payload.coupleId, userId: payload.userId, questionId: payload.questionId + "x", ciphertext: payload.ciphertext, createdAt: payload.createdAt ) XCTAssertThrowsError(try AnswerCrypto.decrypt(tampered, key: key)) } func testTamperedCiphertextRejects() throws { let plaintext = "Tamper me" let payload = try AnswerCrypto.encrypt( answerPlaintext: plaintext, userId: userId, questionId: questionId, coupleId: coupleId, key: key ) var chars = Array(payload.ciphertext) let prefixEnd = FieldEncryptor.prefix.count chars[prefixEnd + 5] ^= 0x01 let tamperedCiphertext = String(chars) let tamperedPayload = SecureAnswerPayload( schemaVersion: payload.schemaVersion, coupleId: payload.coupleId, userId: payload.userId, questionId: payload.questionId, ciphertext: tamperedCiphertext, createdAt: payload.createdAt ) XCTAssertThrowsError(try AnswerCrypto.decrypt(tamperedPayload, key: key)) } func testOuterWrapperTamperRejects() throws { let plaintext = "Outer tamper" let payload = try AnswerCrypto.encrypt( answerPlaintext: plaintext, userId: userId, questionId: questionId, coupleId: coupleId, key: key ) let encoded = try AnswerCrypto.encode(payload) var chars = Array(encoded) let prefixEnd = FieldEncryptor.prefix.count chars[prefixEnd + 3] ^= 0x01 let tampered = String(chars) XCTAssertThrowsError(try AnswerCrypto.decode(tampered)) } }