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