refactor: integration-tests and format whole project with cargo fmt

This commit is contained in:
Mika 2025-06-20 11:12:38 +02:00
parent d477a5f286
commit 9aa9b49318
18 changed files with 665 additions and 94 deletions

View file

@ -1,4 +1,4 @@
use actix_web::{delete, get, post, put, Responder};
use actix_web::{Responder, delete, get, post, put};
pub fn setup(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_classes)

View file

@ -1,4 +1,4 @@
use actix_web::{delete, get, post, put, Responder};
use actix_web::{Responder, delete, get, post, put};
pub fn setup(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_groups)

View file

@ -1,10 +1,10 @@
use actix_web::{delete, get, post, put, web, Result};
use actix_web::{Result, delete, get, post, put, web};
use uuid::Uuid;
use validator::Validate;
use crate::db::project::CreateProject;
use crate::db::Database;
use crate::db::entity;
use crate::db::project::CreateProject;
use crate::error::ApiError;
pub fn setup(cfg: &mut actix_web::web::ServiceConfig) {

View file

@ -1,4 +1,4 @@
use actix_web::{delete, get, post, put, Responder};
use actix_web::{Responder, delete, get, post, put};
pub fn setup(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_templates)

View file

@ -6,9 +6,9 @@ use crate::db::entity::project;
use sea_orm::ActiveValue::{NotSet, Set, Unchanged};
use sea_orm::{ActiveModelTrait, DeleteResult, EntityTrait};
use serde::Deserialize;
use utoipa::ToSchema;
use uuid::Uuid;
use validator::Validate;
use utoipa::ToSchema;
#[derive(Deserialize, Validate, ToSchema)]
pub struct CreateProject {

View file

@ -66,4 +66,3 @@ impl MessageResponse {
}
}
}

View file

@ -1,8 +1,8 @@
pub mod controller;
pub mod db;
pub mod error;
pub mod utoipa;
pub mod utils;
pub mod utoipa;
pub use db::Database;
pub use db::entity;

View file

