eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2@testable import LChatCore
3
4// πŸ€– Cross-platform event contract tests for the space-apps registry β€” the
5// native fold (SpaceApps.fold, fed by SpaceAppsStore) must match the web's
6// spaceapps reducers exactly. Pinned by test/event-vectors/spaceapps.json;
7// the web side runs the same vectors in test/event-vectors.test.mjs.
8final class SpaceAppsVectorTests: XCTestCase {
9 private func fold(seed: [SpaceApps.Item] = [], upTo: Int64 = 0,
10 _ events: [Int64: SpaceApps.Event]) -> [SpaceApps.Item] {
11 SpaceApps.fold(seed: seed, upTo: upTo, events: events)
12 }
13
14 // spaceapps_install: a member installs an app for everyone in the space.
15 func testInstall() {
16 let out = fold([1: .init(type: "spaceapps.add", app: "bookmarks", by: "Ana",
17 ts: 1_700_000_000_000, uid: "u-ana")])
18 XCTAssertEqual(out, [SpaceApps.Item(id: "bookmarks", by: "Ana",
19 ts: 1_700_000_000_000, uid: "u-ana")])
20 }
21
22 // spaceapps_install_duplicate: idempotent by app name β€” the original
23 // install's attribution (by/ts/uid) is kept, a re-install never rewrites it.
24 func testDuplicateInstallKeepsOriginalAttribution() {
25 let out = fold([
26 1: .init(type: "spaceapps.add", app: "bookmarks", by: "Ana",
27 ts: 1_700_000_000_000, uid: "u-ana"),
28 2: .init(type: "spaceapps.add", app: "bookmarks", by: "Bob",
29 ts: 1_700_000_005_000, uid: "u-bob"),
30 ])
31 XCTAssertEqual(out, [SpaceApps.Item(id: "bookmarks", by: "Ana",
32 ts: 1_700_000_000_000, uid: "u-ana")])
33 }
34
35 // spaceapps_install_unknown_app: an install naming an app this client
36 // doesn't know still folds β€” it stays inert at read time, so clients on
37 // different versions converge on the same log.
38 func testUnknownAppFoldsIn() {
39 let seed = [SpaceApps.Item(id: "bookmarks", by: "Ana",
40 ts: 1_700_000_000_000, uid: "u-ana")]
41 let out = fold(seed: seed, upTo: 1, [
42 2: .init(type: "spaceapps.add", app: "someday-app", by: "Ana",
43 ts: 1_700_000_010_000, uid: "u-ana"),
44 ])
45 XCTAssertEqual(out, seed + [SpaceApps.Item(id: "someday-app", by: "Ana",
46 ts: 1_700_000_010_000, uid: "u-ana")])
47 }
48
49 // spaceapps_remove: any member removes a space-wide install.
50 func testRemove() {
51 let seed = [SpaceApps.Item(id: "bookmarks", by: "Ana",
52 ts: 1_700_000_000_000, uid: "u-ana")]
53 let out = fold(seed: seed, upTo: 1, [
54 2: .init(type: "spaceapps.remove", app: "bookmarks", by: "Bob",
55 ts: 1_700_000_020_000, uid: ""),
56 ])
57 XCTAssertEqual(out, [])
58 }
59
60 // spaceapps_remove_absent: removing an app that isn't installed is a no-op
61 // (a remove can race another member's remove).
62 func testRemoveAbsentIsNoop() {
63 let out = fold([1: .init(type: "spaceapps.remove", app: "bookmarks", by: "Bob",
64 ts: 1_700_000_030_000, uid: "")])
65 XCTAssertEqual(out, [])
66 }
67
68 // spaceapps_add_malformed: a hostile or malformed install (missing app)
69 // folds to a no-op, never a crash. The wire decode enforces the same rule
70 // one layer down: no spaceapps.* prefix / no app / undecryptable β†’ nil.
71 func testMalformedAddIsNoop() {
72 let out = fold([1: .init(type: "spaceapps.add", app: "", by: "Mallory",
73 ts: 1_700_000_040_000, uid: "")])
74 XCTAssertEqual(out, [])
75 }
76
77 // Fold order is server-message-id order, whatever order pages arrived in.
78 func testFoldsInMessageIDOrder() {
79 let out = fold([
80 3: .init(type: "spaceapps.remove", app: "gif", by: "Ana", ts: 3, uid: "u-ana"),
81 1: .init(type: "spaceapps.add", app: "gif", by: "Ana", ts: 1, uid: "u-ana"),
82 2: .init(type: "spaceapps.add", app: "bookmarks", by: "Bob", ts: 2, uid: "u-bob"),
83 ])
84 XCTAssertEqual(out, [SpaceApps.Item(id: "bookmarks", by: "Bob", ts: 2, uid: "u-bob")])
85 }
86
87 // Events at or below the snapshot horizon are already inside the seed β€”
88 // refolding them must not double-apply (apps.js foldInto's `mid <= upTo`).
89 func testSnapshotHorizonSkipsSeededEvents() {
90 let seed = [SpaceApps.Item(id: "gif", by: "Ana", ts: 1, uid: "u-ana")]
91 let out = fold(seed: seed, upTo: 10, [
92 9: .init(type: "spaceapps.remove", app: "gif", by: "Ana", ts: 9, uid: "u-ana"),
93 11: .init(type: "spaceapps.add", app: "vault", by: "Bob", ts: 11, uid: "u-bob"),
94 ])
95 XCTAssertEqual(out, [SpaceApps.Item(id: "gif", by: "Ana", ts: 1, uid: "u-ana"),
96 SpaceApps.Item(id: "vault", by: "Bob", ts: 11, uid: "u-bob")])
97 }
98
99 // The slot-"apps" snapshot decode. New snapshots hold the registry's state
100 // under the unified "installs" key (apps.js installsApp) β€” tolerant of
101 // other apps' states sharing the blob.
102 func testSnapshotParseUnifiedKey() {
103 let json = """
104 {"states":{"gif":{"q":""},"installs":{"apps":[{"id":"gif","by":"Ana","ts":1700000000000,"uid":"u-ana"}]}},"upTo":42}
105 """.data(using: .utf8)!
106 let parsed = SpaceApps.parseSnapshot(json)
107 XCTAssertEqual(parsed?.upTo, 42)
108 XCTAssertEqual(parsed?.seed, [SpaceApps.Item(id: "gif", by: "Ana",
109 ts: 1_700_000_000_000, uid: "u-ana")])
110 }
111
112 // Snapshots written before the registry unification hold the state under
113 // the legacy per-family "spaceapps" key β€” read as a fallback, and a pre-uid
114 // row's missing uid decodes to "".
115 func testSnapshotParseLegacyKey() {
116 let json = """
117 {"states":{"spaceapps":{"apps":[{"id":"bookmarks","by":"Ana","ts":7}]}},"upTo":7}
118 """.data(using: .utf8)!
119 let parsed = SpaceApps.parseSnapshot(json)
120 XCTAssertEqual(parsed?.upTo, 7)
121 XCTAssertEqual(parsed?.seed, [SpaceApps.Item(id: "bookmarks", by: "Ana", ts: 7, uid: "")])
122 }
123
124 func testSnapshotParseWithoutRegistryState() {
125 let json = #"{"states":{"gif":{"q":""}},"upTo":7}"#.data(using: .utf8)!
126 let parsed = SpaceApps.parseSnapshot(json)
127 XCTAssertEqual(parsed?.upTo, 7)
128 XCTAssertEqual(parsed?.seed, [])
129 }
130
131 // The wire decode: a real encrypted spaceapps.add round-trips into an
132 // Event; a non-spaceapps type and a missing app both decode to nil (fold
133 // input is only ever well-formed installs/removes).
134 func testWireDecode() throws {
135 let key = try GroupKey(rawKey: Data(repeating: 7, count: 32))
136 let add = #"{"type":"spaceapps.add","app":"bookmarks","name":"Ana","ts":1700000000000,"uid":"u-ana"}"#
137 let blob = try key.encryptData(Data(add.utf8))
138 XCTAssertEqual(SpaceApps.event(blob, key: key),
139 SpaceApps.Event(type: "spaceapps.add", app: "bookmarks", by: "Ana",
140 ts: 1_700_000_000_000, uid: "u-ana"))
141 let chat = try key.encryptData(Data(#"{"type":"chat","text":"hi"}"#.utf8))
142 XCTAssertNil(SpaceApps.event(chat, key: key))
143 let missing = try key.encryptData(Data(#"{"type":"spaceapps.add","name":"Mallory"}"#.utf8))
144 XCTAssertNil(SpaceApps.event(missing, key: key))
145 }
146}
147