mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-10 19:42:21 +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.
61 lines
2.0 KiB
Swift
61 lines
2.0 KiB
Swift
import Foundation
|
|
import HapiClient
|
|
import Testing
|
|
|
|
@Suite("MultipartFormData")
|
|
struct MultipartEncoderTests {
|
|
@Test func encodesFieldsAndOneFilePart() {
|
|
var form = MultipartFormData(boundary: "B")
|
|
form.appendField(name: "provider", value: "openai")
|
|
form.appendField(name: "mode", value: "standard")
|
|
form.appendFile(
|
|
fieldName: "file",
|
|
filename: "clip.m4a",
|
|
mimeType: "audio/mp4",
|
|
data: Data([0x01, 0x02])
|
|
)
|
|
|
|
var expected = Data(
|
|
(
|
|
"--B\r\n"
|
|
+ "Content-Disposition: form-data; name=\"provider\"\r\n\r\n"
|
|
+ "openai\r\n"
|
|
+ "--B\r\n"
|
|
+ "Content-Disposition: form-data; name=\"mode\"\r\n\r\n"
|
|
+ "standard\r\n"
|
|
+ "--B\r\n"
|
|
+ "Content-Disposition: form-data; name=\"file\"; filename=\"clip.m4a\"\r\n"
|
|
+ "Content-Type: audio/mp4\r\n\r\n"
|
|
).utf8
|
|
)
|
|
expected.append(Data([0x01, 0x02]))
|
|
expected.append(Data("\r\n--B--\r\n".utf8))
|
|
|
|
#expect(form.contentType == "multipart/form-data; boundary=B")
|
|
#expect(form.encodedBody() == expected)
|
|
}
|
|
|
|
@Test func encodingDoesNotConsumeTheBuilder() {
|
|
var form = MultipartFormData(boundary: "B")
|
|
form.appendField(name: "a", value: "1")
|
|
let first = form.encodedBody()
|
|
#expect(form.encodedBody() == first)
|
|
}
|
|
|
|
@Test func sanitizesQuotesAndCRLFInHeaderValues() {
|
|
var form = MultipartFormData(boundary: "B")
|
|
form.appendFile(
|
|
fieldName: "file",
|
|
filename: "we\"ird\r\nname.m4a",
|
|
mimeType: "audio/mp4",
|
|
data: Data()
|
|
)
|
|
let body = String(decoding: form.encodedBody(), as: UTF8.self)
|
|
#expect(body.contains("filename=\"we%22irdname.m4a\""))
|
|
}
|
|
|
|
@Test func generatedBoundariesAreUnique() {
|
|
#expect(MultipartFormData.makeBoundary() != MultipartFormData.makeBoundary())
|
|
}
|
|
}
|