import Foundation import Testing import HostRegistry // AccessToken:边界校验 + "零泄漏"表示。 // 契约来源:协调 doc §1.1 —— 字符集 `[A-Za-z0-9._~+/=-]`、长度 16–512 // (与服务端 src/config.ts:178 的 WEBTERM_TOKEN_RE 逐字一致);令牌是密级材料, // 绝不进日志 / URL / 崩溃报告(§5.3)。 // MARK: - 边界校验(字符集) @Test("合法令牌:覆盖全字符集 → 构造成功且 rawValue 逐字保真") func validTokenPreservesRawValueVerbatim() throws { // Arrange let raw = Fixtures.validTokenString // Act let token = try AccessToken(validating: raw) // Assert #expect(token.rawValue == raw) } @Test("非法字符 → .invalidCharacters", arguments: [ "abcdefghijklmno p", // 空格 "abcdefghijklmnop!", // 感叹号 "abcdefghijklmnop%", // 百分号(需 URL 编码 → 不在 cookie-safe 集里) "abcdefghijklmnop,", // 逗号(cookie 分隔符) "abcdefghijklmnop;", // 分号(cookie 分隔符) "abcdefghijklmnop令", // 非 ASCII ]) func invalidCharactersAreRejectedWithTypedError(raw: String) { // Act / Assert #expect(throws: AccessTokenError.invalidCharacters) { _ = try AccessToken(validating: raw) } } @Test("非法字符错误不携带令牌内容(错误对象本身不得成为泄漏面)") func invalidCharactersErrorCarriesNoTokenPayload() { // Arrange let secretish = "supersecret-tail!!!!!!" // Act var captured = "" do { _ = try AccessToken(validating: secretish) Issue.record("期望抛错,实际构造成功") } catch { captured = String(describing: error) + String(reflecting: error) } // Assert #expect(captured.isEmpty == false) #expect(captured.contains("supersecret") == false) } // MARK: - 边界校验(长度) @Test("长度下限:15 → .tooShort(15)") func tokenBelowMinimumLengthIsRejected() { // Act / Assert #expect(throws: AccessTokenError.tooShort(length: 15)) { _ = try AccessToken(validating: Fixtures.tokenString(length: 15)) } } @Test("长度下限:恰好 16 → 通过") func tokenAtMinimumLengthIsAccepted() throws { // Act let token = try AccessToken(validating: Fixtures.tokenString(length: AccessToken.minLength)) // Assert #expect(token.rawValue.count == 16) } @Test("长度上限:恰好 512 → 通过") func tokenAtMaximumLengthIsAccepted() throws { // Act let token = try AccessToken(validating: Fixtures.tokenString(length: AccessToken.maxLength)) // Assert #expect(token.rawValue.count == 512) } @Test("长度上限:513 → .tooLong(513)") func tokenAboveMaximumLengthIsRejected() { // Act / Assert #expect(throws: AccessTokenError.tooLong(length: 513)) { _ = try AccessToken(validating: Fixtures.tokenString(length: 513)) } } @Test("空串 → .tooShort(0)(不是 invalidCharacters:先判长度更贴近用户话术)") func emptyTokenIsRejectedAsTooShort() { // Act / Assert #expect(throws: AccessTokenError.tooShort(length: 0)) { _ = try AccessToken(validating: "") } } // MARK: - 粘贴 UX:首尾空白裁剪 @Test("首尾空白/换行被裁剪后再校验:合法令牌不受影响(空白永非合法令牌字符)") func surroundingWhitespaceIsTrimmedBeforeValidation() throws { // Arrange let pasted = " \n\t" + Fixtures.validTokenString + " \n" // Act let token = try AccessToken(validating: pasted) // Assert #expect(token.rawValue == Fixtures.validTokenString) } @Test("中间空白不被裁剪:仍是 .invalidCharacters(不做静默规范化)") func interiorWhitespaceIsStillInvalid() { // Act / Assert #expect(throws: AccessTokenError.invalidCharacters) { _ = try AccessToken(validating: "abcdefgh ijklmnop") } } @Test("init?(rawValue:) 宽松入口:非法 → nil,不抛") func lenientRawValueInitReturnsNilForInvalidToken() { // Act / Assert #expect(AccessToken(rawValue: "too-short") == nil) #expect(AccessToken(rawValue: Fixtures.validTokenString)?.rawValue == Fixtures.validTokenString) } // MARK: - §5.3 零泄漏表示 @Test("§5.3 description/debugDescription 不含令牌,只给长度") func stringRepresentationsRedactTheToken() { // Arrange let token = Fixtures.makeToken() // Act let description = token.description let debugDescription = token.debugDescription // Assert #expect(description.contains(Fixtures.validTokenString) == false) #expect(debugDescription.contains(Fixtures.validTokenString) == false) #expect(description.contains("redacted")) #expect(description.contains("\(Fixtures.validTokenString.count)")) } @Test("§5.3 String(describing:)/String(reflecting:) 也不泄漏(插值即日志的默认路径)") func interpolationPathsRedactTheToken() { // Arrange let token = Fixtures.makeToken() // Act let interpolated = "token=\(token)" // Assert #expect(interpolated.contains(Fixtures.validTokenString) == false) #expect(String(describing: token).contains(Fixtures.validTokenString) == false) #expect(String(reflecting: token).contains(Fixtures.validTokenString) == false) } @Test("§5.3 Mirror/dump 反射不泄漏令牌(崩溃报告与 dump 是常见旁路)") func reflectionDoesNotExposeTheToken() { // Arrange let token = Fixtures.makeToken() // Act let children = Mirror(reflecting: token).children.map { String(describing: $0.value) } var dumped = "" dump(token, to: &dumped) // Assert #expect(children.contains(where: { $0.contains(Fixtures.validTokenString) }) == false) #expect(dumped.contains(Fixtures.validTokenString) == false) } @Test("§5.3 Host 的字符串表示不含令牌,只标注是否已配置") func hostStringRepresentationRedactsTheToken() { // Arrange let host = Fixtures.makeHost().withAccessToken(Fixtures.makeToken()) // Act let described = String(describing: host) let reflected = String(reflecting: host) var dumped = "" dump(host, to: &dumped) // Assert #expect(described.contains(Fixtures.validTokenString) == false) #expect(reflected.contains(Fixtures.validTokenString) == false) #expect(dumped.contains(Fixtures.validTokenString) == false) #expect(described.contains("hasAccessToken: true")) #expect(String(describing: Fixtures.makeHost()).contains("hasAccessToken: false")) } // MARK: - 值语义 @Test("Equatable/Hashable:同值相等且同 hash,异值不等") func tokenValueSemanticsAreByRawValue() { // Arrange let a = Fixtures.makeToken() let b = Fixtures.makeToken() let other = Fixtures.makeToken(Fixtures.otherValidTokenString) // Assert #expect(a == b) #expect(a.hashValue == b.hashValue) #expect(a != other) }