eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2@testable import LChatCore
3
4// πŸ€– End-to-end proof of the native claim pipeline against a REAL lchat server
5// (wire formats are exactly where a re-implementation lies). Skipped unless the
6// env points at one β€” run locally with:
7//
8// go build -o /tmp/lchat-itest . && LCHAT_ADMIN_TOKEN=itest-token \
9// LCHAT_DB=/tmp/lchat-itest.db ADDR=127.0.0.1:8091 /tmp/lchat-itest &
10// LCHAT_ITEST_ORIGIN=http://127.0.0.1:8091 LCHAT_ITEST_ADMIN_TOKEN=itest-token \
11// swift test --filter SpaceClaimIntegrationTests
12//
13// The test mints its own setup invite (the exact shape setup_invite.go mints),
14// claims it natively, and asserts the space is genuinely stood up: token valid,
15// user in the roster, instance key minted + unwrappable, label decryptable,
16// group named, setup marked complete.
17final class SpaceClaimIntegrationTests: XCTestCase {
18 func testClaimSetupInviteAgainstRealServer() async throws {
19 let env = ProcessInfo.processInfo.environment
20 guard let origin = env["LCHAT_ITEST_ORIGIN"], let admin = env["LCHAT_ITEST_ADMIN_TOKEN"] else {
21 throw XCTSkip("set LCHAT_ITEST_ORIGIN + LCHAT_ITEST_ADMIN_TOKEN to run")
22 }
23 let base = URL(string: origin)!
24
25 // Mint a setup invite the way setup_invite.go does (admin-token API).
26 let t = InviteCrypto.randomSecret()
27 let derived = try InviteCrypto.derive(t)
28 var mint = URLRequest(url: base.appending(path: "/api/admin/key-invites"))
29 mint.httpMethod = "POST"
30 mint.setValue("Bearer " + admin, forHTTPHeaderField: "Authorization")
31 mint.setValue("application/json", forHTTPHeaderField: "Content-Type")
32 mint.httpBody = try JSONSerialization.data(withJSONObject: [
33 "id": derived.id, "claim_hash": derived.claimHash, "grants_admin": true,
34 "expires": Int(Date().addingTimeInterval(3600).timeIntervalSince1970),
35 ])
36 let (_, mintResp) = try await URLSession.shared.data(for: mint)
37 XCTAssertEqual((mintResp as? HTTPURLResponse)?.statusCode, 200, "invite mint failed")
38
39 // Claim it natively β€” the full pipeline.
40 let inst = try await SpaceClaim.claim(setupLink: origin + "/join/setup#" + t,
41 screenName: "Overnight Tester")
42 XCTAssertEqual(inst.origin, origin)
43 XCTAssertFalse(inst.deviceToken.isEmpty)
44 XCTAssertFalse(inst.myUserId.isEmpty, "roster should already know us")
45
46 // The token works and the roster carries our (encrypted) label.
47 let api = LChatAPI(baseURL: base, token: inst.deviceToken)
48 let me = await api.roster().first { $0.user_id == inst.myUserId }
49 XCTAssertNotNil(me)
50 XCTAssertEqual(me?.public_key, inst.userPub)
51
52 // First-member instance-key mint: our wrapped copy exists and opens
53 // with our private key; the published label decrypts under it.
54 let status = await api.instanceKeyStatus()
55 let wrapped = try XCTUnwrap(status?.wrapped, "instance key should be minted + granted to us")
56 let raw = try ECIES.unwrap(wrappedB64: wrapped, privPkcs8B64: inst.userPriv)
57 if let labelBlob = me?.label_blob {
58 let label = try GroupKey(rawKey: raw).decryptString(blob: labelBlob)
59 XCTAssertEqual(label, "Overnight Tester")
60 } else {
61 XCTFail("label_blob should be published at claim time")
62 }
63
64 // The minimal native setup: name + setup_complete via the admin-ROLE
65 // device token (never the admin token).
66 let ok = await SpaceSetup.finish(origin: origin, token: inst.deviceToken,
67 name: "Goes To Eleven")
68 XCTAssertTrue(ok)
69 let groupName = await api.groupName()
70 XCTAssertEqual(groupName, "Goes To Eleven")
71
72 // A second claim of the same (single-use) invite must fail.
73 do {
74 _ = try await SpaceClaim.claim(setupLink: origin + "/join/setup#" + t,
75 screenName: "Second Claimer")
76 XCTFail("single-use invite claimed twice")
77 } catch {}
78
79 // A setup (admin-granting) claim lands in the SETUP conversation, so
80 // the claim must NOT have created a welcome thread for the admin.
81 let adminThreads = await api.threads()
82 for th in adminThreads {
83 guard th.kind == "self", let nb = th.name_blob, !nb.isEmpty,
84 let w = th.wrapped, !w.isEmpty,
85 let rawT = try? ECIES.unwrap(wrappedB64: w, privPkcs8B64: inst.userPriv),
86 let payload = try? GroupKey(rawKey: rawT)
87 .decryptJSON(AppThreadPayload.self, blob: nb) else { continue }
88 XCTAssertNotEqual(payload.app, "welcome",
89 "an admin-granting claim must not create a welcome thread")
90 }
91
92 // ---- the joiner side (#1376): a MEMBER invite claimed natively ------
93 // Mint a plain member invite (no grants_admin) and claim it as a
94 // second person β€” the Mac/iOS "Add from link" path.
95 let t2 = InviteCrypto.randomSecret()
96 let derived2 = try InviteCrypto.derive(t2)
97 var mint2 = URLRequest(url: base.appending(path: "/api/admin/key-invites"))
98 mint2.httpMethod = "POST"
99 mint2.setValue("Bearer " + admin, forHTTPHeaderField: "Authorization")
100 mint2.setValue("application/json", forHTTPHeaderField: "Content-Type")
101 mint2.httpBody = try JSONSerialization.data(withJSONObject: [
102 "id": derived2.id, "claim_hash": derived2.claimHash, "label": "Joiner",
103 "expires": Int(Date().addingTimeInterval(3600).timeIntervalSince1970),
104 ])
105 let (_, mint2Resp) = try await URLSession.shared.data(for: mint2)
106 XCTAssertEqual((mint2Resp as? HTTPURLResponse)?.statusCode, 200, "member invite mint failed")
107
108 let joiner = try await SpaceClaim.claim(setupLink: origin + "/join/member#" + t2,
109 screenName: "")
110 XCTAssertFalse(joiner.myUserId.isEmpty)
111
112 // The joiner is keyless (the key exists, held by the admin, and no
113 // grant has landed β€” no live holder answered the claim-time nudge)
114 // and the claim must NOT have re-minted the instance key over it.
115 let joinerAPI = LChatAPI(baseURL: base, token: joiner.deviceToken)
116 let joinerStatus = await joinerAPI.instanceKeyStatus()
117 XCTAssertNil(joinerStatus?.wrapped, "no holder was online to grant")
118 XCTAssertEqual(joinerStatus?.have, [inst.myUserId],
119 "the admin's key stands β€” a joiner never re-mints")
120
121 // The welcome app-thread exists, keyed to the joiner, its encrypted
122 // name_blob naming the welcome app β€” what WelcomeLanding.threadToLand
123 // scans for, so the bundled flow runs and asks the name.
124 let joinerThreads = await joinerAPI.threads()
125 let welcome = joinerThreads.first { th in
126 guard th.kind == "self", let nb = th.name_blob, !nb.isEmpty,
127 let w = th.wrapped, !w.isEmpty,
128 let rawT = try? ECIES.unwrap(wrappedB64: w, privPkcs8B64: joiner.userPriv),
129 let payload = try? GroupKey(rawKey: rawT)
130 .decryptJSON(AppThreadPayload.self, blob: nb) else { return false }
131 return payload.app == "welcome"
132 }
133 XCTAssertNotNil(welcome, "a member-invite claim creates Eleven's welcome thread (#1376)")
134 }
135}
136