1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151 | import Foundation
import Testing
@testable import IrdP2PSignaling
@Suite struct MailboxTests {
let endpoint = URL(string: "https://app.example.test/mailbox/v1/")!
func token(_ t: String = "tok", expiresIn: TimeInterval = 3600) -> MailboxToken { MailboxToken(token: t, expiresAt: Date().addingTimeInterval(expiresIn)) }
@Test func roomIsOnePercentEncodedSegment() {
#expect(MailboxWire.messagesURL(endpoint: endpoint, room: "lobby").absoluteString == "https://app.example.test/mailbox/v1/rooms/lobby/messages")
#expect(MailboxWire.messagesURL(endpoint: endpoint, room: "/orbitdb/zdpu123/chat").absoluteString == "https://app.example.test/mailbox/v1/rooms/%2Forbitdb%2Fzdpu123%2Fchat/messages")
#expect(MailboxWire.messagesURL(endpoint: endpoint, room: "a b:c?d#e%f+g").absoluteString == "https://app.example.test/mailbox/v1/rooms/a%20b%3Ac%3Fd%23e%25f%2Bg/messages")
#expect(MailboxWire.messagesURL(endpoint: endpoint, room: "ăș").absoluteString == "https://app.example.test/mailbox/v1/rooms/%C4%83%C8%99/messages")
#expect(throws: P2PError.config("mailbox room id is blank")) { try MailboxWire.validateRoom(" ") }
#expect(throws: P2PError.self) { try MailboxWire.validateRoom(String(repeating: "x", count: 4097)) }
}
@Test func seqOrdersBeyondDoublePrecision() throws {
let a = MailboxSeq("1873421000000000000001"), b = MailboxSeq("1873421000000000000002")
#expect(a < b)
#expect(MailboxSeq("9") < MailboxSeq("10"))
#expect(MailboxSeq("99999999999999999999999") > MailboxSeq("100000000000000000000"))
let page = ReplayPage(messages: [], hasMore: false, oldestSeq: MailboxSeq("500"), latestSeq: MailboxSeq("900"))
#expect(MailboxClient.isHistoryGap(cursor: MailboxSeq("400"), page: page))
#expect(!MailboxClient.isHistoryGap(cursor: MailboxSeq("500"), page: page))
#expect(!MailboxClient.isHistoryGap(cursor: nil, page: page))
let decoded = try JSONDecoder().decode([MailboxSeq].self, from: Data(#"["18734210000000000000","7"]"#.utf8))
#expect(decoded.map(\.raw) == ["18734210000000000000", "7"])
}
@Test func depositSendsBearerAndOctetStream() async throws {
let http = FakeHTTP { r in
#expect(r.httpMethod == "POST")
#expect(r.value(forHTTPHeaderField: "Authorization") == "Bearer tok")
#expect(r.value(forHTTPHeaderField: "Content-Type") == "application/octet-stream")
#expect(r.value(forHTTPHeaderField: "Origin") == "https://o.example")
#expect(r.httpBody == Data([1, 2, 3]))
return json(201, #"{"seq":"1873421","id":"00ff","expires_at":1787003600,"deduped":false}"#)
}
let client = MailboxClient(endpoint: endpoint, tokens: StaticTokenProvider(token()), http: http, origin: "https://o.example")
let r = try await client.deposit(room: "lobby", Data([1, 2, 3]))
#expect(r == DepositResult(seq: MailboxSeq("1873421"), id: "00ff", expiresAt: Date(timeIntervalSince1970: 1787003600), deduped: false))
await #expect(throws: P2PError.config("mailbox payload is empty")) { try await client.deposit(room: "lobby", Data()) }
}
@Test func dedupedDepositIs200() async throws {
let http = FakeHTTP { _ in json(200, #"{"seq":"5","id":"aa","expires_at":1,"deduped":true}"#) }
let client = MailboxClient(endpoint: endpoint, tokens: StaticTokenProvider(token()), http: http)
#expect(try await client.deposit(room: "r", Data([1])).deduped)
}
@Test func authFailureRemintsOnceThenThrows() async throws {
let http = FakeHTTP { _ in json(401, #"{"error":"token_expired"}"#) }
let mint = FakeHTTP { _ in json(200, #"{"token":"t2","ttl":60,"expires_at":\#(Int(Date().timeIntervalSince1970) + 60),"endpoint":"https://app.example.test/mailbox/v1"}"#) }
let provider = OpenMintTokenProvider(mintURL: URL(string: "https://app.example.test/mailbox-credentials")!, origin: "https://o.example", http: mint)
let client = MailboxClient(endpoint: endpoint, tokens: provider, http: http)
await #expect(throws: P2PError.http(status: 401, code: "token_expired", body: Data(#"{"error":"token_expired"}"#.utf8))) {
try await client.deposit(room: "r", Data([1]))
}
#expect(http.count == 2) // original + one retry
#expect(mint.count == 2) // initial mint + re-mint
#expect(mint.requests.first?.url?.query == "ttl=86400") // app-scoped: no room parameter
#expect(mint.requests.first?.value(forHTTPHeaderField: "Origin") == "https://o.example")
}
@Test func openMintCachesAndRenewsFiveMinutesEarly() async throws {
nonisolated(unsafe) var now = Date(timeIntervalSince1970: 1_000_000)
let mint = FakeHTTP { _ in json(200, #"{"token":"t","expires_at":1000600}"#) } // 600 s ttl
let provider = OpenMintTokenProvider(mintURL: URL(string: "https://app.example.test/mailbox-credentials")!, origin: nil, http: mint, ttlSeconds: 600, scopeToRoom: true, now: { now })
_ = try await provider.token(for: "r/1")
_ = try await provider.token(for: "r/1")
#expect(mint.count == 1)
#expect(mint.last?.url?.query == "room=r/1&ttl=600" || mint.last?.url?.query == "room=r%2F1&ttl=600")
_ = try await provider.token(for: "other")
#expect(mint.count == 2) // room-scoped: one per room
now = now.addingTimeInterval(301) // 299 s left < 300 s renew window
_ = try await provider.token(for: "r/1")
#expect(mint.count == 3)
}
@Test func mintErrorsSurfaceAsHTTP() async {
let mint = FakeHTTP { _ in json(404, #"{"error":"not_found"}"#) }
let provider = OpenMintTokenProvider(mintURL: URL(string: "https://app.example.test/mailbox-credentials")!, origin: nil, http: mint)
await #expect(throws: P2PError.http(status: 404, code: "not_found", body: Data(#"{"error":"not_found"}"#.utf8))) { try await provider.token(for: "r") }
}
@Test func replayDecodesAndPages() async throws {
let pages = [
#"{"messages":[{"seq":"10","id":"a","ts":1712345678901,"payload":"AQID"},{"seq":"11","id":"b","ts":1712345678902,"payload":"BA=="}],"has_more":true,"oldest_seq":"10","latest_seq":"12"}"#,
#"{"messages":[{"seq":"12","id":"c","ts":1712345678903,"payload":"BQ=="}],"has_more":false,"oldest_seq":"10","latest_seq":"12"}"#,
]
let http = FakeHTTP { r in
let q = r.url!.query!
if q == "after=11&limit=100" || q == "before=12&limit=500" { return json(200, pages[1]) }
#expect(q == "after=5&limit=100")
return json(200, pages[0])
}
let client = MailboxClient(endpoint: endpoint, tokens: StaticTokenProvider(token()), http: http)
let r = try await client.replayAll(room: "lobby", after: MailboxSeq("5"), limit: 100)
#expect(r.messages.map(\.payload) == [Data([1, 2, 3]), Data([4]), Data([5])])
#expect(r.messages[0].date == Date(timeIntervalSince1970: 1712345678.901))
#expect(r.cursor == MailboxSeq("12"))
#expect(r.gap) // cursor 5 < oldest 10
#expect(http.count == 2)
let page = try await client.replay(room: "lobby", before: MailboxSeq("12"), limit: 9999)
#expect(http.last?.url?.query == "before=12&limit=500")
#expect(page.hasMore == false)
}
@Test func nonSuccessMapsToHTTPErrorWithCode() async {
let http = FakeHTTP { _ in json(413, #"{"error":"message_too_large"}"#) }
let client = MailboxClient(endpoint: endpoint, tokens: StaticTokenProvider(token()), http: http)
do {
_ = try await client.deposit(room: "r", Data([1]))
Issue.record("expected failure")
} catch let e as P2PError {
#expect(e.mailboxCode == .messageTooLarge)
if case .http(let status, _, _) = e { #expect(status == 413) } else { Issue.record("wrong error \(e)") }
} catch {
Issue.record("unexpected error \(error)")
}
}
@Test func outboxRetriesBacksOffAndDrops() async throws {
nonisolated(unsafe) var statuses = [429, 507, 201, 413, 201]
let http = FakeHTTP { _ in
let s = statuses.removeFirst()
return s == 201 ? json(201, #"{"seq":"1","id":"a","expires_at":1}"#) : json(s, #"{"error":"x"}"#)
}
let client = MailboxClient(endpoint: endpoint, tokens: StaticTokenProvider(token()), http: http)
let clock = TestClock()
nonisolated(unsafe) var outcomes: [MailboxOutbox.Outcome] = []
let outbox = MailboxOutbox(client: client, clock: clock) { outcomes.append($0) }
await outbox.enqueue(room: "r", payload: Data([1]))
await outbox.enqueue(room: "r", payload: Data([2]))
await outbox.enqueue(room: "r", payload: Data([3]))
let flush = Task { await outbox.flush() }
#expect(await eventually { clock.sleeperCount == 1 })
#expect(outcomes.first == .deferred(.init(room: "r", payload: Data([1]), attempts: 1), retryAfter: .seconds(1)))
clock.advance(by: .seconds(1))
#expect(await eventually { clock.sleeperCount == 1 })
#expect(outcomes[1] == .deferred(.init(room: "r", payload: Data([1]), attempts: 2), retryAfter: .seconds(2)))
clock.advance(by: .seconds(2))
await flush.value
#expect(outcomes.count == 5)
if case .deposited = outcomes[2] {} else { Issue.record("expected deposit, got \(outcomes[2])") }
#expect(outcomes[3] == .dropped(.init(room: "r", payload: Data([2])), reason: "message_too_large"))
if case .deposited = outcomes[4] {} else { Issue.record("expected deposit") }
#expect(await outbox.pending.isEmpty)
}
}
|