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
127 lines
5.0 KiB
Swift
127 lines
5.0 KiB
Swift
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("originHeader:http + 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("originHeader:https + 非标端口保留端口")
|
||
func originHeaderKeepsNonStandardHTTPSPort() throws {
|
||
let sut = try #require(endpoint("https://mac.example:8443"))
|
||
#expect(sut.originHeader == "https://mac.example:8443")
|
||
}
|
||
|
||
@Test("originHeader:https + 443 → 无端口后缀(浏览器 Origin 序列化)")
|
||
func originHeaderOmitsDefaultHTTPSPort() throws {
|
||
let sut = try #require(endpoint("https://mac.example:443"))
|
||
#expect(sut.originHeader == "https://mac.example")
|
||
}
|
||
|
||
@Test("originHeader:http + 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("originHeader:scheme 与 host 小写规范化(服务器两侧 new URL() 规范化对称)")
|
||
func originHeaderLowercasesSchemeAndHost() throws {
|
||
let sut = try #require(endpoint("HTTP://Mac.Example:3000"))
|
||
#expect(sut.originHeader == "http://mac.example:3000")
|
||
}
|
||
|
||
@Test("originHeader:baseURL 的 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("originHeader:IPv6 host 保留方括号")
|
||
func originHeaderBracketsIPv6Host() throws {
|
||
let sut = try #require(endpoint("http://[fe80::1]:3000"))
|
||
#expect(sut.originHeader == "http://[fe80::1]:3000")
|
||
}
|
||
|
||
// MARK: - wsURL 派生向量(scheme http→ws / https→wss + WireConstants.wsPath)
|
||
|
||
@Test("wsURL:http → 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("wsURL:https → 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("wsURL:baseURL 带尾斜杠/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: - Codable(HostRegistry Keychain 持久化经由它)
|
||
|
||
@Test("Codable roundtrip:encode→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)
|
||
}
|
||
}
|