Files
Outpost/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift
T
Puranjay Savar Mattas 07fb08cb6b feat(outlinekit): add REST client package for Outline API
Local SPM package wrapping Outline's RPC-style REST API — auth,
documents, collections, and search — behind a protocol boundary
(OutlineAPIClient) so the app never depends on the transport directly.
2026-08-14 00:23:12 +01:00

242 lines
8.5 KiB
Swift

import XCTest
@testable import OutlineKit
private final class MockHTTPClient: HTTPClient, @unchecked Sendable {
var lastRequest: URLRequest?
var responseData: Data = Data()
var statusCode: Int = 200
func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
lastRequest = request
let response = HTTPURLResponse(
url: request.url!,
statusCode: statusCode,
httpVersion: nil,
headerFields: nil
)!
return (responseData, response)
}
}
private final class StaticTokenStore: TokenStoring, @unchecked Sendable {
private var stored: String?
init(token: String? = "test-token") {
self.stored = token
}
func token() throws -> String {
guard let stored else { throw TokenStoreError.notFound }
return stored
}
func store(_ token: String) throws { stored = token }
func clear() throws { stored = nil }
}
final class LiveOutlineAPIClientTests: XCTestCase {
func testSearchDocumentsDecodesContextAndRanking() async throws {
let httpClient = MockHTTPClient()
httpClient.responseData = """
{
"data": [
{
"context": "At Acme Inc our hiring practices are inclusive",
"ranking": 1.1844109,
"document": {
"id": "doc-1",
"title": "Welcome to Acme Inc",
"text": "",
"url": "/doc/welcome-doc-1",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z"
}
}
]
}
""".data(using: .utf8)!
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(),
httpClient: httpClient
)
let results = try await client.searchDocuments(DocumentSearchRequest(query: "hiring"))
XCTAssertEqual(results.count, 1)
XCTAssertEqual(results[0].context, "At Acme Inc our hiring practices are inclusive")
XCTAssertEqual(results[0].document.title, "Welcome to Acme Inc")
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.search")
}
func testSearchDocumentTitlesReturnsDocumentsDirectly() async throws {
let httpClient = MockHTTPClient()
httpClient.responseData = """
{
"data": [
{
"id": "doc-1",
"title": "Welcome to Acme Inc",
"text": "",
"url": "/doc/welcome-doc-1",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z"
}
]
}
""".data(using: .utf8)!
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(),
httpClient: httpClient
)
let results = try await client.searchDocumentTitles(DocumentSearchTitlesRequest(query: "Welcome"))
XCTAssertEqual(results.count, 1)
XCTAssertEqual(results[0].title, "Welcome to Acme Inc")
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.search_titles")
}
func testAuthInfoDecodesUserAndTeam() async throws {
let httpClient = MockHTTPClient()
httpClient.responseData = """
{
"data": {
"user": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Jane Doe",
"email": "hello@example.com",
"role": "admin"
},
"team": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Inc",
"url": "https://acme-inc.getoutline.com",
"subdomain": "acme-inc"
}
}
}
""".data(using: .utf8)!
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(),
httpClient: httpClient
)
let auth = try await client.authInfo()
XCTAssertEqual(auth.user.name, "Jane Doe")
XCTAssertEqual(auth.team.name, "Acme Inc")
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/auth.info")
XCTAssertEqual(httpClient.lastRequest?.value(forHTTPHeaderField: "Authorization"), "Bearer test-token")
}
func testUpdateCollectionSendsDescriptionAndDecodesResult() async throws {
let httpClient = MockHTTPClient()
httpClient.responseData = """
{
"data": {
"id": "col-1",
"name": "Engineering",
"description": "Updated overview text.",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z"
}
}
""".data(using: .utf8)!
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(),
httpClient: httpClient
)
let collection = try await client.updateCollection(
UpdateCollectionRequest(id: "col-1", description: "Updated overview text.")
)
XCTAssertEqual(collection.description, "Updated overview text.")
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/collections.update")
struct SentBody: Decodable {
let id: String
let description: String?
}
let sentBody = try XCTUnwrap(httpClient.lastRequest?.httpBody)
let decodedBody = try JSONDecoder().decode(SentBody.self, from: sentBody)
XCTAssertEqual(decodedBody.id, "col-1")
XCTAssertEqual(decodedBody.description, "Updated overview text.")
}
func testDocumentInfoDecodesEnvelopeAndSetsAuthHeader() async throws {
let httpClient = MockHTTPClient()
httpClient.responseData = """
{
"data": {
"id": "doc-1",
"title": "Hello",
"text": "World",
"url": "/doc/hello-doc-1",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z"
}
}
""".data(using: .utf8)!
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(),
httpClient: httpClient
)
let document = try await client.documentInfo(id: "doc-1")
XCTAssertEqual(document.id, "doc-1")
XCTAssertEqual(document.title, "Hello")
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.info")
XCTAssertEqual(httpClient.lastRequest?.value(forHTTPHeaderField: "Authorization"), "Bearer test-token")
}
func testUnauthorizedStatusMapsToUnauthorizedError() async throws {
let httpClient = MockHTTPClient()
httpClient.statusCode = 401
httpClient.responseData = """
{ "ok": false, "error": "authentication_required", "message": "Authentication required" }
""".data(using: .utf8)!
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(),
httpClient: httpClient
)
do {
_ = try await client.documentInfo(id: "doc-1")
XCTFail("Expected unauthorized error")
} catch OutlineAPIError.unauthorized {
// expected
}
}
func testMissingTokenThrowsTokenUnavailable() async throws {
let httpClient = MockHTTPClient()
let client = LiveOutlineAPIClient(
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
tokenStore: StaticTokenStore(token: nil),
httpClient: httpClient
)
do {
_ = try await client.documentInfo(id: "doc-1")
XCTFail("Expected tokenUnavailable error")
} catch OutlineAPIError.tokenUnavailable {
// expected
}
}
}