f512d94c74
git-subtree-dir: clients/ds-app git-subtree-mainline:a24e3e6f22git-subtree-split:5206cf3b0c
83 lines
3.7 KiB
Swift
83 lines
3.7 KiB
Swift
import XCTest
|
|
@testable import DSKit
|
|
|
|
/// Offline tests for the live-client plumbing (the live HTTP path itself needs a real backend, FU-A).
|
|
final class ClientPlumbingTests: XCTestCase {
|
|
|
|
func testEndpointPathsAndMethods() {
|
|
XCTAssertEqual(DSEndpoint.documents(DocumentListQuery()).path, "documents/")
|
|
XCTAssertEqual(DSEndpoint.documents(DocumentListQuery()).method, "GET")
|
|
XCTAssertEqual(DSEndpoint.document(4912).path, "documents/4912")
|
|
XCTAssertEqual(DSEndpoint.documentTree.path, "documents/tree")
|
|
XCTAssertEqual(DSEndpoint.ask("q", nil, nil, nil).path, "search/ask")
|
|
XCTAssertEqual(DSEndpoint.pinMemo(5, true).method, "PATCH")
|
|
XCTAssertEqual(DSEndpoint.pinMemo(5, true).path, "memos/5/pin")
|
|
XCTAssertEqual(DSEndpoint.toggleMemoTask(5, 2, true).path, "memos/5/tasks/2")
|
|
XCTAssertEqual(DSEndpoint.putContent(7, "x").method, "PUT")
|
|
XCTAssertEqual(DSEndpoint.deleteMemo(9).method, "DELETE")
|
|
}
|
|
|
|
func testEndpointBearerRule() {
|
|
XCTAssertFalse(DSEndpoint.login("u", "p", nil).requiresBearer)
|
|
XCTAssertFalse(DSEndpoint.refresh.requiresBearer)
|
|
XCTAssertTrue(DSEndpoint.me.requiresBearer)
|
|
XCTAssertTrue(DSEndpoint.documents(DocumentListQuery()).requiresBearer)
|
|
}
|
|
|
|
func testEndpointQueryItems() {
|
|
let items = DSEndpoint.ask("충격시험", 5, "gemma-macmini", nil).queryItems
|
|
let dict = Dictionary(uniqueKeysWithValues: items.map { ($0.name, $0.value) })
|
|
XCTAssertEqual(dict["q"], "충격시험")
|
|
XCTAssertEqual(dict["backend"], "gemma-macmini")
|
|
XCTAssertEqual(dict["limit"], "5")
|
|
XCTAssertNil(dict["debug"]) // nil params are skipped, not sent as "nil"
|
|
}
|
|
|
|
func testEndpointBodies() throws {
|
|
let encoder = DSEncoder.make()
|
|
let pinBody = try DSEndpoint.pinMemo(5, true).httpBody(encoder)
|
|
XCTAssertEqual(String(data: pinBody ?? Data(), encoding: .utf8), #"{"pinned":true}"#)
|
|
let loginBody = try XCTUnwrap(DSEndpoint.login("hyungi", "pw", nil).httpBody(encoder))
|
|
let s = String(data: loginBody, encoding: .utf8) ?? ""
|
|
XCTAssertTrue(s.contains("\"username\":\"hyungi\""))
|
|
XCTAssertFalse(s.contains("totp_code")) // nil optional omitted
|
|
XCTAssertNil(try DSEndpoint.me.httpBody(encoder)) // GET has no body
|
|
}
|
|
|
|
/// Single-flight refresh: N concurrent refreshOnce() calls must fire the refresh closure exactly once.
|
|
func testSingleFlightRefresh() async throws {
|
|
let counter = CallCounter()
|
|
let provider = TokenProvider(persistence: InMemoryTokenStore()) {
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
let n = await counter.inc()
|
|
return "token-\(n)"
|
|
}
|
|
let results = await withTaskGroup(of: String.self) { group -> [String] in
|
|
for _ in 0..<8 { group.addTask { (try? await provider.refreshOnce()) ?? "ERR" } }
|
|
var collected: [String] = []
|
|
for await value in group { collected.append(value) }
|
|
return collected
|
|
}
|
|
let calls = await counter.value
|
|
XCTAssertEqual(calls, 1, "refresh closure must run exactly once under concurrency")
|
|
XCTAssertTrue(results.allSatisfy { $0 == "token-1" })
|
|
}
|
|
|
|
func testTokenProviderCacheAndPersistence() async {
|
|
let store = InMemoryTokenStore()
|
|
let provider = TokenProvider(persistence: store) { "x" }
|
|
await provider.set("abc")
|
|
let current = await provider.current()
|
|
XCTAssertEqual(current, "abc")
|
|
XCTAssertEqual(store.read(), "abc")
|
|
await provider.clear()
|
|
let cleared = await provider.current()
|
|
XCTAssertNil(cleared)
|
|
}
|
|
}
|
|
|
|
actor CallCounter {
|
|
var value = 0
|
|
func inc() -> Int { value += 1; return value }
|
|
}
|