| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // The reverse-direction gate: message-payload-vector proves the web's bytes |
| 5 | // open natively; THIS fixture proves native-encrypted bytes open on the web |
| 6 | // (test/nativeblob.test.mjs decrypts it with crypto.js). Together they pin the |
| 7 | // native send path — a native message must render for every web member. |
| 8 | // |
| 9 | // Regenerate after changing the outgoing envelope or GroupKey: |
| 10 | // RECORD_VECTORS=1 swift test --filter NativeMessageVectorTests |
| 11 | // then re-run `npm test` (the JS side must still pass) and commit the fixture. |
| 12 | final class NativeMessageVectorTests: XCTestCase { |
| 13 | // A fixed key (bytes 0..31, like the thread-key vector) + a payload shaped |
| 14 | // exactly like ChatFeedModel's Outgoing envelope. |
| 15 | private static let keyRaw = Data((0 ..< 32).map { UInt8($0) }) |
| 16 | private static let expect: [String: Any] = [ |
| 17 | "text": "Hello from native! 🎉", |
| 18 | "name": "Fable", |
| 19 | "ts": 1751830000000 as Int64, |
| 20 | "uid": "u_native", |
| 21 | ] |
| 22 | |
| 23 | private var fixtureURL: URL { |
| 24 | URL(fileURLWithPath: #filePath) |
| 25 | .deletingLastPathComponent() |
| 26 | .appending(path: "Fixtures/native-message-vector.json") |
| 27 | } |
| 28 | |
| 29 | func testRecordFixtureIfRequested() throws { |
| 30 | guard ProcessInfo.processInfo.environment["RECORD_VECTORS"] == "1" else { |
| 31 | throw XCTSkip("set RECORD_VECTORS=1 to regenerate the fixture") |
| 32 | } |
| 33 | struct Payload: Encodable { |
| 34 | let text: String; let name: String; let ts: Int64; let uid: String |
| 35 | } |
| 36 | let key = try GroupKey(rawKey: Self.keyRaw) |
| 37 | let blob = try key.encryptJSON(Payload(text: Self.expect["text"] as! String, |
| 38 | name: Self.expect["name"] as! String, |
| 39 | ts: Self.expect["ts"] as! Int64, |
| 40 | uid: Self.expect["uid"] as! String)) |
| 41 | let fixture: [String: Any] = [ |
| 42 | "_comment": "Native-encrypted message vector. Generated by NativeMessageVectorTests " |
| 43 | + "(RECORD_VECTORS=1 swift test) from LChatCore's GroupKey.encryptJSON — " |
| 44 | + "test/nativeblob.test.mjs must decrypt it with web/static/crypto.js, proving " |
| 45 | + "native sends render for web members. base64url throughout.", |
| 46 | "keyB64": Self.keyRaw.base64urlEncodedString(), |
| 47 | "blob": blob, |
| 48 | "expect": Self.expect, |
| 49 | ] |
| 50 | let json = try JSONSerialization.data(withJSONObject: fixture, options: [.prettyPrinted, .sortedKeys]) |
| 51 | try (String(data: json, encoding: .utf8)! + "\n").write(to: fixtureURL, atomically: true, encoding: .utf8) |
| 52 | print("wrote", fixtureURL.path) |
| 53 | } |
| 54 | |
| 55 | func testCommittedFixtureDecryptsNatively() throws { |
| 56 | // Self-check on the committed fixture (the JS gate is the real point). |
| 57 | let data = try Data(contentsOf: fixtureURL) |
| 58 | let fx = try JSONSerialization.jsonObject(with: data) as! [String: Any] |
| 59 | let key = try GroupKey(rawKey: try XCTUnwrap(Data(base64urlEncoded: fx["keyB64"] as! String))) |
| 60 | let p = try key.decryptJSON(MessagePayload.self, blob: fx["blob"] as! String) |
| 61 | let expect = fx["expect"] as! [String: Any] |
| 62 | XCTAssertEqual(p.text, expect["text"] as? String) |
| 63 | XCTAssertEqual(p.name, expect["name"] as? String) |
| 64 | XCTAssertEqual(p.uid, expect["uid"] as? String) |
| 65 | XCTAssertEqual(p.ts, (expect["ts"] as? NSNumber)?.int64Value) |
| 66 | } |
| 67 | } |
| 68 | |