1#[cfg(feature = "openapi")]
26use crate::models::ErrorResponseBody;
27use crate::models::{
28 CreateMessageRequest, CreateMessageResponse, CreatedMessage, Message, MessageListResponse,
29 Response,
30};
31use lambda_runtime::Error;
32use sea_orm::{
33 ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, DatabaseConnection,
34 EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set, Statement,
35};
36use sea_orm_entities::entity::messages::{self, Column, Entity as Messages};
37
38#[cfg_attr(
39 feature = "openapi",
40 utoipa::path(
41 get,
42 path = "/messages",
43 tag = "messages",
44 responses(
45 (status = 200, description = "Get messages", body = MessageListResponse),
46 (status = 401, description = "Unauthorized", body = ErrorResponseBody),
47 (status = 500, description = "Internal server error", body = ErrorResponseBody),
48 ),
49 security(
50 ("CognitoAuthorizer" = []),
51 ("BearerAuth" = []),
52 )
53 )
54)]
55pub(crate) async fn handle_get_messages(
86 db: &DatabaseConnection,
87 email: &str,
88 cognito_id: uuid::Uuid,
89 cors_origin: &str,
90) -> Result<Response, Error> {
91 tracing::info!("Querying messages for email: {}", email);
92
93 let messages: Vec<Message> = Messages::find()
94 .filter(Column::CognitoId.eq(cognito_id))
95 .select_only()
96 .column(Column::CognitoId)
97 .column(Column::IsFromUser)
98 .column(Column::Body)
99 .column(Column::CreatedAt)
100 .order_by_desc(Column::CreatedAt)
101 .into_model::<Message>()
102 .all(db)
103 .await
104 .map_err(|e| {
105 tracing::error!("Database query failed: {}", e);
106 anyhow::anyhow!("Database query failed: {}", e)
107 })?;
108
109 let response_body = MessageListResponse {
110 email: email.to_string(),
111 count: messages.len() as u64,
112 messages,
113 };
114
115 Ok(Response::new(
116 200,
117 serde_json::to_value(response_body)?,
118 cors_origin,
119 ))
120}
121
122#[cfg_attr(
123 feature = "openapi",
124 utoipa::path(
125 post,
126 path = "/message/new",
127 tag = "messages",
128 request_body = CreateMessageRequest,
129 responses(
130 (status = 201, description = "Create message", body = CreateMessageResponse),
131 (status = 401, description = "Unauthorized", body = ErrorResponseBody),
132 (status = 500, description = "Internal server error", body = ErrorResponseBody),
133 ),
134 security(
135 ("CognitoAuthorizer" = []),
136 ("BearerAuth" = []),
137 )
138 )
139)]
140pub(crate) async fn handle_post_message_new(
179 db: &DatabaseConnection,
180 cognito_id: uuid::Uuid,
181 body: &str,
182 row_log: &str,
183 cors_origin: &str,
184) -> Result<Response, Error> {
185 tracing::info!("Creating message for cognito_id: {}", cognito_id);
186
187 let create_request: CreateMessageRequest = match serde_json::from_str(body) {
188 Ok(req) => req,
189 Err(e) => {
190 tracing::error!("Failed to parse request body: {}", e);
191 return Ok(Response::error(
192 400,
193 "Bad Request",
194 &format!("Invalid request body: {}", e),
195 cors_origin,
196 ));
197 }
198 };
199
200 let id = uuid::Uuid::now_v7();
201 let now = Some(chrono::Utc::now().fixed_offset());
202
203 let new_message = messages::ActiveModel {
204 id: Set(id),
205 cognito_id: Set(cognito_id),
206 created_at: Set(now),
207 body: Set(create_request.body.clone()),
208 row_log: Set(row_log.to_string()),
209 is_from_user: Set(true),
210 };
211
212 new_message.insert(db).await.map_err(|e| {
213 tracing::error!("Failed to insert message: {}", e);
214 anyhow::anyhow!("Failed to insert message: {}", e)
215 })?;
216
217 let message = CreatedMessage {
218 id,
219 cognito_id,
220 is_from_user: true,
221 body: create_request.body,
222 created_at: now,
223 };
224
225 let response_body = CreateMessageResponse { message };
226
227 Ok(Response::new(
228 201,
229 serde_json::to_value(response_body)?,
230 cors_origin,
231 ))
232}
233
234#[cfg_attr(
235 feature = "openapi",
236 utoipa::path(
237 get,
238 path = "/health",
239 tag = "health",
240 responses(
241 (status = 200, description = "Health check OK", body = String, content_type = "text/plain"),
242 (status = 500, description = "Internal server error", body = ErrorResponseBody),
243 )
244 )
245)]
246pub(crate) async fn handle_get_health(
247 db: &DatabaseConnection,
248 cors_origin: &str,
249) -> Result<Response, Error> {
250 let result = db
251 .query_one(Statement::from_string(
252 DatabaseBackend::Postgres,
253 "SELECT 'OK' AS status",
254 ))
255 .await
256 .map_err(|e| anyhow::anyhow!("Health check query failed: {}", e))?
257 .ok_or_else(|| anyhow::anyhow!("Health check query returned no rows"))?;
258
259 let status: String = result
260 .try_get("", "status")
261 .map_err(|e| anyhow::anyhow!("Failed to read health check result: {}", e))?;
262
263 Ok(Response::text(200, status, cors_origin))
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use chrono::Duration;
270 use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
271 use sea_orm_entities::entity::messages::{Column, Entity as Messages};
272
273 const ORDERING_OFFSET_SECS: i64 = 1;
274
275 async fn connect_local_test_db() -> DatabaseConnection {
276 let database_url = std::env::var("LOCAL_TEST_DATABASE_URL").unwrap_or_else(|_| {
277 "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable".to_string()
278 });
279 Database::connect(database_url)
280 .await
281 .expect("Failed to connect local PostgreSQL test DB")
282 }
283
284 async fn cleanup_test_messages(db: &DatabaseConnection, cognito_id: uuid::Uuid) {
285 Messages::delete_many()
286 .filter(Column::CognitoId.eq(cognito_id))
287 .exec(db)
288 .await
289 .expect("test message cleanup should succeed");
290 }
291
292 #[tokio::test]
293 #[ignore = "ローカルのDBが必要なためデフォルトでは実行しない"]
294 async fn test_handle_post_message_new_with_local_postgres() {
295 let db = connect_local_test_db().await;
296 let cognito_id = uuid::Uuid::now_v7();
297 let body = r#"{"body":"body from test"}"#;
298 let row_log = r#"{"rawPath":"/message/new","body":"{\"body\":\"body from test\"}"}"#;
299 cleanup_test_messages(&db, cognito_id).await;
300
301 let db_for_test = db.clone();
302 let test_result = tokio::spawn(async move {
303 let response = handle_post_message_new(
304 &db_for_test,
305 cognito_id,
306 body,
307 row_log,
308 "https://example.com",
309 )
310 .await
311 .expect("handle_post_message_new should succeed");
312
313 assert_eq!(response.status_code, 201);
314 let response_body: serde_json::Value =
315 serde_json::from_str(&response.body).expect("response body should be valid JSON");
316 assert_eq!(
317 response_body["message"]["cognito_id"],
318 cognito_id.to_string()
319 );
320 assert_eq!(response_body["message"]["is_from_user"], true);
321 assert_eq!(response_body["message"]["body"], "body from test");
322
323 let inquiry_id = uuid::Uuid::parse_str(
324 response_body["message"]["id"]
325 .as_str()
326 .expect("response should include message id"),
327 )
328 .expect("message id should be valid UUID");
329 let saved = Messages::find_by_id(inquiry_id)
330 .one(&db_for_test)
331 .await
332 .expect("DB query should succeed")
333 .expect("inserted message should exist");
334 assert_eq!(saved.cognito_id, cognito_id);
335 assert_eq!(saved.row_log, row_log);
336 })
337 .await;
338
339 cleanup_test_messages(&db, cognito_id).await;
340
341 if let Err(err) = test_result {
342 if err.is_panic() {
343 std::panic::resume_unwind(err.into_panic());
344 }
345 panic!("test task failed: {err}");
346 }
347 }
348
349 #[tokio::test]
350 #[ignore = "ローカルのDBが必要なためデフォルトでは実行しない"]
351 async fn test_handle_get_messages_with_local_postgres() {
352 let db = connect_local_test_db().await;
353 let cognito_id = uuid::Uuid::now_v7();
354 let email = format!("local-get-{}@example.com", uuid::Uuid::now_v7());
355 let now = chrono::Utc::now().fixed_offset();
356 cleanup_test_messages(&db, cognito_id).await;
357
358 messages::ActiveModel {
359 id: Set(uuid::Uuid::now_v7()),
360 cognito_id: Set(cognito_id),
361 created_at: Set(Some(now - Duration::seconds(ORDERING_OFFSET_SECS))),
362 body: Set("older body".to_string()),
363 row_log: Set("older row log".to_string()),
364 is_from_user: Set(true),
365 }
366 .insert(&db)
367 .await
368 .expect("older test message insert should succeed");
369
370 messages::ActiveModel {
371 id: Set(uuid::Uuid::now_v7()),
372 cognito_id: Set(cognito_id),
373 created_at: Set(Some(now)),
374 body: Set("newer body".to_string()),
375 row_log: Set("newer row log".to_string()),
376 is_from_user: Set(false),
377 }
378 .insert(&db)
379 .await
380 .expect("newer test message insert should succeed");
381
382 let response = handle_get_messages(&db, &email, cognito_id, "https://example.com")
383 .await
384 .expect("handle_get_messages should succeed");
385
386 assert_eq!(response.status_code, 200);
387 let response_body: serde_json::Value =
388 serde_json::from_str(&response.body).expect("response body should be valid JSON");
389 assert_eq!(response_body["email"], email);
390 assert_eq!(response_body["count"], 2);
391 let messages = response_body["messages"]
392 .as_array()
393 .expect("messages should be an array");
394 assert_eq!(messages.len(), 2);
395 assert_eq!(messages[0]["body"], "newer body");
396 assert_eq!(messages[0]["is_from_user"], false);
397 assert!(messages[0].get("row_log").is_none());
398 assert!(messages[0].get("id").is_none());
399 assert_eq!(messages[1]["body"], "older body");
400 cleanup_test_messages(&db, cognito_id).await;
401 }
402}