@ -7,8 +7,8 @@ use utoipa_swagger_ui::SwaggerUi;
mod controller;
mod db;
mod error;
mod utoipa;
mod utils;
mod utoipa;
use db::Database;
use log::info;
@ -61,10 +61,10 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default())
.wrap(session_middleware)
.service(web::scope("/api/v1").configure(controller::register_controllers))
.service(
SwaggerUi::new("/swagger-ui/{_:.*}")
.url("/api-docs/openapi.json", crate::utoipa::ApiDoc::openapi_spec()),
);
.service(SwaggerUi::new("/swagger-ui/{_:.*}").url(
"/api-docs/openapi.json",
crate::utoipa::ApiDoc::openapi_spec(),
));
#[cfg(feature = "serve")]
let app = {
@ -81,7 +81,6 @@ async fn main() -> std::io::Result<()> {
.await
}
pub async fn connect_to_redis_database() -> RedisSessionStore {
let redis_host = get_env_var("REDIS_HOST").expect("REDIS_HOST must be set in .env");
let redis_port = get_env_var("REDIS_PORT")

View file

@ -0,0 +1,121 @@
use crate::common::test_helpers::TestContext;
use backend::{Database, db::entity};
use uuid::Uuid;
impl TestContext {
pub async fn create_user_with_auth(
&self,
db: &Database,
username: Option<String>,
name: Option<String>,
password: String,
) -> Result<entity::user::Model, backend::error::ApiError> {
self.create_user_with_password(db, username, name, password)
.await
}
pub async fn verify_user_login(
&self,
db: &Database,
username: &str,
password: &str,
) -> Result<Uuid, backend::error::ApiError> {
db.verify_local_user(username, password).await
}
pub async fn create_authenticated_user(
&self,
db: &Database,
username: Option<String>,
password: Option<String>,
) -> Result<(entity::user::Model, String), backend::error::ApiError> {
let test_id = &self.test_id;
let username = username.unwrap_or_else(|| format!("auth_user_{}", test_id));
let password = password.unwrap_or_else(|| "test_password_123".to_string());
let user = self
.create_user_with_password(db, Some(username.clone()), None, password.clone())
.await?;
Ok((user, password))
}
pub async fn create_multiple_authenticated_users(
&self,
db: &Database,
count: usize,
) -> Result<Vec<(entity::user::Model, String)>, backend::error::ApiError> {
let mut users = Vec::new();
for i in 0..count {
let username = format!("auth_user_{}_{}", self.test_id, i);
let password = format!("password_{}", i);
let user_data = self
.create_authenticated_user(db, Some(username), Some(password.clone()))
.await?;
users.push(user_data);
}
Ok(users)
}
pub async fn assert_user_can_login(
&self,
db: &Database,
username: &str,
password: &str,
) -> bool {
match self.verify_user_login(db, username, password).await {
Ok(_) => true,
Err(_) => false,
}
}
pub async fn assert_user_cannot_login(
&self,
db: &Database,
username: &str,
password: &str,
) -> bool {
!self.assert_user_can_login(db, username, password).await
}
pub async fn assert_user_login_returns_correct_id(
&self,
db: &Database,
username: &str,
password: &str,
expected_id: Uuid,
) -> bool {
match self.verify_user_login(db, username, password).await {
Ok(id) => id == expected_id,
Err(_) => false,
}
}
pub async fn test_invalid_login_attempts(&self, db: &Database, username: &str) -> Vec<bool> {
let invalid_passwords = vec!["wrong_password", "", "123", "password"];
let mut results = Vec::new();
for password in invalid_passwords {
let can_login = self.assert_user_can_login(db, username, password).await;
results.push(!can_login); // We expect these to fail, so invert the result
}
results
}
pub async fn create_user_and_verify_auth(
&self,
db: &Database,
username: Option<String>,
password: Option<String>,
) -> Result<(entity::user::Model, bool), backend::error::ApiError> {
let (user, pwd) = self
.create_authenticated_user(db, username, password)
.await?;
let can_login = self.assert_user_can_login(db, &user.username, &pwd).await;
Ok((user, can_login))
}
}

View file

@ -0,0 +1,3 @@
pub mod auth_helpers;
pub mod project_helpers;
pub mod user_helpers;

View file

@ -0,0 +1,125 @@
use crate::common::test_helpers::TestContext;
use backend::{
Database,
db::{entity, project::CreateProject},
};
use uuid::Uuid;
impl TestContext {
pub async fn create_project(
&self,
db: &Database,
name: Option<String>,
) -> Result<entity::project::Model, backend::error::ApiError> {
let name = name.unwrap_or_else(|| format!("Test Project {}", self.test_id));
let create_project = CreateProject { name };
let project = db.create_project(create_project).await?;
if let Ok(mut projects) = self.created_projects.lock() {
projects.push(project.id);
}
Ok(project)
}
pub async fn create_project_with_name(
&self,
db: &Database,
name: String,
) -> Result<entity::project::Model, backend::error::ApiError> {
self.create_project(db, Some(name)).await
}
pub async fn create_multiple_projects(
&self,
db: &Database,
count: usize,
) -> Result<Vec<entity::project::Model>, backend::error::ApiError> {
let mut projects = Vec::new();
for i in 0..count {
let name = format!("Test Project {} {}", self.test_id, i);
let project = self.create_project(db, Some(name)).await?;
projects.push(project);
}
Ok(projects)
}
pub async fn get_project_by_id(
&self,
db: &Database,
id: &Uuid,
) -> Result<Option<entity::project::Model>, backend::error::ApiError> {
db.get_project(id).await
}
pub async fn get_all_projects(
&self,
db: &Database,
) -> Result<Vec<entity::project::Model>, backend::error::ApiError> {
db.get_projects().await
}
pub async fn update_project(
&self,
db: &Database,
id: &Uuid,
name: String,
) -> Result<entity::project::Model, backend::error::ApiError> {
let update_data = CreateProject { name };
db.update_project(id, update_data).await
}
pub async fn assert_project_exists(&self, db: &Database, id: &Uuid) -> bool {
match self.get_project_by_id(db, id).await {
Ok(Some(_)) => true,
_ => false,
}
}
pub async fn assert_project_count(&self, db: &Database, expected: usize) -> bool {
match self.get_all_projects(db).await {
Ok(projects) => projects.len() == expected,
Err(_) => false,
}
}
pub async fn assert_project_not_exists(&self, db: &Database, id: &Uuid) -> bool {
!self.assert_project_exists(db, id).await
}
pub async fn assert_project_name(&self, db: &Database, id: &Uuid, expected_name: &str) -> bool {
match self.get_project_by_id(db, id).await {
Ok(Some(project)) => project.name == expected_name,
_ => false,
}
}
pub async fn delete_project(
&self,
db: &Database,
id: &Uuid,
) -> Result<(), backend::error::ApiError> {
db.delete_project(id).await?;
if let Ok(mut projects) = self.created_projects.lock() {
projects.retain(|&project_id| project_id != *id);
}
Ok(())
}
pub async fn cleanup_projects(&self, db: &Database) {
if let Ok(projects) = self.created_projects.lock() {
for project_id in projects.iter() {
let _ = db.delete_project(project_id).await;
}
}
if let Ok(mut projects) = self.created_projects.lock() {
projects.clear();
}
}
}

View file

@ -0,0 +1,121 @@
use crate::common::test_helpers::TestContext;
use backend::{Database, db::entity};
use uuid::Uuid;
impl TestContext {
pub async fn create_user(
&self,
db: &Database,
username: Option<String>,
name: Option<String>,
) -> Result<entity::user::Model, backend::error::ApiError> {
let test_id = &self.test_id;
let username = username.unwrap_or_else(|| format!("user_{}", test_id));
let name = name.unwrap_or_else(|| format!("Test User {}", test_id));
let password = "password123".to_string();
let user = db.create_user(name, username, password).await?;
if let Ok(mut users) = self.created_users.lock() {
users.push(user.id);
}
Ok(user)
}
pub async fn create_user_with_password(
&self,
db: &Database,
username: Option<String>,
name: Option<String>,
password: String,
) -> Result<entity::user::Model, backend::error::ApiError> {
let test_id = &self.test_id;
let username = username.unwrap_or_else(|| format!("user_{}", test_id));
let name = name.unwrap_or_else(|| format!("Test User {}", test_id));
let user = db.create_user(name, username, password).await?;
if let Ok(mut users) = self.created_users.lock() {
users.push(user.id);
}
Ok(user)
}
pub async fn create_multiple_users(
&self,
db: &Database,
count: usize,
) -> Result<Vec<entity::user::Model>, backend::error::ApiError> {
let mut users = Vec::new();
for i in 0..count {
let username = format!("user_{}_{}", self.test_id, i);
let name = format!("Test User {} {}", self.test_id, i);
let user = self.create_user(db, Some(username), Some(name)).await?;
users.push(user);
}
Ok(users)
}
pub async fn get_user_by_id(
&self,
db: &Database,
id: Uuid,
) -> Result<Option<entity::user::Model>, backend::error::ApiError> {
db.get_user(id).await
}
pub async fn get_all_users(
&self,
db: &Database,
) -> Result<Vec<entity::user::Model>, backend::error::ApiError> {
db.get_users().await
}
pub async fn assert_user_exists(&self, db: &Database, id: Uuid) -> bool {
match self.get_user_by_id(db, id).await {
Ok(Some(_)) => true,
_ => false,
}
}
pub async fn assert_user_count(&self, db: &Database, expected: usize) -> bool {
match self.get_all_users(db).await {
Ok(users) => users.len() == expected,
Err(_) => false,
}
}
pub async fn assert_user_not_exists(&self, db: &Database, id: Uuid) -> bool {
!self.assert_user_exists(db, id).await
}
pub async fn delete_user(
&self,
db: &Database,
id: Uuid,
) -> Result<(), backend::error::ApiError> {
db.delete_user(id).await?;
if let Ok(mut users) = self.created_users.lock() {
users.retain(|&user_id| user_id != id);
}
Ok(())
}
pub async fn cleanup_users(&self, db: &Database) {
if let Ok(users) = self.created_users.lock() {
for user_id in users.iter() {
let _ = db.delete_user(*user_id).await;
}
}
if let Ok(mut users) = self.created_users.lock() {
users.clear();
}
}
}

View file

@ -1,76 +1,5 @@
use backend::{Database, build_database_url};
use log::{debug, info};
use migration::{Migrator, MigratorTrait};
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
use testcontainers_modules::{postgres::Postgres, redis::Redis};
pub mod db_helpers;
pub mod setup;
pub mod test_helpers;
pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Database) {
let postgres = Postgres::default()
.with_env_var("POSTGRES_DB", "test_db")
.with_env_var("POSTGRES_USER", "postgres")
.with_env_var("POSTGRES_PASSWORD", "postgres")
.start()
.await
.expect("Failed to start PostgreSQL container");
let redis = Redis::default()
.start()
.await
.expect("Failed to start Redis container");
let postgres_port = postgres.get_host_port_ipv4(5432).await.unwrap();
let redis_port = redis.get_host_port_ipv4(6379).await.unwrap();
debug!("PostgreSQL container started on port: {}", postgres_port);
debug!("Redis container started on port: {}", redis_port);
// Wait for PostgreSQL to be ready
wait_for_postgres_ready(&postgres).await;
unsafe {
std::env::set_var("DB_HOST", "127.0.0.1");
std::env::set_var("DB_PORT", postgres_port.to_string());
std::env::set_var("DB_NAME", "test_db");
std::env::set_var("DB_USER", "postgres");
std::env::set_var("DB_PASSWORD", "postgres");
std::env::set_var("REDIS_HOST", "127.0.0.1");
std::env::set_var("REDIS_PORT", redis_port.to_string());
}
let database_url = build_database_url();
info!("Database URL: {}", database_url);
let database = Database::new(database_url.into()).await.unwrap();
Migrator::up(database.connection(), None).await.unwrap();
(postgres, redis, database)
}
async fn wait_for_postgres_ready(container: &ContainerAsync<Postgres>) {
info!("Waiting for PostgreSQL to be ready...");
for attempt in 1..=30 {
match container.stdout_to_vec().await {
Ok(logs) => {
let log_string = String::from_utf8_lossy(&logs);
if log_string.contains("database system is ready to accept connections") {
info!("PostgreSQL is ready after {} attempts", attempt);
return;
}
debug!("Attempt {}: PostgreSQL not ready yet", attempt);
}
Err(e) => {
debug!("Attempt {}: Failed to read logs: {}", attempt, e);
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
panic!("PostgreSQL failed to become ready within 30 seconds");
}
pub use setup::setup;

View file

@ -0,0 +1,74 @@
use backend::{Database, build_database_url};
use log::{debug, info};
use migration::{Migrator, MigratorTrait};
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
use testcontainers_modules::{postgres::Postgres, redis::Redis};
pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Database) {
let postgres = Postgres::default()
.with_env_var("POSTGRES_DB", "test_db")
.with_env_var("POSTGRES_USER", "postgres")
.with_env_var("POSTGRES_PASSWORD", "postgres")
.start()
.await
.expect("Failed to start PostgreSQL container");
let redis = Redis::default()
.start()
.await
.expect("Failed to start Redis container");
let postgres_port = postgres.get_host_port_ipv4(5432).await.unwrap();
let redis_port = redis.get_host_port_ipv4(6379).await.unwrap();
debug!("PostgreSQL container started on port: {}", postgres_port);
debug!("Redis container started on port: {}", redis_port);
// Wait for PostgreSQL to be ready
wait_for_postgres_ready(&postgres).await;
unsafe {
std::env::set_var("DB_HOST", "127.0.0.1");
std::env::set_var("DB_PORT", postgres_port.to_string());
std::env::set_var("DB_NAME", "test_db");
std::env::set_var("DB_USER", "postgres");
std::env::set_var("DB_PASSWORD", "postgres");
std::env::set_var("REDIS_HOST", "127.0.0.1");
std::env::set_var("REDIS_PORT", redis_port.to_string());
}
let database_url = build_database_url();
info!("Database URL: {}", database_url);
let database = Database::new(database_url.into()).await.unwrap();
Migrator::up(database.connection(), None).await.unwrap();
(postgres, redis, database)
}
async fn wait_for_postgres_ready(container: &ContainerAsync<Postgres>) {
info!("Waiting for PostgreSQL to be ready...");
for attempt in 1..=30 {
match container.stdout_to_vec().await {
Ok(logs) => {
let log_string = String::from_utf8_lossy(&logs);
if log_string.contains("database system is ready to accept connections") {
info!("PostgreSQL is ready after {} attempts", attempt);
return;
}
debug!("Attempt {}: PostgreSQL not ready yet", attempt);
}
Err(e) => {
debug!("Attempt {}: Failed to read logs: {}", attempt, e);
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
panic!("PostgreSQL failed to become ready within 30 seconds");
}

View file

@ -3,6 +3,7 @@ use lazy_static::lazy_static;
use std::sync::atomic::{AtomicU64, Ordering};
use testcontainers::ContainerAsync;
use testcontainers_modules::{postgres::Postgres, redis::Redis};
use uuid::Uuid;
use super::setup;
@ -61,22 +62,35 @@ impl UserFactory {
pub struct TestContext {
pub test_id: String,
pub created_users: std::sync::Arc<std::sync::Mutex<Vec<uuid::Uuid>>>,
pub created_projects: std::sync::Arc<std::sync::Mutex<Vec<uuid::Uuid>>>,
}
impl TestContext {
pub fn new() -> Self {
Self {
test_id: get_unique_test_id(),
created_users: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
created_projects: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
pub fn create_user_data(&self, username_prefix: Option<&str>, name: Option<&str>) -> serde_json::Value {
pub fn create_user_data(
&self,
username_prefix: Option<&str>,
name: Option<&str>,
) -> serde_json::Value {
let username = username_prefix
.map(|prefix| format!("{}_{}", prefix, self.test_id))
.unwrap_or_else(|| format!("user_{}", self.test_id));
UserFactory::create_request(Some(username), name.map(String::from))
}
pub async fn cleanup_all(&self, db: &Database) {
self.cleanup_projects(db).await;
self.cleanup_users(db).await;
}
}
#[macro_export]
@ -95,3 +109,17 @@ macro_rules! create_test_app {
.await
}};
}
#[macro_export]
macro_rules! with_test_context {
($test_fn:expr) => {{
let ctx = $crate::common::test_helpers::TestContext::new();
let db = $crate::common::test_helpers::get_database().await;
let result = $test_fn(ctx.clone(), db).await;
ctx.cleanup_all(db).await;
result
}};
}

View file

@ -1,7 +1,7 @@
use actix_web::{http::header, test};
use serde::{Deserialize, Serialize};
use crate::{create_test_app, common::test_helpers::UserFactory};
use crate::{common::test_helpers::UserFactory, create_test_app};
#[cfg(test)]
mod tests {
@ -58,7 +58,11 @@ mod tests {
.await;
let create_status = create_resp.status();
assert!(create_status.is_success(), "Failed to create user: {}", create_status);
assert!(
create_status.is_success(),
"Failed to create user: {}",
create_status
);
let user: RespCreateUser = test::read_body_json(create_resp).await;
// Delete the user
@ -70,7 +74,11 @@ mod tests {
let delete_message: String = test::read_body_json(delete_resp).await;
assert_eq!(delete_message, format!("User {} deleted", user.id));
assert!(delete_status.is_success(), "Failed to delete user with status: {:?}", delete_status);
assert!(
delete_status.is_success(),
"Failed to delete user with status: {:?}",
delete_status
);
}
#[actix_web::test]

View file

@ -0,0 +1,164 @@
use crate::{common::test_helpers::TestContext, create_test_app, with_test_context};
use actix_web::test;
use backend::Database;
#[cfg(test)]
mod tests {
use super::*;
#[actix_web::test]
async fn test_user_crud_operations() {
with_test_context!(|ctx: TestContext, db: &Database| async move {
// Create single user
let user = ctx
.create_user(
db,
Some("testuser".to_string()),
Some("Test User".to_string()),
)
.await
.unwrap();
assert_eq!(user.username, "testuser");
assert_eq!(user.name, "Test User");
// Assert user exists
assert!(ctx.assert_user_exists(db, user.id).await);
// Get user by ID
let retrieved_user = ctx.get_user_by_id(db, user.id).await.unwrap().unwrap();
assert_eq!(retrieved_user.id, user.id);
// Create multiple users
let users = ctx.create_multiple_users(db, 3).await.unwrap();
assert_eq!(users.len(), 3);
// Assert user count (initial user + 3 new users = 4)
assert!(ctx.assert_user_count(db, 4).await);
// Delete a user
ctx.delete_user(db, user.id).await.unwrap();
assert!(ctx.assert_user_not_exists(db, user.id).await);
// All cleanup handled automatically
});
}
#[actix_web::test]
async fn test_project_crud_operations() {
with_test_context!(|ctx: TestContext, db: &Database| async move {
// Create project with custom name
let project = ctx
.create_project_with_name(db, "My Test Project".to_string())
.await
.unwrap();
assert_eq!(project.name, "My Test Project");
// Assert project exists
assert!(ctx.assert_project_exists(db, &project.id).await);
// Update project
let updated = ctx
.update_project(db, &project.id, "Updated Project Name".to_string())
.await
.unwrap();
assert_eq!(updated.name, "Updated Project Name");
// Assert project name changed
assert!(
ctx.assert_project_name(db, &project.id, "Updated Project Name")
.await
);
// Create multiple projects
let projects = ctx.create_multiple_projects(db, 2).await.unwrap();
assert_eq!(projects.len(), 2);
// Assert project count (1 original + 2 new = 3)
assert!(ctx.assert_project_count(db, 3).await);
// All cleanup automatic
});
}
#[actix_web::test]
async fn test_authentication_operations() {
with_test_context!(|ctx: TestContext, db: &Database| async move {
// Create authenticated user
let (user, password) = ctx
.create_authenticated_user(
db,
Some("authuser".to_string()),
Some("securepass123".to_string()),
)
.await
.unwrap();
// Test successful login
assert!(
ctx.assert_user_can_login(db, "authuser", "securepass123")
.await
);
// Test login returns correct user ID
assert!(
ctx.assert_user_login_returns_correct_id(db, "authuser", "securepass123", user.id)
.await
);
// Test failed login with wrong password
assert!(
ctx.assert_user_cannot_login(db, "authuser", "wrongpassword")
.await
);
// Test multiple invalid login attempts
let invalid_results = ctx.test_invalid_login_attempts(db, "authuser").await;
assert!(invalid_results.iter().all(|&result| result)); // All should fail (return true)
// Create and verify multiple auth users
let auth_users = ctx
.create_multiple_authenticated_users(db, 2)
.await
.unwrap();
assert_eq!(auth_users.len(), 2);
for (user, pwd) in &auth_users {
assert!(ctx.assert_user_can_login(db, &user.username, pwd).await);
}
// All cleanup automatic
});
}
#[actix_web::test]
async fn test_mixed_operations_with_api() {
with_test_context!(|ctx: TestContext, db: &Database| async move {
let app = create_test_app!();
// Create user via helper
let user = ctx.create_user(db, None, None).await.unwrap();
// Create project via helper
let project = ctx.create_project(db, None).await.unwrap();
// Test API endpoints
let user_resp = test::TestRequest::get()
.uri(&format!("/api/v1/user/{}", user.id))
.send_request(&app)
.await;
assert!(user_resp.status().is_success());
let projects_resp = test::TestRequest::get()
.uri("/api/v1/project")
.send_request(&app)
.await;
assert!(projects_resp.status().is_success());
// Assert both exist in database
assert!(ctx.assert_user_exists(db, user.id).await);
assert!(ctx.assert_project_exists(db, &project.id).await);
// All cleanup automatic
});
}
}