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}