Star/Unstar fired the API call but nothing showed it took effect — Document/Collection have no isStarred field, stars.list is the only source of truth, so this adds a shared StarStore (one stars.list fetch, optimistic toggle/rollback on star/unstar) injected at the app root and read by the sidebar tree rows, collection header row, and DocumentRowView (document lists, search results, reader sub-documents). Context menu labels now read "Unstar" once starred, and starred rows show a filled star. OutlineKit: stars.list / stars.delete, with test coverage.
603 lines
21 KiB
Swift
603 lines
21 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 testDeleteCollectionSucceedsOnSuccessTrue() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "success": true }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
try await client.deleteCollection(id: "col-1")
|
|
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/collections.delete")
|
|
}
|
|
|
|
func testDeleteCollectionThrowsOnSuccessFalse() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "success": false }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
do {
|
|
try await client.deleteCollection(id: "col-1")
|
|
XCTFail("Expected an error when success is false")
|
|
} catch {
|
|
// expected
|
|
}
|
|
}
|
|
|
|
func testExportCollectionDecodesFileOperation() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": {
|
|
"fileOperation": { "id": "op-1", "state": "creating" }
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let operation = try await client.exportCollection(ExportCollectionRequest(id: "col-1"))
|
|
|
|
XCTAssertEqual(operation.id, "op-1")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/collections.export")
|
|
}
|
|
|
|
func testStarCollectionDecodesStar() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": {
|
|
"id": "star-1",
|
|
"collectionId": "col-1",
|
|
"documentId": null
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let star = try await client.starCollection(StarCollectionRequest(collectionId: "col-1"))
|
|
|
|
XCTAssertEqual(star.id, "star-1")
|
|
XCTAssertEqual(star.collectionId, "col-1")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/stars.create")
|
|
}
|
|
|
|
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 testListDocumentsSendsParentDocumentIdFilter() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "data": [] }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
_ = try await client.listDocuments(collectionId: nil, parentDocumentId: "doc-1", offset: 0, limit: 100)
|
|
|
|
struct SentBody: Decodable {
|
|
let parentDocumentId: String?
|
|
}
|
|
|
|
let sentBody = try XCTUnwrap(httpClient.lastRequest?.httpBody)
|
|
let decodedBody = try JSONDecoder().decode(SentBody.self, from: sentBody)
|
|
XCTAssertEqual(decodedBody.parentDocumentId, "doc-1")
|
|
}
|
|
|
|
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 testStarDocumentDecodesStar() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": {
|
|
"id": "star-1",
|
|
"collectionId": null,
|
|
"documentId": "doc-1"
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let star = try await client.starDocument(StarDocumentRequest(documentId: "doc-1"))
|
|
|
|
XCTAssertEqual(star.documentId, "doc-1")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/stars.create")
|
|
}
|
|
|
|
func testDuplicateDocumentDecodesDocumentsArray() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": {
|
|
"documents": [
|
|
{
|
|
"id": "doc-2",
|
|
"title": "Hello (copy)",
|
|
"text": "World",
|
|
"url": "/doc/hello-copy-doc-2",
|
|
"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 documents = try await client.duplicateDocument(DuplicateDocumentRequest(id: "doc-1"))
|
|
|
|
XCTAssertEqual(documents.first?.id, "doc-2")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.duplicate")
|
|
}
|
|
|
|
func testMoveDocumentSendsDestination() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "data": { "documents": [], "collections": [] } }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
try await client.moveDocument(MoveDocumentRequest(id: "doc-1", collectionId: "col-2", parentDocumentId: "doc-9"))
|
|
|
|
struct SentBody: Decodable {
|
|
let id: String
|
|
let collectionId: String
|
|
let parentDocumentId: String?
|
|
}
|
|
|
|
let sentBody = try XCTUnwrap(httpClient.lastRequest?.httpBody)
|
|
let decodedBody = try JSONDecoder().decode(SentBody.self, from: sentBody)
|
|
XCTAssertEqual(decodedBody.collectionId, "col-2")
|
|
XCTAssertEqual(decodedBody.parentDocumentId, "doc-9")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.move")
|
|
}
|
|
|
|
func testDeleteDocumentSucceedsOnSuccessTrue() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "success": true }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
try await client.deleteDocument(DeleteDocumentRequest(id: "doc-1"))
|
|
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.delete")
|
|
}
|
|
|
|
func testArchiveDocumentDecodesDocument() 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",
|
|
"archivedAt": "2026-01-03T00: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.archiveDocument(id: "doc-1")
|
|
|
|
XCTAssertNotNil(document.archivedAt)
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.archive")
|
|
}
|
|
|
|
func testExportDocumentReturnsMarkdownString() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "data": "# Hello\\n\\nWorld" }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let markdown = try await client.exportDocument(id: "doc-1")
|
|
|
|
XCTAssertEqual(markdown, "# Hello\n\nWorld")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.export")
|
|
}
|
|
|
|
func testListRevisionsDecodesRevisions() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": [
|
|
{
|
|
"id": "rev-1",
|
|
"documentId": "doc-1",
|
|
"title": "Hello",
|
|
"name": null,
|
|
"icon": null,
|
|
"collaborators": [
|
|
{ "id": "user-1", "name": "Jane Doe" }
|
|
],
|
|
"createdAt": "2026-01-01T00:00:00.000Z"
|
|
}
|
|
]
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let revisions = try await client.listRevisions(ListRevisionsRequest(documentId: "doc-1"))
|
|
|
|
XCTAssertEqual(revisions.first?.id, "rev-1")
|
|
XCTAssertEqual(revisions.first?.collaborators.first?.name, "Jane Doe")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/revisions.list")
|
|
}
|
|
|
|
func testDocumentInsightsDecodesRollups() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": [
|
|
{
|
|
"date": "2026-01-01",
|
|
"period": "day",
|
|
"viewCount": 3,
|
|
"viewerCount": 2,
|
|
"commentCount": 0,
|
|
"reactionCount": 0,
|
|
"revisionCount": 1,
|
|
"editorCount": 1
|
|
}
|
|
]
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let insights = try await client.documentInsights(DocumentInsightsRequest(id: "doc-1"))
|
|
|
|
XCTAssertEqual(insights.first?.viewCount, 3)
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/documents.insights")
|
|
}
|
|
|
|
func testListStarsDecodesStarsArray() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{
|
|
"data": {
|
|
"stars": [
|
|
{ "id": "star-1", "collectionId": "col-1", "documentId": null },
|
|
{ "id": "star-2", "collectionId": null, "documentId": "doc-1" }
|
|
],
|
|
"documents": []
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
let stars = try await client.listStars(ListStarsRequest())
|
|
|
|
XCTAssertEqual(stars.count, 2)
|
|
XCTAssertEqual(stars.first?.collectionId, "col-1")
|
|
XCTAssertEqual(stars.last?.documentId, "doc-1")
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/stars.list")
|
|
}
|
|
|
|
func testDeleteStarSucceedsOnSuccessTrue() async throws {
|
|
let httpClient = MockHTTPClient()
|
|
httpClient.responseData = """
|
|
{ "success": true }
|
|
""".data(using: .utf8)!
|
|
|
|
let client = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!),
|
|
tokenStore: StaticTokenStore(),
|
|
httpClient: httpClient
|
|
)
|
|
|
|
try await client.deleteStar(id: "star-1")
|
|
|
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/stars.delete")
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|