1use sea_orm::FromQueryResult;
49use sea_orm_entities::entity::messages;
50use serde::{Deserialize, Serialize};
51use serde_json::{Value, json};
52use std::collections::HashMap;
53
54#[derive(Debug, Deserialize, Serialize)]
59pub(crate) struct Request {
60 #[serde(rename = "requestContext")]
62 pub(crate) request_context: RequestContext,
63 #[serde(rename = "rawPath")]
64 pub(crate) raw_path: Option<String>,
65 pub(crate) body: Option<String>,
68}
69
70#[derive(Debug, Deserialize, Serialize)]
74pub(crate) struct RequestContext {
75 pub(crate) http: Http,
77 pub(crate) authorizer: Option<Authorizer>,
80}
81
82#[derive(Debug, Deserialize, Serialize)]
84pub(crate) struct Http {
85 pub(crate) method: String,
87 pub(crate) path: Option<String>,
89}
90
91#[derive(Debug, Deserialize, Serialize)]
96pub(crate) struct Authorizer {
97 pub(crate) jwt: Jwt,
99}
100
101#[derive(Debug, Deserialize, Serialize)]
105pub(crate) struct Jwt {
106 pub(crate) claims: Claims,
108}
109
110#[derive(Debug, Deserialize, Serialize)]
115pub(crate) struct Claims {
116 pub(crate) email: Option<String>,
119 #[serde(rename = "sub")]
122 pub(crate) cognito_sub: Option<String>,
123}
124
125#[derive(Debug, Serialize)]
131pub(crate) struct Response {
132 #[serde(rename = "statusCode")]
134 pub(crate) status_code: u16,
135 pub(crate) headers: HashMap<String, String>,
138 pub(crate) body: String,
140}
141
142#[derive(Debug, Serialize, FromQueryResult)]
144#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
145pub(crate) struct Message {
146 pub(crate) cognito_id: uuid::Uuid,
148 pub(crate) is_from_user: bool,
150 pub(crate) body: String,
152 pub(crate) created_at: Option<chrono::DateTime<chrono::FixedOffset>>,
154}
155
156#[derive(Debug, Serialize)]
158#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
159pub(crate) struct MessageListResponse {
160 pub(crate) email: String,
162 pub(crate) count: u64,
164 pub(crate) messages: Vec<Message>,
166}
167
168#[derive(Debug, Deserialize)]
170#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
171pub(crate) struct CreateMessageRequest {
172 pub(crate) body: String,
174}
175
176#[derive(Debug, Serialize)]
178#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
179pub(crate) struct CreateMessageResponse {
180 pub(crate) message: CreatedMessage,
182}
183
184#[derive(Debug, Serialize)]
186#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
187pub(crate) struct CreatedMessage {
188 pub(crate) id: uuid::Uuid,
190 pub(crate) cognito_id: uuid::Uuid,
192 pub(crate) is_from_user: bool,
194 pub(crate) body: String,
196 pub(crate) created_at: Option<chrono::DateTime<chrono::FixedOffset>>,
198}
199
200#[derive(Debug, Serialize)]
215#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
216#[allow(dead_code)]
217pub(crate) struct ErrorResponseBody {
218 pub(crate) error: String,
220 pub(crate) message: String,
222}
223
224impl From<messages::Model> for CreatedMessage {
225 fn from(model: messages::Model) -> Self {
226 CreatedMessage {
227 id: model.id,
228 cognito_id: model.cognito_id,
229 is_from_user: model.is_from_user,
230 body: model.body,
231 created_at: model.created_at,
232 }
233 }
234}
235
236impl Response {
237 pub(crate) fn new(status_code: u16, body: Value, cors_origin: &str) -> Self {
253 Self::new_with_content_type(
254 status_code,
255 body.to_string(),
256 "application/json",
257 cors_origin,
258 )
259 }
260
261 pub(crate) fn new_with_content_type(
262 status_code: u16,
263 body: String,
264 content_type: &str,
265 cors_origin: &str,
266 ) -> Self {
267 let mut headers = HashMap::new();
268 headers.insert("Content-Type".to_string(), content_type.to_string());
269 headers.insert(
270 "Access-Control-Allow-Origin".to_string(),
271 cors_origin.to_string(),
272 );
273
274 Response {
275 status_code,
276 headers,
277 body,
278 }
279 }
280
281 pub(crate) fn text(status_code: u16, body: impl Into<String>, cors_origin: &str) -> Self {
282 Self::new_with_content_type(
283 status_code,
284 body.into(),
285 "text/plain; charset=utf-8",
286 cors_origin,
287 )
288 }
289
290 pub(crate) fn error(status_code: u16, error: &str, message: &str, cors_origin: &str) -> Self {
306 Self::new(
307 status_code,
308 json!({
309 "error": error,
310 "message": message
311 }),
312 cors_origin,
313 )
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_response_new_status_code() {
323 let resp = Response::new(200, serde_json::json!({"ok": true}), "https://example.com");
324 assert_eq!(resp.status_code, 200);
325 }
326
327 #[test]
328 fn test_response_new_cors_header() {
329 let origin = "https://example.com";
330 let resp = Response::new(200, serde_json::json!({}), origin);
331 assert_eq!(
332 resp.headers
333 .get("Access-Control-Allow-Origin")
334 .map(String::as_str),
335 Some(origin)
336 );
337 }
338
339 #[test]
340 fn test_response_new_content_type() {
341 let resp = Response::new(200, serde_json::json!({}), "https://example.com");
342 assert_eq!(
343 resp.headers.get("Content-Type").map(String::as_str),
344 Some("application/json")
345 );
346 }
347
348 #[test]
349 fn test_response_error_status_and_body() {
350 let resp = Response::error(401, "Unauthorized", "Invalid token", "https://example.com");
351 assert_eq!(resp.status_code, 401);
352 let body: serde_json::Value = serde_json::from_str(&resp.body).unwrap();
353 assert_eq!(body["error"], "Unauthorized");
354 assert_eq!(body["message"], "Invalid token");
355 }
356
357 #[test]
358 fn test_message_serialization() {
359 let cognito_id = uuid::Uuid::now_v7();
360 let message = Message {
361 cognito_id,
362 is_from_user: true,
363 body: "Test body".to_string(),
364 created_at: None,
365 };
366 let json = serde_json::to_value(&message).unwrap();
367 assert_eq!(json["cognito_id"], cognito_id.to_string());
368 assert_eq!(json["is_from_user"], true);
369 assert_eq!(json["body"], "Test body");
370 }
371
372 #[test]
373 fn test_create_message_request_deserialization() {
374 let json = r#"{"body": "World"}"#;
375 let req: CreateMessageRequest = serde_json::from_str(json).unwrap();
376 assert_eq!(req.body, "World");
377 }
378
379 #[test]
380 fn test_claims_deserialization_with_missing_sub() {
381 let json = r#"{"email":"test@example.com"}"#;
382 let claims: Claims = serde_json::from_str(json).unwrap();
383 assert_eq!(claims.email.as_deref(), Some("test@example.com"));
384 assert_eq!(claims.cognito_sub, None);
385 }
386
387 #[test]
388 fn test_response_text_content_type() {
389 let resp = Response::text(200, "OK", "https://example.com");
390 assert_eq!(
391 resp.headers.get("Content-Type").map(String::as_str),
392 Some("text/plain; charset=utf-8")
393 );
394 assert_eq!(resp.body, "OK");
395 }
396}
397
398#[cfg(test)]
399mod prop_tests {
400 use super::*;
401 use proptest::prelude::*;
402
403 proptest! {
404 #[test]
406 fn prop_response_new_status_code_is_preserved(
407 status_code in 100u16..=599u16,
408 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
409 ) {
410 let resp = Response::new(status_code, serde_json::json!({}), &origin);
411 prop_assert_eq!(resp.status_code, status_code);
412 }
413
414 #[test]
416 fn prop_response_new_cors_header_matches_origin(
417 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
418 ) {
419 let resp = Response::new(200, serde_json::json!({}), &origin);
420 prop_assert_eq!(
421 resp.headers.get("Access-Control-Allow-Origin").map(String::as_str),
422 Some(origin.as_str())
423 );
424 }
425
426 #[test]
428 fn prop_response_new_content_type_is_always_json(
429 status_code in 100u16..=599u16,
430 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
431 ) {
432 let resp = Response::new(status_code, serde_json::json!({}), &origin);
433 prop_assert_eq!(
434 resp.headers.get("Content-Type").map(String::as_str),
435 Some("application/json")
436 );
437 }
438
439 #[test]
441 fn prop_response_error_body_contains_error_and_message(
442 status_code in 400u16..=599u16,
443 error in "[A-Za-z_]{1,30}",
444 message in "[A-Za-z0-9 ]{1,100}",
445 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
446 ) {
447 let resp = Response::error(status_code, &error, &message, &origin);
448 let body: serde_json::Value = serde_json::from_str(&resp.body).unwrap();
449 prop_assert_eq!(body["error"].as_str(), Some(error.as_str()));
450 prop_assert_eq!(body["message"].as_str(), Some(message.as_str()));
451 }
452
453 #[test]
455 fn prop_create_message_request_roundtrip(body in ".*") {
456 let json = serde_json::json!({ "body": body });
457 let req: CreateMessageRequest = serde_json::from_value(json).unwrap();
458 prop_assert_eq!(&req.body, &body);
459 }
460
461 #[test]
463 fn prop_message_serialization_preserves_fields(body in "[A-Za-z0-9 ]{1,200}") {
464 let cognito_id = uuid::Uuid::now_v7();
465 let cognito_id_string = cognito_id.to_string();
466 let message = Message {
467 cognito_id,
468 is_from_user: true,
469 body: body.clone(),
470 created_at: None,
471 };
472 let json = serde_json::to_value(&message).unwrap();
473 prop_assert_eq!(json["cognito_id"].as_str(), Some(cognito_id_string.as_str()));
474 prop_assert_eq!(json["is_from_user"].as_bool(), Some(true));
475 prop_assert_eq!(json["body"].as_str(), Some(body.as_str()));
476 }
477 }
478}