feat(backend): DELETE /avatars removes the row and the S3 object
Frontend "Remove avatar" needs a way to clear the current user's picture without uploading a placeholder. Adds the missing route. - S3Storage::delete wraps aws_sdk_s3 DeleteObject. MinIO's delete is idempotent for missing keys, so a stray miss is silent. - repo::clear_avatar returns true when a row existed so the handler can decide whether to also try the S3 delete (skip for users without an avatar). - handlers::delete is authenticated (GatewayIdentity) and always returns 204, matching how the frontend button should behave after a repeat click. S3 delete is best-effort: if MinIO is unreachable we already committed the DB row removal and log a warn — the object becomes orphaned but is not served, because serve() looks up keys via the DB, not by URL. - Route registered on the existing avatar_routes Router. - 3 tests: happy path (post-then-delete → count==0), idempotent no-op, 401 without identity.
This commit is contained in:
parent
d5f17eb0ac
commit
a597425e45
5 changed files with 122 additions and 0 deletions
|
|
@ -121,3 +121,78 @@ async fn upload_without_identity_is_401() {
|
|||
.await
|
||||
.assert_status(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_the_row_and_returns_204() {
|
||||
common::init_test_logging();
|
||||
common::ensure_avatar_bucket().await;
|
||||
let pool = common::test_pool().await;
|
||||
let server = common::test_server(accounts_service::build_app(
|
||||
pool.clone(),
|
||||
&common::test_config(),
|
||||
));
|
||||
let (account_id, email) = common::register_account(&server).await;
|
||||
|
||||
server
|
||||
.post("/avatars")
|
||||
.add_header(ACCOUNT_ID_HEADER, account_id.to_string())
|
||||
.multipart(form(png_bytes()))
|
||||
.await
|
||||
.assert_status_ok();
|
||||
|
||||
server
|
||||
.delete("/avatars")
|
||||
.add_header(ACCOUNT_ID_HEADER, account_id.to_string())
|
||||
.await
|
||||
.assert_status(StatusCode::NO_CONTENT);
|
||||
|
||||
let count: i64 = sqlx::query_as::<_, (i64,)>("SELECT COUNT(*) FROM avatars WHERE account_id = $1")
|
||||
.bind(account_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(count, 0);
|
||||
|
||||
sqlx::query("DELETE FROM accounts WHERE lower(email) = $1")
|
||||
.bind(email.to_lowercase())
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_without_avatar_is_idempotent() {
|
||||
let pool = common::test_pool().await;
|
||||
let server = common::test_server(accounts_service::build_app(
|
||||
pool.clone(),
|
||||
&common::test_config(),
|
||||
));
|
||||
let (account_id, email) = common::register_account(&server).await;
|
||||
|
||||
server
|
||||
.delete("/avatars")
|
||||
.add_header(ACCOUNT_ID_HEADER, account_id.to_string())
|
||||
.await
|
||||
.assert_status(StatusCode::NO_CONTENT);
|
||||
|
||||
sqlx::query("DELETE FROM accounts WHERE lower(email) = $1")
|
||||
.bind(email.to_lowercase())
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_without_identity_is_401() {
|
||||
let pool = common::test_pool().await;
|
||||
let server = common::test_server(accounts_service::build_app(
|
||||
pool.clone(),
|
||||
&common::test_config(),
|
||||
));
|
||||
|
||||
server
|
||||
.delete("/avatars")
|
||||
.await
|
||||
.assert_status(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue