serverify/
session_endpoint.rs

1use axum::{
2    extract::{Json, Path, State},
3    http::StatusCode,
4    response::IntoResponse,
5    routing::{delete, get, post},
6    Router,
7};
8use once_cell::sync::Lazy;
9use regex::Regex;
10
11use crate::{
12    request_logger::{LoggerError, RequestLog},
13    response::{error_response, success_response, WithError},
14    state::AppState,
15};
16
17pub fn route_session_to(app: Router<AppState>) -> Router<AppState> {
18    app.route("/session", post(create_session))
19        .route("/session/:session", get(get_session))
20        .route("/session/:session", delete(delete_session))
21}
22
23#[derive(serde::Deserialize)]
24struct CreateReqBody {
25    session: String,
26}
27
28#[derive(serde::Serialize)]
29struct CreateResBody {
30    session: String,
31}
32
33static SESSION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[-a-zA-Z0-9_]+$").unwrap());
34
35async fn create_session(
36    State(state): State<AppState>,
37    Json(CreateReqBody { session }): Json<CreateReqBody>,
38) -> impl IntoResponse {
39    if !SESSION_NAME_REGEX.is_match(&session) {
40        return error_response(
41            StatusCode::BAD_REQUEST,
42            "session name should contains only alphanumeric, hyphen or underscore",
43        );
44    }
45
46    match state.logger.create_session(&session).await {
47        Ok(_) => success_response(StatusCode::CREATED, CreateResBody { session }),
48        Err(LoggerError::InvalidSession(message)) => error_response(StatusCode::CONFLICT, message),
49        Err(LoggerError::InternalError(message)) => {
50            error_response(StatusCode::INTERNAL_SERVER_ERROR, message)
51        }
52    }
53}
54
55#[derive(serde::Serialize)]
56struct GetResBody {
57    histories: Vec<RequestLog>,
58}
59
60async fn get_session(
61    State(state): State<AppState>,
62    Path(session): Path<String>,
63) -> (StatusCode, Json<WithError<GetResBody>>) {
64    match state.logger.get_session_history(&session).await {
65        Ok(histories) => success_response(StatusCode::OK, GetResBody { histories }),
66        Err(LoggerError::InvalidSession(message)) => error_response(StatusCode::NOT_FOUND, message),
67        Err(LoggerError::InternalError(message)) => {
68            error_response(StatusCode::INTERNAL_SERVER_ERROR, message)
69        }
70    }
71}
72
73#[derive(serde::Serialize)]
74struct DeleteResBody {
75    session: String,
76}
77
78async fn delete_session(
79    State(state): State<AppState>,
80    Path(session): Path<String>,
81) -> impl IntoResponse {
82    match state.logger.delete_session(&session).await {
83        Ok(_) => success_response(StatusCode::OK, DeleteResBody { session }),
84        Err(LoggerError::InvalidSession(message)) => error_response(StatusCode::NOT_FOUND, message),
85        Err(LoggerError::InternalError(message)) => {
86            error_response(StatusCode::INTERNAL_SERVER_ERROR, message)
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93
94    use crate::{method::Method, request_logger::testutil::new_logger};
95
96    use super::*;
97    use axum_test::TestServer;
98    use chrono::{Local, NaiveDate, TimeZone};
99    use indexmap::indexmap;
100    use pretty_assertions::assert_eq;
101    use rstest::*;
102    use serde_json::{json, Value};
103
104    const EXIST_SESSION: &str = "exist_session";
105
106    async fn new_test_server_with_default_session() -> (TestServer, AppState) {
107        let logger = new_logger().await;
108        logger.create_session(EXIST_SESSION).await.unwrap();
109
110        let requested_at = Local
111            .from_local_datetime(
112                &NaiveDate::from_ymd_opt(2024, 1, 2)
113                    .unwrap()
114                    .and_hms_opt(3, 4, 5)
115                    .unwrap(),
116            )
117            .unwrap();
118        logger
119            .log_request(
120                EXIST_SESSION,
121                &RequestLog {
122                    method: Method::Post,
123                    path: "/greet".to_string(),
124                    headers: indexmap! {
125                        "token".to_string() => "abc".to_string()
126                    },
127                    query: indexmap! {
128                        "answer".to_string() => "42".to_string(),
129                    },
130                    body: r#"{"message":"hello"}"#.to_string(),
131                    requested_at,
132                },
133            )
134            .await
135            .unwrap();
136
137        let state = AppState { logger };
138        (
139            TestServer::new(route_session_to(Router::new()).with_state(state.clone())).unwrap(),
140            state,
141        )
142    }
143
144    mod create_session {
145        use super::*;
146        use pretty_assertions::assert_eq;
147
148        #[tokio::test]
149        async fn success_case() {
150            let (server, state) = new_test_server_with_default_session().await;
151
152            let response = server
153                .post("/session")
154                .json(&json!({ "session": "mysession" }))
155                .await;
156
157            assert_eq!(
158                (StatusCode::CREATED, json!({ "session": "mysession" })),
159                (response.status_code(), response.json()),
160            );
161
162            assert_eq!(
163                Ok(vec![]),
164                state.logger.get_session_history("mysession").await
165            );
166        }
167
168        #[tokio::test]
169        async fn when_already_exists() {
170            let (server, _) = new_test_server_with_default_session().await;
171
172            let response = server
173                .post("/session")
174                .json(&json!({ "session": EXIST_SESSION }))
175                .await;
176
177            assert_eq!(
178                (
179                    StatusCode::CONFLICT,
180                    json!({ "serverify_error": { "message": "session \"exist_session\" already exists" } })
181                ),
182                (response.status_code(), response.json()),
183            );
184        }
185
186        #[tokio::test]
187        async fn when_invalid_session_name() {
188            let (server, state) = new_test_server_with_default_session().await;
189
190            let response = server
191                .post("/session")
192                .json(&json!({ "session": "invalid session" }))
193                .await;
194
195            assert_eq!(
196                (
197                    StatusCode::BAD_REQUEST,
198                    json!({ "serverify_error": { "message": "session name should contains only alphanumeric, hyphen or underscore" } })
199                ),
200                (response.status_code(), response.json()),
201            );
202
203            assert_eq!(
204                Err(LoggerError::InvalidSession(
205                    "session \"invalid session\" is not found".to_string()
206                )),
207                state.logger.get_session_history("invalid session").await
208            );
209        }
210    }
211
212    #[rstest]
213    #[tokio::test]
214    #[case(
215        "success case",
216        "exist_session",
217        StatusCode::OK,
218        json!({
219            "histories": [
220                {
221                    "method": "post",
222                    "path": "/greet",
223                    "headers": {
224                        "token": "abc"
225                    },
226                    "query": {"answer": "42" },
227                    "body": r#"{"message":"hello"}"#,
228                    "requested_at": "2024-01-02T03:04:05+09:00"
229                }
230            ]
231        }),
232
233    )]
234    #[tokio::test]
235    #[case(
236        "session dose not exist",
237        "undefined_session",
238        StatusCode::NOT_FOUND,
239        json!({ "serverify_error": { "message": "session \"undefined_session\" is not found" } }),
240    )]
241    async fn get_session(
242        #[case] title: &str,
243        #[case] session: &str,
244        #[case] expected_status_code: StatusCode,
245        #[case] expected_res_body: Value,
246    ) {
247        let (server, _) = new_test_server_with_default_session().await;
248
249        let response = server.get(&format!("/session/{}", session)).await;
250
251        assert_eq!(
252            (expected_status_code, expected_res_body),
253            (response.status_code(), response.json()),
254            "{}: response",
255            title
256        );
257    }
258
259    #[rstest]
260    #[tokio::test]
261    #[case(
262        "success case",
263        "exist_session",
264        StatusCode::OK,
265        json!({ "session": "exist_session" }),
266        false,
267    )]
268    #[tokio::test]
269    #[case(
270        "session is not found",
271        "undefined_session",
272        StatusCode::NOT_FOUND,
273        json!({ "serverify_error": { "message": "session \"undefined_session\" is not found" } }),
274        true
275    )]
276    async fn delete_session(
277        #[case] title: &str,
278        #[case] session: &str,
279        #[case] expected_status_code: StatusCode,
280        #[case] expected_res_body: Value,
281        #[case] expected_exist_session_found: bool,
282    ) {
283        let (server, state) = new_test_server_with_default_session().await;
284
285        let response = server.delete(&format!("/session/{}", session)).await;
286
287        assert_eq!(
288            (expected_status_code, expected_res_body),
289            (response.status_code(), response.json()),
290            "{}: response",
291            title
292        );
293
294        assert_eq!(
295            expected_exist_session_found,
296            state
297                .logger
298                .get_session_history(EXIST_SESSION)
299                .await
300                .is_ok(),
301        );
302    }
303}