From 052db93d698b5a6c96752dcad673fb991eeff56a Mon Sep 17 00:00:00 2001 From: psavarmattas Date: Sat, 15 Aug 2026 16:23:43 +0100 Subject: [PATCH] feat(profile): name update + attachment cleanup backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdateUserNameRequest (users.update, name only) and deleteAttachment (attachments.delete, id-only — same shape as every other simple delete in this API, not yet confirmed live) so the app can clean up the previous avatar attachment when replacing or removing it instead of leaking an orphaned blob in Outline's storage every time. 2 new tests, 65/65 passing. --- .../Caching/CachingOutlineAPIClient.swift | 8 ++++ .../OutlineKit/Core/OutlineAPIClient.swift | 6 +++ .../OutlineKit/LiveOutlineAPIClient.swift | 8 ++++ .../Requests/UpdateUserNameRequest.swift | 12 ++++++ .../CachingOutlineAPIClientTests.swift | 2 + .../LiveOutlineAPIClientTests.swift | 40 +++++++++++++++++++ 6 files changed, 76 insertions(+) create mode 100644 OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift diff --git a/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift index 9ec88a5..4f59bf3 100644 --- a/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift @@ -343,10 +343,18 @@ public actor CachingOutlineAPIClient: OutlineAPIClient { try await live.uploadAttachmentFile(result, fileData: fileData) } + public func deleteAttachment(id: String) async throws { + try await live.deleteAttachment(id: id) + } + public func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser { try await live.updateUserAvatar(request) } + public func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser { + try await live.updateUserName(request) + } + // MARK: - Sync management (Settings surface) public func pendingOperations() async -> [PendingOperationSummary] { diff --git a/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift index 53307ca..21f18d9 100644 --- a/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift @@ -75,6 +75,12 @@ public protocol OutlineAPIClient: Sendable { /// target. See `OutlineAttachment`/`CreateAttachmentResult`. func createAttachment(_ request: CreateAttachmentRequest) async throws -> CreateAttachmentResult func uploadAttachmentFile(_ result: CreateAttachmentResult, fileData: Data) async throws + /// Best-effort — matches the shape every other simple `id`-only delete + /// in this API uses (`pins.delete`, `stars.delete`, …), not confirmed + /// against a live server specifically for attachments yet. + func deleteAttachment(id: String) async throws /// `users.update`, avatar only. See `UpdateUserAvatarRequest`. func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser + /// `users.update`, name only. See `UpdateUserNameRequest`. + func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser } diff --git a/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift index 579d744..444a11c 100644 --- a/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift @@ -278,10 +278,18 @@ public actor LiveOutlineAPIClient: OutlineAPIClient { } } + public func deleteAttachment(id: String) async throws { + try await postForSuccess("attachments.delete", body: StarIDParams(id: id)) + } + public func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser { try await post("users.update", body: request) } + public func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser { + try await post("users.update", body: request) + } + private func post(_ path: String, body: Body) async throws -> Response { guard let token = try? tokenStore.token() else { throw OutlineAPIError.tokenUnavailable diff --git a/OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift b/OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift new file mode 100644 index 0000000..5857409 --- /dev/null +++ b/OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift @@ -0,0 +1,12 @@ +import Foundation + +/// `users.update`, name only. +public struct UpdateUserNameRequest: Encodable, Sendable { + public let id: String + public let name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } +} diff --git a/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift b/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift index 339efa8..8256a1d 100644 --- a/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift +++ b/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift @@ -91,7 +91,9 @@ private final class StubOutlineAPIClient: OutlineAPIClient, @unchecked Sendable func currentUser() async throws -> OutlineUser { throw NotStubbed() } func createAttachment(_ request: CreateAttachmentRequest) async throws -> CreateAttachmentResult { throw NotStubbed() } func uploadAttachmentFile(_ result: CreateAttachmentResult, fileData: Data) async throws { throw NotStubbed() } + func deleteAttachment(id: String) async throws { throw NotStubbed() } func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser { throw NotStubbed() } + func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser { throw NotStubbed() } } private struct StubTransportError: Error {} diff --git a/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift b/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift index 74a6aad..3627531 100644 --- a/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift +++ b/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift @@ -1051,6 +1051,46 @@ final class LiveOutlineAPIClientTests: XCTestCase { XCTAssertTrue(sentJSON.contains("\"id\":\"user-1\"")) } + func testUpdateUserNameSendsRequestAndDecodesResult() async throws { + let httpClient = MockHTTPClient() + httpClient.responseData = """ + { + "data": { + "id": "user-1", + "name": "New Name" + } + } + """.data(using: .utf8)! + + let client = LiveOutlineAPIClient( + configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!), + tokenStore: StaticTokenStore(), + httpClient: httpClient + ) + + let user = try await client.updateUserName(UpdateUserNameRequest(id: "user-1", name: "New Name")) + + XCTAssertEqual(user.name, "New Name") + XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/users.update") + } + + func testDeleteAttachmentSendsRequest() 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.deleteAttachment(id: "attach-1") + + XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/attachments.delete") + } + func testCreateAttachmentDecodesUploadTargetAndFormFields() async throws { let httpClient = MockHTTPClient() httpClient.responseData = """