1use crate::models::{
26 CreateInquiryRequest, CreateInquiryResponse, Inquiry, InquiryListResponse, Response,
27};
28#[cfg(feature = "openapi")]
29use crate::models::ErrorResponseBody;
30use lambda_runtime::Error;
31use sea_orm::{
32 ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, Set,
33};
34use sea_orm_entities::entity::inquiries::{self, Column, Entity as Inquiries};
35
36#[cfg_attr(
37 feature = "openapi",
38 utoipa::path(
39 get,
40 path = "/inquiries",
41 tag = "inquiries",
42 responses(
43 (status = 200, description = "Get inquiries", body = InquiryListResponse),
44 (status = 401, description = "Unauthorized", body = ErrorResponseBody),
45 (status = 500, description = "Internal server error", body = ErrorResponseBody),
46 ),
47 security(
48 ("CognitoAuthorizer" = []),
49 ("BearerAuth" = []),
50 )
51 )
52)]
53pub(crate) async fn handle_get_inquiries(
86 db: &DatabaseConnection,
87 email: &str,
88 cognito_sub: uuid::Uuid,
89 cors_origin: &str,
90) -> Result<Response, Error> {
91 tracing::info!("Querying inquiries for email: {}", email);
92
93 let inquiries: Vec<Inquiry> = Inquiries::find()
94 .filter(Column::Email.eq(email))
95 .filter(Column::CognitoSub.eq(cognito_sub))
96 .order_by_desc(Column::CreatedAt)
97 .into_model::<Inquiry>()
98 .all(db)
99 .await
100 .map_err(|e| {
101 tracing::error!("Database query failed: {}", e);
102 anyhow::anyhow!("Database query failed: {}", e)
103 })?;
104
105 let response_body = InquiryListResponse {
106 email: email.to_string(),
107 count: inquiries.len() as u64,
108 inquiries,
109 };
110
111 Ok(Response::new(
112 200,
113 serde_json::to_value(response_body)?,
114 cors_origin,
115 ))
116}
117
118#[cfg_attr(
119 feature = "openapi",
120 utoipa::path(
121 post,
122 path = "/inquiries",
123 tag = "inquiries",
124 request_body = CreateInquiryRequest,
125 responses(
126 (status = 201, description = "Create inquiry", body = CreateInquiryResponse),
127 (status = 401, description = "Unauthorized", body = ErrorResponseBody),
128 (status = 500, description = "Internal server error", body = ErrorResponseBody),
129 ),
130 security(
131 ("CognitoAuthorizer" = []),
132 ("BearerAuth" = []),
133 )
134 )
135)]
136pub(crate) async fn handle_post_inquiry(
176 db: &DatabaseConnection,
177 email: &str,
178 cognito_sub: uuid::Uuid,
179 body: &str,
180 cors_origin: &str,
181) -> Result<Response, Error> {
182 tracing::info!("Creating inquiry for email: {}", email);
183
184 let create_request: CreateInquiryRequest = serde_json::from_str(body).map_err(|e| {
185 tracing::error!("Failed to parse request body: {}", e);
186 anyhow::anyhow!("Invalid request body: {}", e)
187 })?;
188
189 let id = uuid::Uuid::now_v7();
190 let now = chrono::Utc::now().fixed_offset();
191
192 let new_inquiry = inquiries::ActiveModel {
193 id: Set(id),
194 cognito_sub: Set(cognito_sub),
195 email: Set(email.to_string()),
196 subject: Set(create_request.subject.clone()),
197 body: Set(create_request.body.clone()),
198 created_at: Set(now),
199 reply: Set(None),
200 respondent: Set(None),
201 reply_at: Set(None),
202 };
203
204 new_inquiry.insert(db).await.map_err(|e| {
205 tracing::error!("Failed to insert inquiry: {}", e);
206 anyhow::anyhow!("Failed to insert inquiry: {}", e)
207 })?;
208
209 let inquiry = Inquiry {
210 id,
211 cognito_sub,
212 email: email.to_string(),
213 subject: create_request.subject,
214 body: create_request.body,
215 created_at: now,
216 };
217
218 let response_body = CreateInquiryResponse { inquiry };
219
220 Ok(Response::new(
221 201,
222 serde_json::to_value(response_body)?,
223 cors_origin,
224 ))
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use chrono::Duration;
231 use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
232 use sea_orm_entities::entity::inquiries::{Column, Entity as Inquiries};
233
234 const ORDERING_OFFSET_SECS: i64 = 1;
235
236 async fn connect_local_test_db() -> DatabaseConnection {
237 let database_url = std::env::var("LOCAL_TEST_DATABASE_URL").unwrap_or_else(|_| {
238 "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable".to_string()
239 });
240 Database::connect(database_url)
241 .await
242 .expect("Failed to connect local PostgreSQL test DB")
243 }
244
245 async fn cleanup_test_inquiries(db: &DatabaseConnection, email: &str) {
246 Inquiries::delete_many()
247 .filter(Column::Email.eq(email))
248 .exec(db)
249 .await
250 .expect("test inquiry cleanup should succeed");
251 }
252
253 #[tokio::test]
254 #[ignore = "ローカルのDBが必要なためデフォルトでは実行しない"]
255 async fn test_handle_post_inquiry_with_local_postgres() {
256 let db = connect_local_test_db().await;
257 let email = format!("local-post-{}@example.com", uuid::Uuid::now_v7());
258 let cognito_sub = uuid::Uuid::now_v7();
259 let body = r#"{"subject":"subject from test","body":"body from test"}"#;
260 cleanup_test_inquiries(&db, &email).await;
261
262 let db_for_test = db.clone();
263 let email_for_test = email.clone();
264 let test_result = tokio::spawn(async move {
265 let response =
266 handle_post_inquiry(&db_for_test, &email_for_test, cognito_sub, body, "https://example.com")
267 .await
268 .expect("handle_post_inquiry should succeed");
269
270 assert_eq!(response.status_code, 201);
271 let response_body: serde_json::Value =
272 serde_json::from_str(&response.body).expect("response body should be valid JSON");
273 assert_eq!(response_body["inquiry"]["email"], email_for_test);
274 assert_eq!(response_body["inquiry"]["subject"], "subject from test");
275 assert_eq!(response_body["inquiry"]["body"], "body from test");
276
277 let inquiry_id = uuid::Uuid::parse_str(
278 response_body["inquiry"]["id"]
279 .as_str()
280 .expect("response should include inquiry id"),
281 )
282 .expect("inquiry id should be valid UUID");
283 let saved = Inquiries::find_by_id(inquiry_id)
284 .one(&db_for_test)
285 .await
286 .expect("DB query should succeed")
287 .expect("inserted inquiry should exist");
288 assert_eq!(saved.email, email_for_test);
289 assert_eq!(saved.cognito_sub, cognito_sub);
290 })
291 .await;
292
293 cleanup_test_inquiries(&db, &email).await;
294
295 if let Err(err) = test_result {
296 if err.is_panic() {
297 std::panic::resume_unwind(err.into_panic());
298 }
299 panic!("test task failed: {err}");
300 }
301 }
302
303 #[tokio::test]
304 #[ignore = "ローカルのDBが必要なためデフォルトでは実行しない"]
305 async fn test_handle_get_inquiries_with_local_postgres() {
306 let db = connect_local_test_db().await;
307 let cognito_sub = uuid::Uuid::now_v7();
308 let email = format!("local-get-{}@example.com", uuid::Uuid::now_v7());
309 let now = chrono::Utc::now().fixed_offset();
310 cleanup_test_inquiries(&db, &email).await;
311
312 inquiries::ActiveModel {
313 id: Set(uuid::Uuid::now_v7()),
314 cognito_sub: Set(cognito_sub),
315 email: Set(email.clone()),
316 subject: Set("older subject".to_string()),
317 body: Set("older body".to_string()),
318 reply: Set(None),
319 respondent: Set(None),
320 created_at: Set(now - Duration::seconds(ORDERING_OFFSET_SECS)),
321 reply_at: Set(None),
322 }
323 .insert(&db)
324 .await
325 .expect("older test record insert should succeed");
326
327 inquiries::ActiveModel {
328 id: Set(uuid::Uuid::now_v7()),
329 cognito_sub: Set(cognito_sub),
330 email: Set(email.clone()),
331 subject: Set("newer subject".to_string()),
332 body: Set("newer body".to_string()),
333 reply: Set(None),
334 respondent: Set(None),
335 created_at: Set(now),
336 reply_at: Set(None),
337 }
338 .insert(&db)
339 .await
340 .expect("newer test record insert should succeed");
341
342 let response = handle_get_inquiries(&db, &email, cognito_sub, "https://example.com")
343 .await
344 .expect("handle_get_inquiries should succeed");
345
346 assert_eq!(response.status_code, 200);
347 let response_body: serde_json::Value =
348 serde_json::from_str(&response.body).expect("response body should be valid JSON");
349 assert_eq!(response_body["email"], email);
350 assert_eq!(response_body["count"], 2);
351 let inquiries = response_body["inquiries"]
352 .as_array()
353 .expect("inquiries should be an array");
354 assert_eq!(inquiries.len(), 2);
355 assert_eq!(inquiries[0]["subject"], "newer subject");
356 assert_eq!(inquiries[1]["subject"], "older subject");
357 cleanup_test_inquiries(&db, &email).await;
358 }
359}