full backend rewrite.
calling this v0.4.0
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
use backend::rocket_builder;
|
||||
use backend::repo::mock::{MockUserRepo, MockTokenRepo};
|
||||
use backend::repo::message_repo::MessageRepository;
|
||||
use backend::svc::chat_svc::ChatService;
|
||||
use backend::repo::user_repo::UserRepository;
|
||||
use backend::repo::{Repo, AccessTokenRepoTrait};
|
||||
use rocket::local::asynchronous::Client;
|
||||
use rocket::http::{Status, ContentType};
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use sqlx::PgPool;
|
||||
use chrono::Utc;
|
||||
use backend::svc::llm_service::LlmService;
|
||||
|
||||
async fn test_rocket() -> rocket::Rocket<rocket::Build> {
|
||||
let users = Arc::new(MockUserRepo { users: Mutex::new(vec![]) });
|
||||
let tokens = Arc::new(MockTokenRepo { tokens: Mutex::new(vec![]) });
|
||||
|
||||
let pool = PgPool::connect_lazy("postgres://localhost/unused").unwrap();
|
||||
let messages = MessageRepository::new(pool.clone());
|
||||
let user_repo = Arc::new(UserRepository::new(pool));
|
||||
let llm_service = LlmService::new();
|
||||
let chat_service = ChatService::new(32, messages, user_repo, llm_service);
|
||||
|
||||
rocket_builder(users, tokens, chat_service)
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_unauthorized_access() {
|
||||
let client = Client::tracked(test_rocket().await).await.expect("valid rocket instance");
|
||||
|
||||
// Attempt to access a protected endpoint without authentication
|
||||
let response = client.patch("/api/settings/display_name").dispatch().await;
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
|
||||
let response = client.post("/api/settings/password").dispatch().await;
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
|
||||
let response = client.delete("/api/settings").dispatch().await;
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_signup_invalid_token() {
|
||||
let client = Client::tracked(test_rocket().await).await.expect("valid rocket instance");
|
||||
|
||||
let signup_data = json!({
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"access_token": "invalid-token"
|
||||
});
|
||||
|
||||
let response = client.post("/api/signup")
|
||||
.header(ContentType::JSON)
|
||||
.body(signup_data.to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_login_invalid_credentials() {
|
||||
let client = Client::tracked(test_rocket().await).await.expect("valid rocket instance");
|
||||
|
||||
let login_data = json!({
|
||||
"username": "nonexistent",
|
||||
"password": "wrongpassword"
|
||||
});
|
||||
|
||||
let response = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(login_data.to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_full_auth_flow() {
|
||||
let users = Arc::new(MockUserRepo { users: Mutex::new(vec![]) });
|
||||
let tokens = Arc::new(MockTokenRepo { tokens: Mutex::new(vec![]) });
|
||||
let pool = PgPool::connect_lazy("postgres://localhost/unused").unwrap();
|
||||
let messages = MessageRepository::new(pool.clone());
|
||||
let user_repo = Arc::new(UserRepository::new(pool));
|
||||
let llm_service = LlmService::new();
|
||||
let chat_service = ChatService::new(32, messages, user_repo, llm_service);
|
||||
|
||||
let token_code = "valid-token";
|
||||
tokens.create_new(1, "test", token_code, 1, Utc::now(), Utc::now()).await.unwrap();
|
||||
|
||||
let client = Client::tracked(rocket_builder(users, tokens, chat_service)).await.expect("valid rocket instance");
|
||||
|
||||
// 1. Signup
|
||||
let signup_data = json!({
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"access_token": token_code
|
||||
});
|
||||
|
||||
let response = client.post("/api/signup")
|
||||
.header(ContentType::JSON)
|
||||
.body(signup_data.to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let body = response.into_string().await.unwrap();
|
||||
assert!(body.contains("token"));
|
||||
|
||||
// 2. Login
|
||||
let login_data = json!({
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
});
|
||||
|
||||
let response = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(login_data.to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let body = response.into_string().await.unwrap();
|
||||
assert!(body.contains("token"));
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_delete_account_security() {
|
||||
let users = Arc::new(MockUserRepo { users: Mutex::new(vec![]) });
|
||||
let tokens = Arc::new(MockTokenRepo { tokens: Mutex::new(vec![]) });
|
||||
let pool = PgPool::connect_lazy("postgres://localhost/unused").unwrap();
|
||||
let messages = MessageRepository::new(pool.clone());
|
||||
let user_repo = Arc::new(UserRepository::new(pool));
|
||||
let llm_service = LlmService::new();
|
||||
let chat_service = ChatService::new(32, messages, user_repo, llm_service);
|
||||
|
||||
let client = Client::tracked(rocket_builder(users.clone(), tokens.clone(), chat_service)).await.expect("valid rocket instance");
|
||||
|
||||
let token_code = "valid-token";
|
||||
tokens.create_new(1, "test", token_code, 1, Utc::now(), Utc::now()).await.unwrap();
|
||||
|
||||
client.post("/api/signup")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"access_token": token_code
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
// Login to get JWT
|
||||
let login_res = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
let auth_resp: serde_json::Value = serde_json::from_str(&login_res.into_string().await.unwrap()).unwrap();
|
||||
let jwt = auth_resp["token"].as_str().unwrap();
|
||||
|
||||
// 1. Delete with WRONG password
|
||||
let response = client.delete("/api/settings")
|
||||
.header(ContentType::JSON)
|
||||
.header(rocket::http::Header::new("Authorization", format!("Bearer {}", jwt)))
|
||||
.body(json!({
|
||||
"password": "wrongpassword",
|
||||
"totp_code": null
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
|
||||
// 2. Delete with CORRECT password
|
||||
let response = client.delete("/api/settings")
|
||||
.header(ContentType::JSON)
|
||||
.header(rocket::http::Header::new("Authorization", format!("Bearer {}", jwt)))
|
||||
.body(json!({
|
||||
"password": "password123",
|
||||
"totp_code": null
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
// Verify user is gone
|
||||
assert!(users.users.lock().unwrap().is_empty());
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use backend::rocket_builder;
|
||||
use backend::repo::mock::{MockUserRepo, MockTokenRepo};
|
||||
use backend::repo::message_repo::MessageRepository;
|
||||
use backend::svc::chat_svc::ChatService;
|
||||
use backend::repo::{Repo, AccessTokenRepoTrait};
|
||||
use rocket::local::asynchronous::Client;
|
||||
use rocket::http::{Status, ContentType, Header};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use sqlx::PgPool;
|
||||
use chrono::Utc;
|
||||
use backend::svc::llm_service::LlmService;
|
||||
|
||||
async fn setup_client_with_svc(chat_service: ChatService, users: Arc<MockUserRepo>, tokens: Arc<MockTokenRepo>) -> (Client, String) {
|
||||
let client = Client::tracked(rocket_builder(users.clone(), tokens.clone(), chat_service)).await.expect("valid rocket instance");
|
||||
|
||||
// Create a user and get JWT
|
||||
let token_code = "valid-token";
|
||||
tokens.create_new(1, "test", token_code, 1, Utc::now(), Utc::now()).await.unwrap();
|
||||
|
||||
let jwt = {
|
||||
let signup_res = client.post("/api/signup")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"access_token": token_code
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(signup_res.status(), Status::Ok);
|
||||
|
||||
let login_res = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(login_res.status(), Status::Ok, "Login failed");
|
||||
|
||||
let body = login_res.into_string().await.expect("login body");
|
||||
let auth_resp: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||
auth_resp["token"].as_str().unwrap().to_string()
|
||||
};
|
||||
|
||||
(client, jwt)
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_chat_event_stream_consistency() {
|
||||
unsafe { std::env::set_var("JWT_SECRET", "test_secret"); }
|
||||
let pool = PgPool::connect_lazy("postgres://localhost/unused").unwrap();
|
||||
let messages = <MessageRepository as Repo>::new(pool.clone());
|
||||
let users_repo = Arc::new(MockUserRepo { users: Mutex::new(vec![]) });
|
||||
let tokens_repo = Arc::new(MockTokenRepo { tokens: Mutex::new(vec![]) });
|
||||
let llm_service = LlmService::new();
|
||||
let chat_service = ChatService::new(1024, messages, users_repo.clone(), llm_service);
|
||||
|
||||
let (client, jwt) = setup_client_with_svc(chat_service.clone(), users_repo.clone(), tokens_repo.clone()).await;
|
||||
|
||||
// Use the same client for sender but with a different user (or the same, doesn't matter for broadcast)
|
||||
// Actually, to simulate another user, we should sign up another user.
|
||||
let jwt_sender = {
|
||||
let token_code = "valid-token-2";
|
||||
tokens_repo.create_new(1, "test2", token_code, 1, Utc::now(), Utc::now() + chrono::Duration::days(1)).await.unwrap();
|
||||
let signup_res = client.post("/api/signup")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"email": "test2@example.com",
|
||||
"username": "testuser2",
|
||||
"password": "password123",
|
||||
"access_token": token_code
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(signup_res.status(), Status::Ok);
|
||||
let login_res = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"username": "testuser2",
|
||||
"password": "password123"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
let body = login_res.into_string().await.unwrap();
|
||||
let auth_resp: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||
auth_resp["token"].as_str().unwrap().to_string()
|
||||
};
|
||||
|
||||
let channel_id = 1;
|
||||
|
||||
// Start listening to the event stream
|
||||
let mut response = client.get(format!("/api/events/{}", channel_id))
|
||||
.header(Header::new("Authorization", format!("Bearer {}", jwt)))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let num_messages = 5; // Reduced for faster debugging
|
||||
let mut received_count = 0;
|
||||
|
||||
let jwt_clone = jwt.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
for i in 0..num_messages {
|
||||
let msg = format!("Message {}", i);
|
||||
let res = sender_client.post(format!("/api/chat/{}", channel_id))
|
||||
.header(ContentType::JSON)
|
||||
.header(Header::new("Authorization", format!("Bearer {}", jwt_clone)))
|
||||
.body(json!({
|
||||
"display_name": "testuser",
|
||||
"user_id": 1,
|
||||
"text": msg,
|
||||
"timestamp": Utc::now()
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(res.status(), Status::Ok);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Wait a bit for messages to be posted
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Consume the stream
|
||||
let text = response.into_string().await.unwrap();
|
||||
println!("Received chunk: {}", text);
|
||||
let mut received_count = 0;
|
||||
for line in text.lines() {
|
||||
if line.starts_with("data:") {
|
||||
received_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(received_count, num_messages, "Should receive all posted messages. Received: {}. Full text: {}", received_count, text);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use backend::rocket_builder;
|
||||
use backend::repo::mock::{MockUserRepo, MockTokenRepo};
|
||||
use backend::repo::message_repo::MessageRepository;
|
||||
use backend::svc::chat_svc::ChatService;
|
||||
use backend::repo::user_repo::UserRepository;
|
||||
use backend::repo::{Repo, AccessTokenRepoTrait};
|
||||
use rocket::local::asynchronous::Client;
|
||||
use rocket::http::{Status, ContentType, Header};
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use sqlx::PgPool;
|
||||
use chrono::Utc;
|
||||
use backend::svc::llm_service::LlmService;
|
||||
|
||||
async fn setup_client() -> (Client, Arc<MockUserRepo>, String) {
|
||||
let users = Arc::new(MockUserRepo { users: Mutex::new(vec![]) });
|
||||
let tokens = Arc::new(MockTokenRepo { tokens: Mutex::new(vec![]) });
|
||||
let pool = PgPool::connect_lazy("postgres://localhost/unused").unwrap();
|
||||
let messages = MessageRepository::new(pool.clone());
|
||||
let user_repo = Arc::new(UserRepository::new(pool));
|
||||
let llm_service = LlmService::new();
|
||||
let chat_service = ChatService::new(32, messages, user_repo, llm_service);
|
||||
|
||||
let client = Client::tracked(rocket_builder(users.clone(), tokens.clone(), chat_service)).await.expect("valid rocket instance");
|
||||
|
||||
// Create a user and get JWT
|
||||
let token_code = "valid-token";
|
||||
tokens.create_new(1, "test", token_code, 1, Utc::now(), Utc::now()).await.unwrap();
|
||||
|
||||
client.post("/api/signup")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"access_token": token_code
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
let login_res = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
let auth_resp: serde_json::Value = serde_json::from_str(&login_res.into_string().await.unwrap()).unwrap();
|
||||
let jwt = auth_resp["token"].as_str().unwrap().to_string();
|
||||
|
||||
(client, users, jwt)
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_change_display_name() {
|
||||
let (client, users, jwt) = setup_client().await;
|
||||
|
||||
let response = client.patch("/api/settings/display_name")
|
||||
.header(ContentType::JSON)
|
||||
.header(Header::new("Authorization", format!("Bearer {}", jwt)))
|
||||
.body(json!({
|
||||
"display_name": "New Display Name"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let user = users.users.lock().unwrap()[0].clone();
|
||||
assert_eq!(user.nickname, Some("New Display Name".to_string()));
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_change_username() {
|
||||
let (client, users, jwt) = setup_client().await;
|
||||
|
||||
let response = client.patch("/api/settings/username")
|
||||
.header(ContentType::JSON)
|
||||
.header(Header::new("Authorization", format!("Bearer {}", jwt)))
|
||||
.body(json!({
|
||||
"username": "newusername"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let user = users.users.lock().unwrap()[0].clone();
|
||||
assert_eq!(user.username, "newusername");
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_change_password() {
|
||||
let (client, _, jwt) = setup_client().await;
|
||||
|
||||
let response = client.post("/api/settings/password")
|
||||
.header(ContentType::JSON)
|
||||
.header(Header::new("Authorization", format!("Bearer {}", jwt)))
|
||||
.body(json!({
|
||||
"old_password": "password123",
|
||||
"new_password": "newpassword456"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
// Verify login with new password
|
||||
let login_res = client.post("/api/login")
|
||||
.header(ContentType::JSON)
|
||||
.body(json!({
|
||||
"username": "testuser",
|
||||
"password": "newpassword456"
|
||||
}).to_string())
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(login_res.status(), Status::Ok);
|
||||
}
|
||||
Reference in New Issue
Block a user