feat(ios): P1-A — server touch-points (lastOutputAt, APNs sender+token endpoint) + APIClient P1 contract
T-iOS-37: LiveSessionInfo.lastOutputAt additive-optional field + manager.list() mapping (+4 tests; web consumers unaffected) T-iOS-20: src/push/apns.ts — env-gated (all-or-disabled, no crash, zero key-material logging), hand-rolled ES256 JWT (node:crypto ieee-p1363, zero new deps), NEEDS-INPUT/DONE payloads with structural minimization, token store per subscription-store conventions, combineNotifyServices parallel to web-push, POST/DELETE /push/apns-token per frozen wire shape (G-guard, 5/min/IP, 8kb); 65 tests incl. dual-channel e2e vs local fake APNs T-iOS-38: APIClient builders — apns-token (client-side hex mirror, pre-network reject), projects/detail (lossy decode, single-point percent-encoding), prefs (unknown-key byte-exact round-trip preservation), public four-tier HostNetworkTier Coordination: webterminal:// CFBundleURLTypes pre-registered in project.yml for T-iOS-22 Verified: root 1470 tests + tsc clean; APIClient 76 tests, 95.26% coverage; wire-shape cross-check zero mismatches
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import WireProtocol
|
||||
import APIClient
|
||||
|
||||
/// T-iOS-38 · 四层 host 分级(plan §5.4 提示分层表),公开 API:
|
||||
/// loopback / privateLAN / tailscale / public。W3 时 PairingViewModel 因
|
||||
/// `isPrivateOrLocalHost` 是 internal 单桶谓词而自行重写了分类——本类型
|
||||
/// 是给 VM 换用的单一出处(本任务不改 VM,见日志条目)。
|
||||
/// 注意本文件**不用 @testable**:证明 API 确实 public。
|
||||
struct HostClassificationTests {
|
||||
@Test("loopback 层:localhost/127.0.0.0\u{2044}8/IPv6 ::1(裸与带括号)")
|
||||
func loopbackTierMatchesLocalTargets() {
|
||||
// Arrange
|
||||
let hosts = ["localhost", "LOCALHOST", "127.0.0.1", "127.8.8.8", "::1", "[::1]"]
|
||||
|
||||
// Act + Assert
|
||||
for host in hosts {
|
||||
#expect(HostClassifier.classify(host: host) == .loopback, "\(host) 应为 loopback")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("privateLAN 层:RFC1918 三段 + link-local 169.254\u{2044}16 + mDNS .local")
|
||||
func privateLANTierMatchesRFC1918AndLinkLocal() {
|
||||
// Arrange
|
||||
let hosts = [
|
||||
"10.0.0.5", "192.168.0.9", "172.16.0.1", "172.31.255.255",
|
||||
"169.254.1.1", "mac-mini.local", "Mac-Mini.LOCAL",
|
||||
]
|
||||
|
||||
// Act + Assert
|
||||
for host in hosts {
|
||||
#expect(HostClassifier.classify(host: host) == .privateLAN, "\(host) 应为 privateLAN")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("tailscale 层:CGNAT 100.64.0.0\u{2044}10 与 MagicDNS *.ts.net")
|
||||
func tailscaleTierMatchesCGNATAndMagicDNS() {
|
||||
// Arrange
|
||||
let hosts = [
|
||||
"100.64.0.1", "100.100.1.1", "100.127.255.255",
|
||||
"mac.tailnet.ts.net", "foo.TS.NET",
|
||||
]
|
||||
|
||||
// Act + Assert
|
||||
for host in hosts {
|
||||
#expect(HostClassifier.classify(host: host) == .tailscale, "\(host) 应为 tailscale")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("public 层:公网 IP/域名/越界 CIDR/畸形 IPv4 一律最强警告层")
|
||||
func publicTierIsTheFailSafeDefault() {
|
||||
// Arrange — 越界:172.32 超出 172.16/12;100.128 与 100.63 超出 100.64/10
|
||||
let hosts = [
|
||||
"8.8.8.8", "203.0.113.7", "example.com", "tsnet.example.com",
|
||||
"172.32.0.1", "100.128.0.1", "100.63.255.255", "256.1.1.1", "1.2.3", "",
|
||||
]
|
||||
|
||||
// Act + Assert — 分不清 → public(fail-safe:宁可多警告,plan §5.4)
|
||||
for host in hosts {
|
||||
#expect(HostClassifier.classify(host: host) == .public, "\(host) 应为 public")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("classify(endpoint:) 便捷入口与 host 字符串版一致(VM 只有 HostEndpoint 可用)")
|
||||
func endpointConvenienceMatchesHostClassification() throws {
|
||||
// Arrange
|
||||
let vectors: [(url: String, tier: HostNetworkTier)] = [
|
||||
("http://127.0.0.1:3000", .loopback),
|
||||
("http://192.168.1.5:3000", .privateLAN),
|
||||
("http://100.100.1.1:3000", .tailscale),
|
||||
("https://mac.tailnet.ts.net", .tailscale),
|
||||
("https://example.com", .public),
|
||||
]
|
||||
|
||||
// Act + Assert
|
||||
for vector in vectors {
|
||||
let url = try #require(URL(string: vector.url))
|
||||
let endpoint = try #require(HostEndpoint(baseURL: url))
|
||||
#expect(HostClassifier.classify(endpoint: endpoint) == vector.tier, "\(vector.url)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user