use axum::{ body::Body, http::{Request, StatusCode, header}, }; use frangipane::{ AppConfig, auth::{create_jwt, hash_password, verify_jwt_string, verify_password}, create_app, realtime, routes::{messages::Message, rooms::Room}, users::LoginResponse, }; use serde_json::json; use sha2::{Digest, Sha256}; use sqlx::PgPool; use std::sync::Arc; use tower::ServiceExt; use uuid::Uuid; // --- HELPERS --- async fn get_test_pool() -> sqlx::PgPool { let database_url = format!("postgres://frangipane:secret@127.0.0.1/frangipane"); PgPool::connect(database_url.as_str()).await.unwrap() } fn get_test_config() -> Arc { let mut path = std::env::current_dir().unwrap(); path.push("target"); path.push("test_uploads"); std::fs::create_dir_all(&path).ok(); Arc::new(AppConfig { avatar_dir: path.clone(), prohibit_registration: false, max_file_size: 1024 * 1024 * 10, uploads_dir: path, }) } async fn setup_app() -> (axum::Router, sqlx::PgPool) { let _ = tracing_subscriber::fmt() .with_env_filter("debug") .with_test_writer() .try_init(); let pool = get_test_pool().await; let app = create_app( pool.clone(), get_test_config(), realtime::RealtimeMessages::new(), realtime::RealTimeVoices::new(), false, ); (app, pool) } /// Helper to register a user and return credentials. async fn register_test_user(app: &mut axum::Router, username: &str, email: &str) -> (String, Uuid) { let payload = json!({ "username": username, "email": email, "password": "secure_password_123" }); let response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/register") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(serde_json::to_vec(&payload).unwrap())) .unwrap(), ) .await .unwrap(); let status = response.status(); let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); if !status.is_success() { panic!( "Registration failed for {} with status {}. Body: {:?}", username, status, String::from_utf8_lossy(&body_bytes) ); } let data: LoginResponse = serde_json::from_slice(&body_bytes).expect("Failed to parse LoginResponse"); (data.token, data.uuid) } // --- AUTH TESTS --- #[test] fn test_password_hashing_logic() { let pass = "hunter2_extra_safe"; let hash = hash_password(pass).unwrap(); assert!(verify_password(&hash, pass)); assert!(!verify_password(&hash, "wrong_pass")); } #[test] fn test_jwt_logic() { let id = Uuid::now_v7(); let token = create_jwt(id).unwrap(); let claims = verify_jwt_string(&token).unwrap(); assert_eq!(claims.sub, id); } // --- USER / REGISTRATION TESTS --- #[tokio::test] async fn test_api_register_success() { let (mut app, _) = setup_app().await; let username = format!("u{}", Uuid::now_v7().simple()); let email = format!("{}@example.com", username); let (token, _) = register_test_user(&mut app, &username, &email).await; assert!(!token.is_empty()); } #[tokio::test] async fn test_api_register_duplicate_username() { let (mut app, _) = setup_app().await; let name = format!("d{}", Uuid::now_v7().simple()); let email1 = format!("{}1@test.com", name); let email2 = format!("{}2@test.com", name); register_test_user(&mut app, &name, &email1).await; let payload = json!({ "username": name, "email": email2, "password": "password123" }); let res = app .oneshot( Request::builder() .method("POST") .uri("/register") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(serde_json::to_vec(&payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::CONFLICT); } #[tokio::test] async fn test_api_register_invalid_email() { let (app, _) = setup_app().await; let payload = json!({ "username": "bademail", "email": "not-an-email", "password": "password123" }); let res = app .oneshot( Request::builder() .method("POST") .uri("/register") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(serde_json::to_vec(&payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_api_login_success() { let (mut app, _) = setup_app().await; let id = Uuid::now_v7().simple(); let name = format!("l{}", id); let email = format!("{}@test.com", id); register_test_user(&mut app, &name, &email).await; let payload = json!({ "email": email, "password": "secure_password_123" }); let res = app .oneshot( Request::builder() .method("POST") .uri("/login") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(serde_json::to_vec(&payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); } // --- ROOM TESTS --- #[tokio::test] async fn test_create_room_and_list() { let (mut app, _) = setup_app().await; let id = Uuid::now_v7().simple(); let name = format!("o{}", id); let (token, _) = register_test_user(&mut app, &name, &format!("{}@t.com", id)).await; let room_name = "Testing Room"; let _ = app .clone() .oneshot( Request::builder() .method("POST") .uri("/rooms") .header(header::AUTHORIZATION, format!("Bearer {token}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"name": room_name, "global": false})).unwrap(), )) .unwrap(), ) .await .unwrap(); let res = app .oneshot( Request::builder() .method("GET") .uri("/rooms") .header(header::AUTHORIZATION, format!("Bearer {token}")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); let body = axum::body::to_bytes(res.into_body(), usize::MAX) .await .unwrap(); let rooms: Vec = serde_json::from_slice(&body).unwrap(); assert!(rooms.iter().any(|r| r.name == room_name)); } #[tokio::test] async fn test_delete_room_permission() { let (mut app, _) = setup_app().await; let id1 = Uuid::now_v7().simple(); let id2 = Uuid::now_v7().simple(); let (t1, _) = register_test_user(&mut app, &format!("u1{}", id1), &format!("{}@t.com", id1)).await; let (t2, _) = register_test_user(&mut app, &format!("u2{}", id2), &format!("{}@t.com", id2)).await; // u1 creates room let res = app .clone() .oneshot( Request::builder() .method("POST") .uri("/rooms") .header(header::AUTHORIZATION, format!("Bearer {t1}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"name": "KillMe", "global": false})).unwrap(), )) .unwrap(), ) .await .unwrap(); let room: Room = serde_json::from_slice( &axum::body::to_bytes(res.into_body(), usize::MAX) .await .unwrap(), ) .unwrap(); // u2 tries to delete it let res = app .oneshot( Request::builder() .method("DELETE") .uri(format!("/rooms/{}/delete", room.uuid)) .header(header::AUTHORIZATION, format!("Bearer {t2}")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::FORBIDDEN); } // --- FRIEND TESTS --- #[tokio::test] async fn test_friend_request_self_error() { let (mut app, _) = setup_app().await; let id = Uuid::now_v7().simple(); let name = format!("s{}", id); let (token, _) = register_test_user(&mut app, &name, &format!("{}@t.com", id)).await; let res = app .oneshot( Request::builder() .method("POST") .uri("/friends/request") .header(header::AUTHORIZATION, format!("Bearer {token}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"receiver_username": name})).unwrap(), )) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_accept_friend_request() { let (mut app, _) = setup_app().await; let id1 = Uuid::now_v7().simple(); let id2 = Uuid::now_v7().simple(); let n1 = format!("f1{}", id1); let n2 = format!("f2{}", id2); let (t1, u1_uuid) = register_test_user(&mut app, &n1, &format!("{}@t.com", id1)).await; let (t2, _) = register_test_user(&mut app, &n2, &format!("{}@t.com", id2)).await; // t1 sends to t2 let _ = app .clone() .oneshot( Request::builder() .method("POST") .uri("/friends/request") .header(header::AUTHORIZATION, format!("Bearer {t1}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"receiver_username": n2})).unwrap(), )) .unwrap(), ) .await .unwrap(); // t2 accepts let res = app .oneshot( Request::builder() .method("POST") .uri("/friends/accept") .header(header::AUTHORIZATION, format!("Bearer {t2}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"sender_uuid": u1_uuid})).unwrap(), )) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::CREATED); } // --- MESSAGE TESTS --- #[tokio::test] async fn test_send_message_not_member() { let (mut app, _) = setup_app().await; let id1 = Uuid::now_v7().simple(); let id2 = Uuid::now_v7().simple(); let n1 = format!("ma{}", id1); let n2 = format!("mb{}", id2); let (t1, _) = register_test_user(&mut app, &n1, &format!("{}@t.com", id1)).await; let (t2, _) = register_test_user(&mut app, &n2, &format!("{}@t.com", id2)).await; // t1 creates room let res = app .clone() .oneshot( Request::builder() .method("POST") .uri("/rooms") .header(header::AUTHORIZATION, format!("Bearer {t1}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"name": "Private", "global": false})).unwrap(), )) .unwrap(), ) .await .unwrap(); let room: Room = serde_json::from_slice( &axum::body::to_bytes(res.into_body(), usize::MAX) .await .unwrap(), ) .unwrap(); // t2 (non-member) tries to send let res = app .oneshot( Request::builder() .method("POST") .uri(format!("/messages/{}", room.uuid)) .header(header::AUTHORIZATION, format!("Bearer {t2}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"message_type": "text", "content": "spy"})).unwrap(), )) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::FORBIDDEN); } #[tokio::test] async fn test_send_and_list_messages() { let (mut app, _) = setup_app().await; let id = Uuid::now_v7().simple(); let n = format!("m{}", id); let (token, _) = register_test_user(&mut app, &n, &format!("{}@t.com", id)).await; let res = app .clone() .oneshot( Request::builder() .method("POST") .uri("/rooms") .header(header::AUTHORIZATION, format!("Bearer {token}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"name": "Chat", "global": false})).unwrap(), )) .unwrap(), ) .await .unwrap(); let room: Room = serde_json::from_slice( &axum::body::to_bytes(res.into_body(), usize::MAX) .await .unwrap(), ) .unwrap(); // Send let _ = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/messages/{}", room.uuid)) .header(header::AUTHORIZATION, format!("Bearer {token}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({"message_type": "text", "content": "hello"})) .unwrap(), )) .unwrap(), ) .await .unwrap(); // List let res = app .oneshot( Request::builder() .method("GET") .uri(format!("/messages/{}", room.uuid)) .header(header::AUTHORIZATION, format!("Bearer {token}")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let messages: Vec = serde_json::from_slice( &axum::body::to_bytes(res.into_body(), usize::MAX) .await .unwrap(), ) .unwrap(); assert_eq!(messages.len(), 1); } // --- MISC / SYSTEM TESTS --- #[tokio::test] async fn test_update_settings_and_validate() { let (mut app, _) = setup_app().await; let id = Uuid::now_v7().simple(); let n = format!("st{}", id); let (token, _) = register_test_user(&mut app, &n, &format!("{}@t.com", id)).await; let new_id = Uuid::now_v7().simple(); let new_name = format!("nw{}", new_id); let new_email = format!("{}@new.com", new_id); let res = app .clone() .oneshot( Request::builder() .method("PUT") .uri("/account/settings") .header(header::AUTHORIZATION, format!("Bearer {token}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&json!({ "username": new_name, "email": new_email, "password": "new_secure_pass_123" })) .unwrap(), )) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::CREATED); let res = app .oneshot( Request::builder() .method("GET") .uri("/validate-token") .header(header::AUTHORIZATION, format!("Bearer {token}")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn test_version_route() { let (app, _) = setup_app().await; let res = app .oneshot( Request::builder() .uri("/version") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn test_unauthorized_access() { let (app, _) = setup_app().await; let paths = vec![ ("GET", "/rooms", Body::empty()), ("GET", "/friends", Body::empty()), ("GET", "/friends/requests", Body::empty()), ( "PUT", "/account/settings", Body::from( json!({ "username": "a", "email": "a@a.com", "password": "p" }) .to_string(), ), ), ]; for (method, path, body) in paths { let res = app .clone() .oneshot( Request::builder() .method(method) .uri(path) .header("content-type", "application/json") .body(body) .unwrap(), ) .await .unwrap(); assert_eq!( res.status(), StatusCode::UNAUTHORIZED, "Path {} should be protected", path ); } } #[tokio::test] async fn test_upload_large_file_and_verify_hash() { let (mut app, _) = setup_app().await; let id = Uuid::now_v7().simple(); let username = format!("u{}", id); let (token, _) = register_test_user(&mut app, &username, &format!("{}@t.com", id)).await; // Create a room to upload into let res = app .clone() .oneshot( Request::builder() .method("POST") .uri("/rooms") .header(header::AUTHORIZATION, format!("Bearer {token}")) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( json!({"name": "UploadRoom", "global": false}).to_string(), )) .unwrap(), ) .await .unwrap(); let room: Room = serde_json::from_slice( &axum::body::to_bytes(res.into_body(), usize::MAX) .await .unwrap(), ) .unwrap(); // Prepare file let file_size = 1024 * 1024; let file_bytes: Vec = (0..file_size).map(|i| (i % 255) as u8).collect(); let mut hasher = Sha256::new(); hasher.update(&file_bytes); let original_hash = hasher.finalize(); let boundary = "boundary123"; let mut body_content = Vec::new(); // First boundary body_content.extend_from_slice(format!("--{boundary}\r\n").as_bytes()); body_content.extend_from_slice(b"Content-Disposition: form-data; name=\"content\"\r\n\r\n"); body_content.extend_from_slice(b"Check out this file"); body_content.extend_from_slice(b"\r\n"); // CRLF denoting end of field data body_content.extend_from_slice(format!("--{boundary}\r\n").as_bytes()); body_content.extend_from_slice( b"Content-Disposition: form-data; name=\"file\"; filename=\"test_file.bin\"\r\n", ); body_content.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n"); body_content.extend_from_slice(&file_bytes); body_content.extend_from_slice(b"\r\n"); // CRLF denoting end of field data // Final boundary body_content.extend_from_slice(format!("--{boundary}--\r\n").as_bytes()); let upload_res = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/messages/{}/upload", room.uuid)) .header(header::AUTHORIZATION, format!("Bearer {token}")) .header( header::CONTENT_TYPE, format!("multipart/form-data; boundary={boundary}"), ) .body(Body::from(body_content)) .unwrap(), ) .await .unwrap(); assert_eq!(upload_res.status(), StatusCode::CREATED); let message_resp: Message = serde_json::from_slice( &axum::body::to_bytes(upload_res.into_body(), usize::MAX) .await .unwrap(), ) .unwrap(); let file_uuid = message_resp.attachments[0].uuid; // Download the file back let download_res = app .oneshot( Request::builder() .method("GET") .uri(format!("/uploads/{}", file_uuid)) .header(header::AUTHORIZATION, format!("Bearer {token}")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(download_res.status(), StatusCode::OK); let downloaded_bytes = axum::body::to_bytes(download_res.into_body(), usize::MAX) .await .unwrap(); // Verify integrity let mut hasher = Sha256::new(); hasher.update(&downloaded_bytes); let downloaded_hash = hasher.finalize(); assert_eq!( file_bytes.len(), downloaded_bytes.len(), "File sizes must match" ); assert_eq!(original_hash, downloaded_hash, "File hashes must match"); }