feat(ios): W0 scaffold + day-1 spike + WireProtocol frozen contract + TestSupport doubles

T-iOS-1: ios/ XcodeGen project (iOS 17, Swift 6 strict concurrency, ATS per PLAN §5.2), 5 SPM package shells, CI skeleton
T-iOS-2: Origin spike vs real server — URLSessionWebSocketTask custom Origin CONFIRMED (no Starscream); 16MiB replay + EMSGSIZE(40) errno correction written back to plan
T-iOS-3: WireProtocol frozen contract, 59 tests, 100% line coverage, cross-impl vectors vs src/protocol.ts via tsx
T-iOS-4: FakeTransport/FakeClock/FakeHTTPTransport doubles
Verify: independent agent re-ran all acceptance — 6/6 PASS
This commit is contained in:
Yaojia Wang
2026-07-04 21:19:30 +02:00
parent 9b41ffa574
commit cbaa08daba
46 changed files with 3203 additions and 3 deletions

View File

@@ -0,0 +1,126 @@
import Foundation
import Testing
import WireProtocol
// T-iOS-3 · HostEndpoint originHeader/wsURL T-iOS-7 plan §7
// Origin plan §5.1 Origin
private func endpoint(_ urlString: String) -> HostEndpoint? {
guard let url = URL(string: urlString) else { return nil }
return HostEndpoint(baseURL: url)
}
// MARK: - originHeader
@Test("originHeaderhttp + IPv4 + 非默认端口 → 与 baseURL 同串")
func originHeaderKeepsNonDefaultPortIPv4() throws {
let sut = try #require(endpoint("http://192.168.1.5:3000"))
#expect(sut.originHeader == "http://192.168.1.5:3000")
}
@Test("originHeaderhttps + 非标端口保留端口")
func originHeaderKeepsNonStandardHTTPSPort() throws {
let sut = try #require(endpoint("https://mac.example:8443"))
#expect(sut.originHeader == "https://mac.example:8443")
}
@Test("originHeaderhttps + 443 → 无端口后缀(浏览器 Origin 序列化)")
func originHeaderOmitsDefaultHTTPSPort() throws {
let sut = try #require(endpoint("https://mac.example:443"))
#expect(sut.originHeader == "https://mac.example")
}
@Test("originHeaderhttp + 80 → 无端口后缀")
func originHeaderOmitsDefaultHTTPPort() throws {
let sut = try #require(endpoint("http://mac.example:80"))
#expect(sut.originHeader == "http://mac.example")
}
@Test("originHeader无端口 URL → 无端口后缀localhost 保留非默认端口")
func originHeaderWithoutExplicitPort() throws {
let bare = try #require(endpoint("https://mac.tailnet-1234.ts.net"))
#expect(bare.originHeader == "https://mac.tailnet-1234.ts.net")
let localhost = try #require(endpoint("http://localhost:3000"))
#expect(localhost.originHeader == "http://localhost:3000")
}
@Test("originHeaderscheme 与 host 小写规范化(服务器两侧 new URL() 规范化对称)")
func originHeaderLowercasesSchemeAndHost() throws {
let sut = try #require(endpoint("HTTP://Mac.Example:3000"))
#expect(sut.originHeader == "http://mac.example:3000")
}
@Test("originHeaderbaseURL 的 path/query 不进入 Origin")
func originHeaderIgnoresPathAndQuery() throws {
let sut = try #require(endpoint("http://192.168.1.5:3000/index.html?x=1"))
#expect(sut.originHeader == "http://192.168.1.5:3000")
}
@Test("originHeaderIPv6 host 保留方括号")
func originHeaderBracketsIPv6Host() throws {
let sut = try #require(endpoint("http://[fe80::1]:3000"))
#expect(sut.originHeader == "http://[fe80::1]:3000")
}
// MARK: - wsURL scheme httpws / httpswss + WireConstants.wsPath
@Test("wsURLhttp → ws 同 host 同 port + /term")
func wsURLDerivesWsFromHttp() throws {
let sut = try #require(endpoint("http://192.168.1.5:3000"))
#expect(sut.wsURL.absoluteString == "ws://192.168.1.5:3000/term")
}
@Test("wsURLhttps → wss + /term非标端口保留")
func wsURLDerivesWssFromHttps() throws {
let sut = try #require(endpoint("https://mac.example:8443"))
#expect(sut.wsURL.absoluteString == "wss://mac.example:8443/term")
}
@Test("wsURL无端口 https → wss 无端口path 恒为 WireConstants.wsPath")
func wsURLWithoutPort() throws {
let sut = try #require(endpoint("https://mac.tailnet-1234.ts.net"))
#expect(sut.wsURL.absoluteString == "wss://mac.tailnet-1234.ts.net/term")
#expect(sut.wsURL.path == WireConstants.wsPath)
}
@Test("wsURLbaseURL 带尾斜杠/path/query 时仍只保留 /term")
func wsURLReplacesPathAndDropsQuery() throws {
let sut = try #require(endpoint("http://192.168.1.5:3000/launcher?join=abc"))
#expect(sut.wsURL.absoluteString == "ws://192.168.1.5:3000/term")
}
// MARK: - http(s) / host plan §5
@Test("init非 http(s) scheme 或无 host → nil",
arguments: ["ftp://mac.example", "file:///tmp/x", "mailto:hi@example.com", "http://", "ws://mac.example:3000"])
func initRejectsNonHTTPBaseURLs(urlString: String) {
#expect(endpoint(urlString) == nil)
}
// MARK: - CodableHostRegistry Keychain
@Test("Codable roundtripencode→decode 恒等")
func codableRoundtripPreservesEquality() throws {
// Arrange
let original = try #require(endpoint("https://mac.example:8443"))
// Act
let data = try JSONEncoder().encode(original)
let decoded = try JSONDecoder().decode(HostEndpoint.self, from: data)
// Assert
#expect(decoded == original)
#expect(decoded.originHeader == original.originHeader)
#expect(decoded.wsURL == original.wsURL)
}
@Test("Codable持久化数据被篡改成非 http(s) URL → decode 显式 throw边界再验证")
func codableDecodeRevalidatesBaseURL() {
// Arrange
let tampered = Data("{\"baseURL\":\"ftp://mac.example\"}".utf8)
// Act / Assert
#expect(throws: DecodingError.self) {
_ = try JSONDecoder().decode(HostEndpoint.self, from: tampered)
}
}