feat(profile): name update + attachment cleanup backend
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.
This commit is contained in:
@@ -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] {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Body: Encodable, Response: Decodable>(_ path: String, body: Body) async throws -> Response {
|
||||
guard let token = try? tokenStore.token() else {
|
||||
throw OutlineAPIError.tokenUnavailable
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
Reference in New Issue
Block a user