fix(shares): shares.info wraps the share in {shares: [...]}, not bare
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.
This commit is contained in:
@@ -121,7 +121,13 @@ public actor LiveOutlineAPIClient: OutlineAPIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func shareInfo(documentId: String) async throws -> OutlineShare? {
|
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 {
|
public func updateShare(_ request: UpdateShareRequest) async throws -> OutlineShare {
|
||||||
@@ -354,6 +360,10 @@ private struct PinsListPayload: Decodable {
|
|||||||
let documents: [OutlineDocument]
|
let documents: [OutlineDocument]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct SharesInfoPayload: Decodable {
|
||||||
|
let shares: [OutlineShare]
|
||||||
|
}
|
||||||
|
|
||||||
private struct ListStarsResponse: Decodable {
|
private struct ListStarsResponse: Decodable {
|
||||||
let stars: [OutlineStar]
|
let stars: [OutlineStar]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -669,54 +669,59 @@ final class LiveOutlineAPIClientTests: XCTestCase {
|
|||||||
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/shares.create")
|
XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/shares.create")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testShareInfoDecodesFullDocumentedShape() async throws {
|
func testShareInfoDecodesRealShareArrayShape() async throws {
|
||||||
// Matches the exact response shape from Outline's official
|
// Real shape confirmed against a live server — `data` is
|
||||||
// shares.info docs, including every field the hosted app can send —
|
// `{ shares: [...] }`, not the bare share object the official docs
|
||||||
// the model must tolerate all of this without throwing.
|
// imply (same pattern as `pins.list`). This is the exact payload
|
||||||
|
// captured for an existing, unpublished share.
|
||||||
let httpClient = MockHTTPClient()
|
let httpClient = MockHTTPClient()
|
||||||
httpClient.responseData = """
|
httpClient.responseData = """
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
"shares": [
|
||||||
"documentTitle": "React best practices",
|
{
|
||||||
"documentUrl": "https://example.com",
|
"id": "861559ac-906b-4dec-9c1f-3a2d2000745d",
|
||||||
"sourceTitle": "string",
|
"sourceTitle": "Test Document 3",
|
||||||
"sourcePath": "string",
|
"sourcePath": "/doc/test-document-3-VHxABl5RaD",
|
||||||
"documentId": null,
|
"collectionId": null,
|
||||||
"collectionId": null,
|
"documentId": "b1196971-4239-4e35-970f-7fbbc60af044",
|
||||||
"urlId": null,
|
"documentTitle": "Test Document 3",
|
||||||
"url": "https://example.com",
|
"documentUrl": "/doc/test-document-3-VHxABl5RaD",
|
||||||
"domain": null,
|
"published": false,
|
||||||
"title": null,
|
"url": "https://docs.psmattas.com/s/861559ac-906b-4dec-9c1f-3a2d2000745d",
|
||||||
"iconUrl": null,
|
"urlId": null,
|
||||||
"published": false,
|
"createdBy": {
|
||||||
"includeChildDocuments": true,
|
"id": "1e2ef39c-aa82-475b-b5af-d76bb4f023ed",
|
||||||
"allowSubscriptions": true,
|
"name": "Puranjay Savar Mattas",
|
||||||
"allowIndexing": true,
|
"avatarUrl": "/api/files.get?key=public/avatar.png",
|
||||||
"showLastUpdated": true,
|
"color": "#1c9152",
|
||||||
"showTOC": true,
|
"role": "admin",
|
||||||
"views": 1,
|
"isSuspended": false,
|
||||||
"createdAt": "2026-08-12T17:41:28.333Z",
|
"createdAt": "2025-07-30T17:36:58.560Z",
|
||||||
"createdBy": {
|
"updatedAt": "2026-08-14T16:47:45.037Z",
|
||||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
"deletedAt": null,
|
||||||
"name": "Jane Doe",
|
"lastActiveAt": "2026-08-14T16:47:45.037Z",
|
||||||
"avatarUrl": "https://example.com",
|
"timezone": "Europe/Dublin"
|
||||||
"color": "string",
|
},
|
||||||
"email": "hello@example.com",
|
"includeChildDocuments": false,
|
||||||
"role": "admin",
|
"allowIndexing": false,
|
||||||
"isSuspended": true,
|
"allowSubscriptions": true,
|
||||||
"lastActiveAt": null,
|
"showLastUpdated": false,
|
||||||
"timezone": null,
|
"showTOC": false,
|
||||||
"createdAt": "2026-08-12T17:41:28.333Z",
|
"title": null,
|
||||||
"updatedAt": "2026-08-12T17:41:28.333Z",
|
"iconUrl": null,
|
||||||
"deletedAt": null
|
"views": 0,
|
||||||
},
|
"domain": null,
|
||||||
"updatedAt": "2026-08-12T17:41:28.333Z",
|
"createdAt": "2026-08-14T16:49:40.146Z",
|
||||||
"lastAccessedAt": null
|
"updatedAt": "2026-08-14T16:49:40.146Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"policies": [
|
"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)!
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
@@ -728,10 +733,11 @@ final class LiveOutlineAPIClientTests: XCTestCase {
|
|||||||
|
|
||||||
let share = try await client.shareInfo(documentId: "doc-1")
|
let share = try await client.shareInfo(documentId: "doc-1")
|
||||||
|
|
||||||
|
XCTAssertEqual(share?.id, "861559ac-906b-4dec-9c1f-3a2d2000745d")
|
||||||
XCTAssertEqual(share?.published, false)
|
XCTAssertEqual(share?.published, false)
|
||||||
XCTAssertEqual(share?.views, 1)
|
XCTAssertEqual(share?.views, 0)
|
||||||
XCTAssertEqual(share?.createdBy?.name, "Jane Doe")
|
XCTAssertEqual(share?.createdBy?.name, "Puranjay Savar Mattas")
|
||||||
XCTAssertNil(share?.documentId)
|
XCTAssertEqual(share?.documentId, "b1196971-4239-4e35-970f-7fbbc60af044")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testListSharesDecodesSharesArray() async throws {
|
func testListSharesDecodesSharesArray() async throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user