mas_handlers/admin/v1/user_emails/
delete.rs1use aide::{NoApi, OperationIo, transform::TransformOperation};
7use axum::{Json, response::IntoResponse};
8use hyper::StatusCode;
9use mas_storage::{
10 BoxRng,
11 queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
12};
13use ulid::Ulid;
14
15use crate::{
16 admin::{call_context::CallContext, params::UlidPathParam, response::ErrorResponse},
17 impl_from_error_for_route,
18};
19
20#[derive(Debug, thiserror::Error, OperationIo)]
21#[aide(output_with = "Json<ErrorResponse>")]
22pub enum RouteError {
23 #[error(transparent)]
24 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
25
26 #[error("User email ID {0} not found")]
27 NotFound(Ulid),
28}
29
30impl_from_error_for_route!(mas_storage::RepositoryError);
31
32impl IntoResponse for RouteError {
33 fn into_response(self) -> axum::response::Response {
34 let error = ErrorResponse::from_error(&self);
35 let status = match self {
36 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
37 Self::NotFound(_) => StatusCode::NOT_FOUND,
38 };
39 (status, Json(error)).into_response()
40 }
41}
42
43pub fn doc(operation: TransformOperation) -> TransformOperation {
44 operation
45 .id("deleteUserEmail")
46 .summary("Delete a user email")
47 .tag("user-email")
48 .response_with::<204, (), _>(|t| t.description("User email was found"))
49 .response_with::<404, RouteError, _>(|t| {
50 let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
51 t.description("User email was not found").example(response)
52 })
53}
54
55#[tracing::instrument(name = "handler.admin.v1.user_emails.delete", skip_all, err)]
56pub async fn handler(
57 CallContext {
58 mut repo, clock, ..
59 }: CallContext,
60 NoApi(mut rng): NoApi<BoxRng>,
61 id: UlidPathParam,
62) -> Result<StatusCode, RouteError> {
63 let email = repo
64 .user_email()
65 .lookup(*id)
66 .await?
67 .ok_or(RouteError::NotFound(*id))?;
68
69 let job = ProvisionUserJob::new_for_id(email.user_id);
70 repo.user_email().remove(email).await?;
71
72 repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
74
75 repo.save().await?;
76
77 Ok(StatusCode::NO_CONTENT)
78}
79
80#[cfg(test)]
81mod tests {
82 use hyper::{Request, StatusCode};
83 use sqlx::PgPool;
84 use ulid::Ulid;
85
86 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
87 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
88 async fn test_delete(pool: PgPool) {
89 setup();
90 let mut state = TestState::from_pool(pool).await.unwrap();
91 let token = state.token_with_scope("urn:mas:admin").await;
92 let mut rng = state.rng();
93
94 let mut repo = state.repository().await.unwrap();
96 let alice = repo
97 .user()
98 .add(&mut rng, &state.clock, "alice".to_owned())
99 .await
100 .unwrap();
101 let mas_data_model::UserEmail { id, .. } = repo
102 .user_email()
103 .add(
104 &mut rng,
105 &state.clock,
106 &alice,
107 "alice@example.com".to_owned(),
108 )
109 .await
110 .unwrap();
111
112 repo.save().await.unwrap();
113
114 let request = Request::delete(format!("/api/admin/v1/user-emails/{id}"))
115 .bearer(&token)
116 .empty();
117 let response = state.request(request).await;
118 response.assert_status(StatusCode::NO_CONTENT);
119
120 let request = Request::get(format!("/api/admin/v1/user-emails/{id}"))
122 .bearer(&token)
123 .empty();
124 let response = state.request(request).await;
125 response.assert_status(StatusCode::NOT_FOUND);
126 }
127
128 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
129 async fn test_not_found(pool: PgPool) {
130 setup();
131 let mut state = TestState::from_pool(pool).await.unwrap();
132 let token = state.token_with_scope("urn:mas:admin").await;
133
134 let email_id = Ulid::nil();
135 let request = Request::delete(format!("/api/admin/v1/user-emails/{email_id}"))
136 .bearer(&token)
137 .empty();
138 let response = state.request(request).await;
139 response.assert_status(StatusCode::NOT_FOUND);
140 }
141}