Files
hyungi_document_server/clients/ds-app/Tests/DSKitTests/ClientPlumbingTests.swift
T

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 }
}