mas_handlers/admin/v1/upstream_oauth_links/
delete.rs1use aide::{OperationIo, transform::TransformOperation};
7use axum::{Json, response::IntoResponse};
8use hyper::StatusCode;
9use mas_axum_utils::record_error;
10use ulid::Ulid;
11
12use crate::{
13 admin::{call_context::CallContext, params::UlidPathParam, response::ErrorResponse},
14 impl_from_error_for_route,
15};
16
17#[derive(Debug, thiserror::Error, OperationIo)]
18#[aide(output_with = "Json<ErrorResponse>")]
19pub enum RouteError {
20 #[error(transparent)]
21 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
22
23 #[error("Upstream OAuth 2.0 Link ID {0} not found")]
24 NotFound(Ulid),
25}
26
27impl_from_error_for_route!(mas_storage::RepositoryError);
28
29impl IntoResponse for RouteError {
30 fn into_response(self) -> axum::response::Response {
31 let error = ErrorResponse::from_error(&self);
32 let sentry_event_id = record_error!(self, Self::Internal(_));
33 let status = match self {
34 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
35 Self::NotFound(_) => StatusCode::NOT_FOUND,
36 };
37 (status, sentry_event_id, Json(error)).into_response()
38 }
39}
40
41pub fn doc(operation: TransformOperation) -> TransformOperation {
42 operation
43 .id("deleteUpstreamOAuthLink")
44 .summary("Delete an upstream OAuth 2.0 link")
45 .tag("upstream-oauth-link")
46 .response_with::<204, (), _>(|t| t.description("Upstream OAuth 2.0 link was deleted"))
47 .response_with::<404, RouteError, _>(|t| {
48 let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
49 t.description("Upstream OAuth 2.0 link was not found")
50 .example(response)
51 })
52}
53
54#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.delete", skip_all)]
55pub async fn handler(
56 CallContext {
57 mut repo, clock, ..
58 }: CallContext,
59 id: UlidPathParam,
60) -> Result<StatusCode, RouteError> {
61 let link = repo
62 .upstream_oauth_link()
63 .lookup(*id)
64 .await?
65 .ok_or(RouteError::NotFound(*id))?;
66
67 repo.upstream_oauth_link().remove(&clock, link).await?;
68
69 repo.save().await?;
70
71 Ok(StatusCode::NO_CONTENT)
72}
73
74#[cfg(test)]
75mod tests {
76 use hyper::{Request, StatusCode};
77 use mas_data_model::UpstreamOAuthAuthorizationSessionState;
78 use sqlx::PgPool;
79 use ulid::Ulid;
80
81 use super::super::test_utils;
82 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
83
84 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
85 async fn test_delete(pool: PgPool) {
86 setup();
87 let mut state = TestState::from_pool(pool).await.unwrap();
88 let token = state.token_with_scope("urn:mas:admin").await;
89 let mut rng = state.rng();
90 let mut repo = state.repository().await.unwrap();
91
92 let alice = repo
93 .user()
94 .add(&mut rng, &state.clock, "alice".to_owned())
95 .await
96 .unwrap();
97
98 let provider = repo
99 .upstream_oauth_provider()
100 .add(
101 &mut rng,
102 &state.clock,
103 test_utils::oidc_provider_params("provider1"),
104 )
105 .await
106 .unwrap();
107
108 let session = repo
110 .upstream_oauth_session()
111 .add(
112 &mut rng,
113 &state.clock,
114 &provider,
115 String::new(),
116 None,
117 String::new(),
118 )
119 .await
120 .unwrap();
121
122 let link = repo
123 .upstream_oauth_link()
124 .add(
125 &mut rng,
126 &state.clock,
127 &provider,
128 String::from("subject1"),
129 None,
130 )
131 .await
132 .unwrap();
133
134 let session = repo
135 .upstream_oauth_session()
136 .complete_with_link(&state.clock, session, &link, None, None, None)
137 .await
138 .unwrap();
139
140 repo.upstream_oauth_link()
141 .associate_to_user(&link, &alice)
142 .await
143 .unwrap();
144
145 repo.save().await.unwrap();
146
147 let request = Request::delete(format!("/api/admin/v1/upstream-oauth-links/{}", link.id))
148 .bearer(&token)
149 .empty();
150 let response = state.request(request).await;
151 response.assert_status(StatusCode::NO_CONTENT);
152
153 let request = Request::get(format!("/api/admin/v1/upstream-oauth-links/{}", link.id))
155 .bearer(&token)
156 .empty();
157 let response = state.request(request).await;
158 response.assert_status(StatusCode::NOT_FOUND);
159
160 let mut repo = state.repository().await.unwrap();
162 let session = repo
163 .upstream_oauth_session()
164 .lookup(session.id)
165 .await
166 .unwrap()
167 .unwrap();
168 assert!(matches!(
169 session.state,
170 UpstreamOAuthAuthorizationSessionState::Unlinked { .. }
171 ));
172 }
173
174 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
175 async fn test_not_found(pool: PgPool) {
176 setup();
177 let mut state = TestState::from_pool(pool).await.unwrap();
178 let token = state.token_with_scope("urn:mas:admin").await;
179
180 let link_id = Ulid::nil();
181 let request = Request::delete(format!("/api/admin/v1/upstream-oauth-links/{link_id}"))
182 .bearer(&token)
183 .empty();
184 let response = state.request(request).await;
185 response.assert_status(StatusCode::NOT_FOUND);
186 }
187}