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(&mut rng, &state.clock, &provider, String::new(), None, None)
112 .await
113 .unwrap();
114
115 let link = repo
116 .upstream_oauth_link()
117 .add(
118 &mut rng,
119 &state.clock,
120 &provider,
121 String::from("subject1"),
122 None,
123 )
124 .await
125 .unwrap();
126
127 let session = repo
128 .upstream_oauth_session()
129 .complete_with_link(&state.clock, session, &link, None, None, None)
130 .await
131 .unwrap();
132
133 repo.upstream_oauth_link()
134 .associate_to_user(&link, &alice)
135 .await
136 .unwrap();
137
138 repo.save().await.unwrap();
139
140 let request = Request::delete(format!("/api/admin/v1/upstream-oauth-links/{}", link.id))
141 .bearer(&token)
142 .empty();
143 let response = state.request(request).await;
144 response.assert_status(StatusCode::NO_CONTENT);
145
146 let request = Request::get(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::NOT_FOUND);
152
153 let mut repo = state.repository().await.unwrap();
155 let session = repo
156 .upstream_oauth_session()
157 .lookup(session.id)
158 .await
159 .unwrap()
160 .unwrap();
161 assert!(matches!(
162 session.state,
163 UpstreamOAuthAuthorizationSessionState::Unlinked { .. }
164 ));
165 }
166
167 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
168 async fn test_not_found(pool: PgPool) {
169 setup();
170 let mut state = TestState::from_pool(pool).await.unwrap();
171 let token = state.token_with_scope("urn:mas:admin").await;
172
173 let link_id = Ulid::nil();
174 let request = Request::delete(format!("/api/admin/v1/upstream-oauth-links/{link_id}"))
175 .bearer(&token)
176 .empty();
177 let response = state.request(request).await;
178 response.assert_status(StatusCode::NOT_FOUND);
179 }
180}