Files
frangipane-backend/tests/tests.rs
T

721 lines
21 KiB
Rust

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<AppConfig> {
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<Room> = 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<Message> = 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<u8> = (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");
}