12 · Tests
Tests/SophaxChatCoreTests/NetworkTests.swift
245 lines · Swift
1// NetworkTests.swift
2// SophaxChatCoreTests
3//
4// Tests for:
5// • TCP length-prefix framing (4-byte big-endian: parse, multi-frame, partial)
6// • WireMessage encode/decode round-trip (all message types carry correctly)
7// • PreKeyBundle tcpAddress field propagation
8
9import Testing
10import Foundation
11import CryptoKit
12@testable import SophaxChatCore
13
14// MARK: - TCP Framing Logic
15//
16// TCPTransport uses a 4-byte big-endian length prefix.
17// These tests validate the framing algorithm independently of NWConnection
18// so we can catch regressions without real networking.
19
20private func encodeFrame(_ data: Data) -> Data {
21 var len = UInt32(data.count).bigEndian
22 return Data(bytes: &len, count: 4) + data
23}
24
25/// Decode as many complete frames from `buf` as possible.
26/// Returns (frames, remainingBytes) — mirrors TCPTransport.didReceive(data:from:).
27private func decodeFrames(from buf: Data, maxSize: Int = 4 * 1024 * 1024) -> (frames: [Data], remainder: Data) {
28 var buf = buf
29 var frames: [Data] = []
30 while buf.count >= 4 {
31 let length = buf.prefix(4).withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
32 guard length <= maxSize else { return (frames, Data()) } // oversized: discard buffer
33 guard buf.count >= 4 + Int(length) else { break }
34 frames.append(Data(buf[4..<(4 + Int(length))]))
35 buf = Data(buf[(4 + Int(length))...])
36 }
37 return (frames, buf)
38}
39
40@Suite("TCP Framing")
41struct TCPFramingTests {
42
43 @Test("Single frame round-trip")
44 func singleFrameRoundTrip() {
45 let payload = Data("hello sophaxchat".utf8)
46 let framed = encodeFrame(payload)
47 let (frames, remainder) = decodeFrames(from: framed)
48 #expect(frames.count == 1)
49 #expect(frames[0] == payload)
50 #expect(remainder.isEmpty)
51 }
52
53 @Test("Multiple frames in one buffer are all decoded")
54 func multipleFrames() {
55 let p1 = Data("frame one".utf8)
56 let p2 = Data("frame two".utf8)
57 let p3 = Data("frame three".utf8)
58 let combined = encodeFrame(p1) + encodeFrame(p2) + encodeFrame(p3)
59 let (frames, remainder) = decodeFrames(from: combined)
60 #expect(frames.count == 3)
61 #expect(frames[0] == p1)
62 #expect(frames[1] == p2)
63 #expect(frames[2] == p3)
64 #expect(remainder.isEmpty)
65 }
66
67 @Test("Partial frame: header present but body incomplete → zero frames, full buffer preserved")
68 func partialFrameHeaderOnly() {
69 let payload = Data(repeating: 0xAB, count: 100)
70 let full = encodeFrame(payload)
71 // Deliver only the 4-byte header — body missing
72 let partial = full.prefix(4)
73 let (frames, remainder) = decodeFrames(from: partial)
74 #expect(frames.isEmpty)
75 #expect(remainder == partial)
76 }
77
78 @Test("Partial frame: body truncated by 1 byte")
79 func partialFrameBodyTruncated() {
80 let payload = Data(repeating: 0xCD, count: 50)
81 let full = encodeFrame(payload)
82 // Drop the last byte
83 let truncated = full.dropLast(1)
84 let (frames, remainder) = decodeFrames(from: Data(truncated))
85 #expect(frames.isEmpty)
86 #expect(remainder.count == full.count - 1)
87 }
88
89 @Test("Frame followed by partial next frame: first decoded, partial preserved")
90 func completeThenPartial() {
91 let p1 = Data("complete".utf8)
92 let p2 = Data(repeating: 0xFF, count: 200)
93 let buf = encodeFrame(p1) + encodeFrame(p2).prefix(10) // only 10 bytes of second frame
94 let (frames, remainder) = decodeFrames(from: buf)
95 #expect(frames.count == 1)
96 #expect(frames[0] == p1)
97 #expect(remainder.count == 10)
98 }
99
100 @Test("Empty payload produces a 4-byte zero-length frame")
101 func emptyPayload() {
102 let framed = encodeFrame(Data())
103 #expect(framed.count == 4)
104 let (frames, _) = decodeFrames(from: framed)
105 #expect(frames.count == 1)
106 #expect(frames[0].isEmpty)
107 }
108
109 @Test("Oversized frame (> maxSize) causes buffer discard")
110 func oversizedFrameDiscarded() {
111 // Craft a 4-byte header claiming 10 MB (above the 4 MB cap)
112 var len = UInt32(10 * 1024 * 1024).bigEndian
113 let oversized = Data(bytes: &len, count: 4)
114 let (frames, remainder) = decodeFrames(from: oversized, maxSize: 4 * 1024 * 1024)
115 #expect(frames.isEmpty)
116 #expect(remainder.isEmpty, "Buffer must be discarded when oversized frame detected")
117 }
118
119 @Test("Length prefix is big-endian")
120 func bigEndianLength() {
121 let payload = Data(repeating: 0x00, count: 256)
122 let framed = encodeFrame(payload)
123 // Bytes [0..3] must encode 256 in big-endian = 0x00 0x00 0x01 0x00
124 #expect(framed[0] == 0x00)
125 #expect(framed[1] == 0x00)
126 #expect(framed[2] == 0x01)
127 #expect(framed[3] == 0x00)
128 }
129}
130
131// MARK: - WireMessage Codec
132
133@Suite("WireMessage Codec")
134struct WireMessageCodecTests {
135
136 private func makeIdentity() throws -> IdentityManager {
137 let kc = KeychainManager(service: "test.wire.\(UUID().uuidString)")
138 return try IdentityManager(keychain: kc)
139 }
140
141 @Test("Hello WireMessage encodes and decodes correctly")
142 func helloRoundTrip() throws {
143 let identity = try makeIdentity()
144 let kc = KeychainManager(service: "test.wire.pk.\(UUID().uuidString)")
145 let preKeys = try PreKeyManager(identity: identity, keychain: kc)
146 let bundle = try preKeys.generateBundle(tcpAddress: "127.0.0.1:25519")
147 let hello = HelloMessage(bundle: bundle)
148 let builder = WireMessageBuilder(identity: identity)
149 let wire = try builder.build(.hello, payload: hello)
150
151 let data = try JSONEncoder().encode(wire)
152 let decoded = try JSONDecoder().decode(WireMessage.self, from: data)
153
154 #expect(decoded.type == .hello)
155 #expect(decoded.senderID == identity.publicIdentity.peerID)
156
157 let decodedHello = try builder.decodePayload(HelloMessage.self, from: decoded)
158 #expect(decodedHello.bundle.username == identity.publicIdentity.username)
159 #expect(decodedHello.bundle.tcpAddress == "127.0.0.1:25519")
160 }
161
162 @Test("WireMessage signature round-trip verifies correctly")
163 func signatureVerifies() throws {
164 let identity = try makeIdentity()
165 let builder = WireMessageBuilder(identity: identity)
166 let kc = KeychainManager(service: "test.wire.pk2.\(UUID().uuidString)")
167 let preKeys = try PreKeyManager(identity: identity, keychain: kc)
168 let bundle = try preKeys.generateBundle()
169 let hello = HelloMessage(bundle: bundle)
170 let wire = try builder.build(.hello, payload: hello)
171
172 let isValid = try WireMessageBuilder.verify(wire, signingKeyPublic: identity.publicIdentity.signingKeyPublic)
173 #expect(isValid, "Signature on self-generated message must verify")
174 }
175
176 @Test("Tampered payload fails signature verification")
177 func tamperedPayloadFails() throws {
178 let identity = try makeIdentity()
179 let builder = WireMessageBuilder(identity: identity)
180 let kc = KeychainManager(service: "test.wire.pk3.\(UUID().uuidString)")
181 let preKeys = try PreKeyManager(identity: identity, keychain: kc)
182 let bundle = try preKeys.generateBundle()
183 let hello = HelloMessage(bundle: bundle)
184 var wire = try builder.build(.hello, payload: hello)
185
186 // Flip one bit in the payload
187 var payload = wire.payload
188 payload[0] ^= 0xFF
189 wire = WireMessage(type: wire.type, payload: payload, senderID: wire.senderID,
190 timestamp: wire.timestamp, signature: wire.signature)
191
192 let isValid = try WireMessageBuilder.verify(wire, signingKeyPublic: identity.publicIdentity.signingKeyPublic)
193 #expect(!isValid, "Tampered payload must not verify")
194 }
195}
196
197// MARK: - PreKeyBundle tcpAddress propagation
198
199@Suite("PreKeyBundle TCP address")
200struct PreKeyBundleTCPAddressTests {
201
202 private func makeBundle(tcpAddress: String?) throws -> PreKeyBundle {
203 let kc = KeychainManager(service: "test.pkb.\(UUID().uuidString)")
204 let identity = try IdentityManager(keychain: kc)
205 let preKeys = try PreKeyManager(identity: identity, keychain: kc)
206 return try preKeys.generateBundle(tcpAddress: tcpAddress)
207 }
208
209 @Test("tcpAddress is nil when not provided")
210 func nilTCPAddress() throws {
211 let bundle = try makeBundle(tcpAddress: nil)
212 #expect(bundle.tcpAddress == nil)
213 }
214
215 @Test("tcpAddress is preserved through bundle generation")
216 func tcpAddressPreserved() throws {
217 let addr = "203.0.113.42:25519"
218 let bundle = try makeBundle(tcpAddress: addr)
219 #expect(bundle.tcpAddress == addr)
220 }
221
222 @Test("Tor .onion address is accepted as tcpAddress")
223 func onionAddressAccepted() throws {
224 let onion = "abcdefghijklmnop.onion:25519"
225 let bundle = try makeBundle(tcpAddress: onion)
226 #expect(bundle.tcpAddress == onion)
227 }
228
229 @Test("PreKeyBundle encodes and decodes tcpAddress correctly")
230 func bundleCodableWithTCPAddress() throws {
231 let addr = "198.51.100.1:25519"
232 let bundle = try makeBundle(tcpAddress: addr)
233 let data = try JSONEncoder().encode(bundle)
234 let decoded = try JSONDecoder().decode(PreKeyBundle.self, from: data)
235 #expect(decoded.tcpAddress == addr)
236 }
237
238 @Test("KnownPeer learns tcpAddress from PreKeyBundle")
239 func knownPeerLearnsTCPAddress() throws {
240 let addr = "198.51.100.2:25519"
241 let bundle = try makeBundle(tcpAddress: addr)
242 let peer = KnownPeer(from: bundle, safetyNumber: "test-sn")
243 #expect(peer.tcpAddress == addr)
244 }
245}