From 013df89b4fdbed50bca7f248c213d954b9b84508 Mon Sep 17 00:00:00 2001 From: psavarmattas Date: Fri, 14 Aug 2026 17:55:07 +0100 Subject: [PATCH] fix(shares): shares.info wraps the share in {shares: [...]}, not bare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Empty-body handling fixed "no share yet" but re-opening the share sheet for a document that already had one still errored — confirmed via a raw response capture that data is { shares: [...] }, a one-element array, not the bare share object the docs show. Same pattern as pins.list. Added SharesInfoPayload (mirrors PinsListPayload), shareInfo now takes .shares.first. createShare/updateShare are unaffected — the user's earlier successful create-and-copy confirms those aren't wrapped this way, only shares.info. Test rewritten with the exact captured payload. --- .../OutlineKit/LiveOutlineAPIClient.swift | 12 ++- .../LiveOutlineAPIClientTests.swift | 96 ++++++++++--------- 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift index 39d29be..a7f3029 100644 --- a/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift @@ -121,7 +121,13 @@ public actor LiveOutlineAPIClient: OutlineAPIClient { } public func shareInfo(documentId: String) async throws -> OutlineShare? { - try await postOptional("shares.info", body: ShareInfoRequest(documentId: documentId)) + // Real shape confirmed against a live server: `data` is + // `{ shares: [...] }`, not the bare share object the docs imply — + // same pattern as `pins.list`. A document could in principle have + // more than one share record; the first is what the reader's + // share sheet cares about. + let payload: SharesInfoPayload? = try await postOptional("shares.info", body: ShareInfoRequest(documentId: documentId)) + return payload?.shares.first } public func updateShare(_ request: UpdateShareRequest) async throws -> OutlineShare { @@ -354,6 +360,10 @@ private struct PinsListPayload: Decodable { let documents: [OutlineDocument] } +private struct SharesInfoPayload: Decodable { + let shares: [OutlineShare] +} + private struct ListStarsResponse: Decodable { let stars: [OutlineStar] } diff --git a/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift b/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift index eae2b40..998300d 100644 --- a/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift +++ b/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift @@ -669,54 +669,59 @@ final class LiveOutlineAPIClientTests: XCTestCase { XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/shares.create") } - func testShareInfoDecodesFullDocumentedShape() async throws { - // Matches the exact response shape from Outline's official - // shares.info docs, including every field the hosted app can send — - // the model must tolerate all of this without throwing. + func testShareInfoDecodesRealShareArrayShape() async throws { + // Real shape confirmed against a live server — `data` is + // `{ shares: [...] }`, not the bare share object the official docs + // imply (same pattern as `pins.list`). This is the exact payload + // captured for an existing, unpublished share. let httpClient = MockHTTPClient() httpClient.responseData = """ { "data": { - "id": "123e4567-e89b-12d3-a456-426614174000", - "documentTitle": "React best practices", - "documentUrl": "https://example.com", - "sourceTitle": "string", - "sourcePath": "string", - "documentId": null, - "collectionId": null, - "urlId": null, - "url": "https://example.com", - "domain": null, - "title": null, - "iconUrl": null, - "published": false, - "includeChildDocuments": true, - "allowSubscriptions": true, - "allowIndexing": true, - "showLastUpdated": true, - "showTOC": true, - "views": 1, - "createdAt": "2026-08-12T17:41:28.333Z", - "createdBy": { - "id": "123e4567-e89b-12d3-a456-426614174000", - "name": "Jane Doe", - "avatarUrl": "https://example.com", - "color": "string", - "email": "hello@example.com", - "role": "admin", - "isSuspended": true, - "lastActiveAt": null, - "timezone": null, - "createdAt": "2026-08-12T17:41:28.333Z", - "updatedAt": "2026-08-12T17:41:28.333Z", - "deletedAt": null - }, - "updatedAt": "2026-08-12T17:41:28.333Z", - "lastAccessedAt": null + "shares": [ + { + "id": "861559ac-906b-4dec-9c1f-3a2d2000745d", + "sourceTitle": "Test Document 3", + "sourcePath": "/doc/test-document-3-VHxABl5RaD", + "collectionId": null, + "documentId": "b1196971-4239-4e35-970f-7fbbc60af044", + "documentTitle": "Test Document 3", + "documentUrl": "/doc/test-document-3-VHxABl5RaD", + "published": false, + "url": "https://docs.psmattas.com/s/861559ac-906b-4dec-9c1f-3a2d2000745d", + "urlId": null, + "createdBy": { + "id": "1e2ef39c-aa82-475b-b5af-d76bb4f023ed", + "name": "Puranjay Savar Mattas", + "avatarUrl": "/api/files.get?key=public/avatar.png", + "color": "#1c9152", + "role": "admin", + "isSuspended": false, + "createdAt": "2025-07-30T17:36:58.560Z", + "updatedAt": "2026-08-14T16:47:45.037Z", + "deletedAt": null, + "lastActiveAt": "2026-08-14T16:47:45.037Z", + "timezone": "Europe/Dublin" + }, + "includeChildDocuments": false, + "allowIndexing": false, + "allowSubscriptions": true, + "showLastUpdated": false, + "showTOC": false, + "title": null, + "iconUrl": null, + "views": 0, + "domain": null, + "createdAt": "2026-08-14T16:49:40.146Z", + "updatedAt": "2026-08-14T16:49:40.146Z" + } + ] }, "policies": [ - { "id": "123e4567-e89b-12d3-a456-426614174000", "abilities": { "read": true, "update": true, "delete": false } } - ] + { "id": "861559ac-906b-4dec-9c1f-3a2d2000745d", "abilities": { "read": true, "update": false, "revoke": true } } + ], + "status": 200, + "ok": true } """.data(using: .utf8)! @@ -728,10 +733,11 @@ final class LiveOutlineAPIClientTests: XCTestCase { let share = try await client.shareInfo(documentId: "doc-1") + XCTAssertEqual(share?.id, "861559ac-906b-4dec-9c1f-3a2d2000745d") XCTAssertEqual(share?.published, false) - XCTAssertEqual(share?.views, 1) - XCTAssertEqual(share?.createdBy?.name, "Jane Doe") - XCTAssertNil(share?.documentId) + XCTAssertEqual(share?.views, 0) + XCTAssertEqual(share?.createdBy?.name, "Puranjay Savar Mattas") + XCTAssertEqual(share?.documentId, "b1196971-4239-4e35-970f-7fbbc60af044") } func testListSharesDecodesSharesArray() async throws {