mas_handlers/admin/v1/compat_sessions/
get.rs

1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4// Please see LICENSE in the repository root for full details.
5
6use 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::{
14        call_context::CallContext,
15        model::CompatSession,
16        params::UlidPathParam,
17        response::{ErrorResponse, SingleResponse},
18    },
19    impl_from_error_for_route,
20};
21
22#[derive(Debug, thiserror::Error, OperationIo)]
23#[aide(output_with = "Json<ErrorResponse>")]
24pub enum RouteError {
25    #[error(transparent)]
26    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
27
28    #[error("Compatibility session ID {0} not found")]
29    NotFound(Ulid),
30}
31
32impl_from_error_for_route!(mas_storage::RepositoryError);
33
34impl IntoResponse for RouteError {
35    fn into_response(self) -> axum::response::Response {
36        let error = ErrorResponse::from_error(&self);
37        let sentry_event_id = record_error!(self, RouteError::Internal(_));
38        let status = match &self {
39            Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
40            Self::NotFound(_) => StatusCode::NOT_FOUND,
41        };
42
43        (status, sentry_event_id, Json(error)).into_response()
44    }
45}
46
47pub fn doc(operation: TransformOperation) -> TransformOperation {
48    operation
49        .id("getCompatSession")
50        .summary("Get a compatibility session")
51        .tag("compat-session")
52        .response_with::<200, Json<SingleResponse<CompatSession>>, _>(|t| {
53            let [sample, ..] = CompatSession::samples();
54            let response = SingleResponse::new_canonical(sample);
55            t.description("Compatibility session was found")
56                .example(response)
57        })
58        .response_with::<404, RouteError, _>(|t| {
59            let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
60            t.description("Compatibility session was not found")
61                .example(response)
62        })
63}
64
65#[tracing::instrument(name = "handler.admin.v1.compat_sessions.get", skip_all)]
66pub async fn handler(
67    CallContext { mut repo, .. }: CallContext,
68    id: UlidPathParam,
69) -> Result<Json<SingleResponse<CompatSession>>, RouteError> {
70    let session = repo
71        .compat_session()
72        .lookup(*id)
73        .await?
74        .ok_or(RouteError::NotFound(*id))?;
75
76    let sso_login = repo.compat_sso_login().find_for_session(&session).await?;
77
78    Ok(Json(SingleResponse::new_canonical(CompatSession::from((
79        session, sso_login,
80    )))))
81}
82
83#[cfg(test)]
84mod tests {
85    use hyper::{Request, StatusCode};
86    use insta::assert_json_snapshot;
87    use mas_data_model::Device;
88    use sqlx::PgPool;
89    use ulid::Ulid;
90
91    use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
92
93    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
94    async fn test_get(pool: PgPool) {
95        setup();
96        let mut state = TestState::from_pool(pool).await.unwrap();
97        let token = state.token_with_scope("urn:mas:admin").await;
98        let mut rng = state.rng();
99
100        // Provision a user and a compat session
101        let mut repo = state.repository().await.unwrap();
102        let user = repo
103            .user()
104            .add(&mut rng, &state.clock, "alice".to_owned())
105            .await
106            .unwrap();
107        let device = Device::generate(&mut rng);
108        let session = repo
109            .compat_session()
110            .add(&mut rng, &state.clock, &user, device, None, false)
111            .await
112            .unwrap();
113        repo.save().await.unwrap();
114
115        let session_id = session.id;
116        let request = Request::get(format!("/api/admin/v1/compat-sessions/{session_id}"))
117            .bearer(&token)
118            .empty();
119        let response = state.request(request).await;
120        response.assert_status(StatusCode::OK);
121        let body: serde_json::Value = response.json();
122        assert_json_snapshot!(body, @r###"
123        {
124          "data": {
125            "type": "compat-session",
126            "id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
127            "attributes": {
128              "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
129              "device_id": "TpLoieH5Ie",
130              "user_session_id": null,
131              "redirect_uri": null,
132              "created_at": "2022-01-16T14:40:00Z",
133              "user_agent": null,
134              "last_active_at": null,
135              "last_active_ip": null,
136              "finished_at": null
137            },
138            "links": {
139              "self": "/api/admin/v1/compat-sessions/01FSHN9AG0QHEHKX2JNQ2A2D07"
140            }
141          },
142          "links": {
143            "self": "/api/admin/v1/compat-sessions/01FSHN9AG0QHEHKX2JNQ2A2D07"
144          }
145        }
146        "###);
147    }
148
149    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
150    async fn test_not_found(pool: PgPool) {
151        setup();
152        let mut state = TestState::from_pool(pool).await.unwrap();
153        let token = state.token_with_scope("urn:mas:admin").await;
154
155        let session_id = Ulid::nil();
156        let request = Request::get(format!("/api/admin/v1/compat-sessions/{session_id}"))
157            .bearer(&token)
158            .empty();
159        let response = state.request(request).await;
160        response.assert_status(StatusCode::NOT_FOUND);
161    }
162}