1use sea_orm::FromQueryResult;
49use sea_orm_entities::entity::inquiries;
50use serde::{Deserialize, Serialize};
51use serde_json::{json, Value};
52use std::collections::HashMap;
53
54#[derive(Debug, Deserialize)]
59pub(crate) struct Request {
60 #[serde(rename = "requestContext")]
62 pub(crate) request_context: RequestContext,
63 pub(crate) body: Option<String>,
66}
67
68#[derive(Debug, Deserialize)]
72pub(crate) struct RequestContext {
73 pub(crate) http: Http,
75 pub(crate) authorizer: Option<Authorizer>,
78}
79
80#[derive(Debug, Deserialize)]
82pub(crate) struct Http {
83 pub(crate) method: String,
85}
86
87#[derive(Debug, Deserialize)]
92pub(crate) struct Authorizer {
93 pub(crate) jwt: Jwt,
95}
96
97#[derive(Debug, Deserialize)]
101pub(crate) struct Jwt {
102 pub(crate) claims: Claims,
104}
105
106#[derive(Debug, Deserialize)]
111pub(crate) struct Claims {
112 pub(crate) email: Option<String>,
115 #[serde(rename = "sub")]
118 pub(crate) cognito_sub: Option<String>,
119}
120
121#[derive(Debug, Serialize)]
127pub(crate) struct Response {
128 #[serde(rename = "statusCode")]
130 pub(crate) status_code: u16,
131 pub(crate) headers: HashMap<String, String>,
134 pub(crate) body: String,
136}
137
138#[derive(Debug, Serialize, FromQueryResult)]
144#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
145pub(crate) struct Inquiry {
146 pub(crate) id: uuid::Uuid,
148 pub(crate) cognito_sub: uuid::Uuid,
150 pub(crate) email: String,
152 pub(crate) subject: String,
154 pub(crate) body: String,
156 pub(crate) created_at: chrono::DateTime<chrono::FixedOffset>,
158}
159
160#[derive(Debug, Serialize)]
165#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
166pub(crate) struct InquiryListResponse {
167 pub(crate) email: String,
169 pub(crate) count: u64,
171 pub(crate) inquiries: Vec<Inquiry>,
173}
174
175#[derive(Debug, Deserialize)]
189#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
190pub(crate) struct CreateInquiryRequest {
191 pub(crate) subject: String,
193 pub(crate) body: String,
195}
196
197#[derive(Debug, Serialize)]
202#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
203pub(crate) struct CreateInquiryResponse {
204 pub(crate) inquiry: Inquiry,
206}
207
208#[derive(Debug, Serialize)]
223#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
224#[allow(dead_code)]
225pub(crate) struct ErrorResponseBody {
226 pub(crate) error: String,
228 pub(crate) message: String,
230}
231
232impl From<inquiries::Model> for Inquiry {
237 fn from(model: inquiries::Model) -> Self {
238 Inquiry {
239 id: model.id,
240 cognito_sub: model.cognito_sub,
241 email: model.email,
242 subject: model.subject,
243 body: model.body,
244 created_at: model.created_at,
245 }
246 }
247}
248
249impl Response {
250 pub(crate) fn new(status_code: u16, body: Value, cors_origin: &str) -> Self {
266 let mut headers = HashMap::new();
267 headers.insert("Content-Type".to_string(), "application/json".to_string());
268 headers.insert(
269 "Access-Control-Allow-Origin".to_string(),
270 cors_origin.to_string(),
271 );
272
273 Response {
274 status_code,
275 headers,
276 body: body.to_string(),
277 }
278 }
279
280 pub(crate) fn error(status_code: u16, error: &str, message: &str, cors_origin: &str) -> Self {
296 Self::new(
297 status_code,
298 json!({
299 "error": error,
300 "message": message
301 }),
302 cors_origin,
303 )
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_response_new_status_code() {
313 let resp = Response::new(200, serde_json::json!({"ok": true}), "https://example.com");
314 assert_eq!(resp.status_code, 200);
315 }
316
317 #[test]
318 fn test_response_new_cors_header() {
319 let origin = "https://example.com";
320 let resp = Response::new(200, serde_json::json!({}), origin);
321 assert_eq!(
322 resp.headers
323 .get("Access-Control-Allow-Origin")
324 .map(String::as_str),
325 Some(origin)
326 );
327 }
328
329 #[test]
330 fn test_response_new_content_type() {
331 let resp = Response::new(200, serde_json::json!({}), "https://example.com");
332 assert_eq!(
333 resp.headers.get("Content-Type").map(String::as_str),
334 Some("application/json")
335 );
336 }
337
338 #[test]
339 fn test_response_error_status_and_body() {
340 let resp = Response::error(401, "Unauthorized", "Invalid token", "https://example.com");
341 assert_eq!(resp.status_code, 401);
342 let body: serde_json::Value = serde_json::from_str(&resp.body).unwrap();
343 assert_eq!(body["error"], "Unauthorized");
344 assert_eq!(body["message"], "Invalid token");
345 }
346
347 #[test]
348 fn test_inquiry_serialization() {
349 let id = uuid::Uuid::now_v7();
350 let cognito_sub = uuid::Uuid::now_v7();
351 let now = chrono::Utc::now().fixed_offset();
352 let inquiry = Inquiry {
353 id,
354 cognito_sub,
355 email: "test@example.com".to_string(),
356 subject: "Test subject".to_string(),
357 body: "Test body".to_string(),
358 created_at: now,
359 };
360 let json = serde_json::to_value(&inquiry).unwrap();
361 assert_eq!(json["email"], "test@example.com");
362 assert_eq!(json["subject"], "Test subject");
363 assert_eq!(json["body"], "Test body");
364 }
365
366 #[test]
367 fn test_create_inquiry_request_deserialization() {
368 let json = r#"{"subject": "Hello", "body": "World"}"#;
369 let req: CreateInquiryRequest = serde_json::from_str(json).unwrap();
370 assert_eq!(req.subject, "Hello");
371 assert_eq!(req.body, "World");
372 }
373
374 #[test]
375 fn test_claims_deserialization_with_missing_sub() {
376 let json = r#"{"email":"test@example.com"}"#;
377 let claims: Claims = serde_json::from_str(json).unwrap();
378 assert_eq!(claims.email.as_deref(), Some("test@example.com"));
379 assert_eq!(claims.cognito_sub, None);
380 }
381}
382
383#[cfg(test)]
384mod prop_tests {
385 use super::*;
386 use proptest::prelude::*;
387
388 proptest! {
389 #[test]
391 fn prop_response_new_status_code_is_preserved(
392 status_code in 100u16..=599u16,
393 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
394 ) {
395 let resp = Response::new(status_code, serde_json::json!({}), &origin);
396 prop_assert_eq!(resp.status_code, status_code);
397 }
398
399 #[test]
401 fn prop_response_new_cors_header_matches_origin(
402 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
403 ) {
404 let resp = Response::new(200, serde_json::json!({}), &origin);
405 prop_assert_eq!(
406 resp.headers.get("Access-Control-Allow-Origin").map(String::as_str),
407 Some(origin.as_str())
408 );
409 }
410
411 #[test]
413 fn prop_response_new_content_type_is_always_json(
414 status_code in 100u16..=599u16,
415 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
416 ) {
417 let resp = Response::new(status_code, serde_json::json!({}), &origin);
418 prop_assert_eq!(
419 resp.headers.get("Content-Type").map(String::as_str),
420 Some("application/json")
421 );
422 }
423
424 #[test]
426 fn prop_response_error_body_contains_error_and_message(
427 status_code in 400u16..=599u16,
428 error in "[A-Za-z_]{1,30}",
429 message in "[A-Za-z0-9 ]{1,100}",
430 origin in "https?://[a-z]{1,10}\\.[a-z]{2,4}",
431 ) {
432 let resp = Response::error(status_code, &error, &message, &origin);
433 let body: serde_json::Value = serde_json::from_str(&resp.body).unwrap();
434 prop_assert_eq!(body["error"].as_str(), Some(error.as_str()));
435 prop_assert_eq!(body["message"].as_str(), Some(message.as_str()));
436 }
437
438 #[test]
440 fn prop_create_inquiry_request_roundtrip(
441 subject in ".*",
442 body in ".*",
443 ) {
444 let json = serde_json::json!({ "subject": subject, "body": body });
445 let req: CreateInquiryRequest = serde_json::from_value(json).unwrap();
446 prop_assert_eq!(&req.subject, &subject);
447 prop_assert_eq!(&req.body, &body);
448 }
449
450 #[test]
452 fn prop_inquiry_serialization_preserves_fields(
453 email in "[a-z]{1,10}@[a-z]{1,8}\\.[a-z]{2,4}",
454 subject in "[A-Za-z0-9 ]{1,50}",
455 body in "[A-Za-z0-9 ]{1,200}",
456 ) {
457 let id = uuid::Uuid::now_v7();
458 let cognito_sub = uuid::Uuid::now_v7();
459 let now = chrono::Utc::now().fixed_offset();
460 let inquiry = Inquiry { id, cognito_sub, email: email.clone(), subject: subject.clone(), body: body.clone(), created_at: now };
461 let json = serde_json::to_value(&inquiry).unwrap();
462 prop_assert_eq!(json["email"].as_str(), Some(email.as_str()));
463 prop_assert_eq!(json["subject"].as_str(), Some(subject.as_str()));
464 prop_assert_eq!(json["body"].as_str(), Some(body.as_str()));
465 }
466 }
467}