mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-11 19:50:05 +00:00
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.
91 lines
3.8 KiB
Swift
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)
|
|
}
|
|
}
|