Files
hapi/ios/Packages/HapiKit/Tests/HapiClientTests/HubRegistryTests.swift
T
weishu 1f0ec6184d feat(ios): HapiClient API transport + auth (A-M1b)
APIClient (final class, Sendable) with typed endpoints for the M2/M3 REST
surface behind an HTTPPerforming seam; actor AuthManager with single-flight
JWT refresh (POST /api/auth), proactive refresh 10 min before exp, terminal
authFailed state, and 401 -> refresh -> retry-once wiring per
docs/api/client-contract/auth.md; Keychain credential store (per-hub records
under run.hapi.companion) with CredentialStoring seam + in-memory double;
HubRegistry (ordered hubs + active hub in injectable UserDefaults, origin
normalization); payload-only JWT decoding; APIError {status, code, body}
parsing per errors.md; minimal multipart builder for M4c dictation; 256 MB
URLCache session for generated images.

Request/response wire models (SendMessageRequest, PermissionApproveRequest,
SpawnRequest/SpawnResponse, MessagesQuery, envelopes, RPC-wrapped shapes)
join HapiProtocol/Models mirroring shared/src/apiTypes.ts.

swift-testing coverage: JWT decode (padding/garbage/hostile exp), auth
single-flight (8 concurrent callers -> one exchange), 401 retry-once and
terminal paths, APIError body parsing, endpoint request construction
byte-checks (cursor queries, deliveryMode body, answers formats, explicit
null model reset) via a recording performer, hub URL normalization,
multipart bytes.
2026-08-17 15:20:03 +08:00

91 lines
3.8 KiB
Swift

import Foundation
import HapiClient
import Testing
@Suite("Hub URL normalization")
struct HubURLNormalizationTests {
@Test func normalizesToLowercasedOrigin() {
#expect(HubURLNormalization.normalize(" https://Hub.Example.com/ ") == "https://hub.example.com")
#expect(HubURLNormalization.normalize("https://hub.example.com/some/path?q=1#f") == "https://hub.example.com")
#expect(HubURLNormalization.normalize("HTTP://hub.example.com") == "http://hub.example.com")
}
@Test func dropsDefaultPortsKeepsCustomOnes() {
#expect(HubURLNormalization.normalize("https://hub.example.com:443/") == "https://hub.example.com")
#expect(HubURLNormalization.normalize("http://hub.example.com:80") == "http://hub.example.com")
#expect(HubURLNormalization.normalize("http://192.168.1.5:8005/") == "http://192.168.1.5:8005")
}
@Test func keepsIPv6Brackets() {
#expect(HubURLNormalization.normalize("http://[::1]:8005/") == "http://[::1]:8005")
}
@Test(arguments: ["", " ", "hub.example.com", "ftp://hub.example.com", "hapicompanion://bind"])
func rejectsNonHTTPInputs(raw: String) {
#expect(HubURLNormalization.normalize(raw) == nil)
}
}
@Suite("HubRegistry")
struct HubRegistryTests {
private func makeDefaults() throws -> (UserDefaults, String) {
let suiteName = "HubRegistryTests-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suiteName))
return (defaults, suiteName)
}
@Test func registersInOrderAndActivatesFirst() throws {
let (defaults, suiteName) = try makeDefaults()
defer { defaults.removePersistentDomain(forName: suiteName) }
let registry = HubRegistry(defaults: defaults)
#expect(registry.register("https://one.test/") == "https://one.test")
#expect(registry.register("https://two.test") == "https://two.test")
// Re-registering (differently formatted) is a no-op.
#expect(registry.register("https://ONE.test") == "https://one.test")
#expect(registry.hubs == ["https://one.test", "https://two.test"])
#expect(registry.activeHub == "https://one.test")
}
@Test func invalidURLIsRejected() throws {
let (defaults, suiteName) = try makeDefaults()
defer { defaults.removePersistentDomain(forName: suiteName) }
let registry = HubRegistry(defaults: defaults)
#expect(registry.register("not a url") == nil)
#expect(registry.hubs.isEmpty)
#expect(registry.activeHub == nil)
}
@Test func switchesAndValidatesActiveHub() throws {
let (defaults, suiteName) = try makeDefaults()
defer { defaults.removePersistentDomain(forName: suiteName) }
let registry = HubRegistry(defaults: defaults)
registry.register("https://one.test")
registry.register("https://two.test")
#expect(registry.setActiveHub("https://two.test/"))
#expect(registry.activeHub == "https://two.test")
// Unregistered hubs cannot become active.
#expect(registry.setActiveHub("https://three.test") == false)
#expect(registry.activeHub == "https://two.test")
}
@Test func removalFallsBackToFirstRemainingHub() throws {
let (defaults, suiteName) = try makeDefaults()
defer { defaults.removePersistentDomain(forName: suiteName) }
let registry = HubRegistry(defaults: defaults)
registry.register("https://one.test")
registry.register("https://two.test")
registry.setActiveHub("https://two.test")
registry.remove("https://two.test")
#expect(registry.hubs == ["https://one.test"])
#expect(registry.activeHub == "https://one.test")
registry.remove("https://one.test")
#expect(registry.hubs.isEmpty)
#expect(registry.activeHub == nil)
}
}