You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

377 lines
13 KiB
Rust

//! Integration tests for the themes CRUD endpoints.
//!
//! Tests:
//! - GET /api/v1/themes — list user's themes
//! - POST /api/v1/themes — create a theme
//! - PUT /api/v1/themes/:id — update a theme
//! - DELETE /api/v1/themes/:id — delete a theme
//!
//! Covers authentication, validation, ownership isolation, and CRUD operations.
//!
//! Requires a running Postgres instance. Set `TEST_DATABASE_URL` to run.
mod common;
use axum::http::StatusCode;
fn require_test_db() -> bool {
std::env::var("TEST_DATABASE_URL").is_ok()
}
// ═══════════════════════════════════════════════════════════════════════════
// Authentication
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn list_themes_without_auth_returns_401() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (status, body) = app
.get_with_session("/api/v1/themes", "invalid-token")
.await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"GET /themes without auth should return 401"
);
assert_eq!(body["error"], "unauthorized");
}
// ═══════════════════════════════════════════════════════════════════════════
// CRUD — List
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn list_themes_returns_created_theme() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-list@example.com")
.await;
// Create a theme so the list is not empty
let body = serde_json::json!({
"name": "Test Theme",
"theme": "Intelligence Artificielle",
"categories": ["AI News", "Research"]
});
let (create_status, _) = app
.post_with_session("/api/v1/themes", &body, &session)
.await;
assert_eq!(create_status, StatusCode::CREATED);
// List themes
let (status, resp) = app.get_with_session("/api/v1/themes", &session).await;
assert_eq!(status, StatusCode::OK, "GET /themes should return 200");
let themes = resp.as_array().expect("Response should be an array");
assert!(
!themes.is_empty(),
"User should have at least 1 theme after creation"
);
assert_eq!(themes[0]["name"], "Test Theme");
assert_eq!(themes[0]["theme"], "Intelligence Artificielle");
}
// ═══════════════════════════════════════════════════════════════════════════
// CRUD — Create
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn create_theme_returns_201() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-create@example.com")
.await;
let body = serde_json::json!({
"name": "Test Theme",
"theme": "Intelligence Artificielle",
"categories": ["AI News", "Research"]
});
let (status, resp) = app
.post_with_session("/api/v1/themes", &body, &session)
.await;
assert_eq!(
status,
StatusCode::CREATED,
"POST /themes with valid data should return 201"
);
assert!(
resp["id"].as_str().is_some(),
"Response should contain an id"
);
assert_eq!(resp["name"], "Test Theme");
let categories = resp["categories"]
.as_array()
.expect("categories should be an array");
assert_eq!(categories.len(), 2);
assert_eq!(categories[0], "AI News");
assert_eq!(categories[1], "Research");
}
#[tokio::test]
async fn create_theme_empty_name_returns_422() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-empty-name@example.com")
.await;
let body = serde_json::json!({
"name": "",
"theme": "Intelligence Artificielle",
"categories": ["AI News"]
});
let (status, _resp) = app
.post_with_session("/api/v1/themes", &body, &session)
.await;
assert_eq!(
status,
StatusCode::UNPROCESSABLE_ENTITY,
"POST /themes with empty name should return 422"
);
}
#[tokio::test]
async fn create_theme_empty_categories_returns_422() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-empty-cats@example.com")
.await;
let body = serde_json::json!({
"name": "Test Theme",
"theme": "Intelligence Artificielle",
"categories": []
});
let (status, _resp) = app
.post_with_session("/api/v1/themes", &body, &session)
.await;
assert_eq!(
status,
StatusCode::UNPROCESSABLE_ENTITY,
"POST /themes with empty categories should return 422"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// CRUD — Update
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn update_theme_changes_name() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-update@example.com")
.await;
// Create a theme
let body = serde_json::json!({
"name": "Original Name",
"theme": "Intelligence Artificielle",
"categories": ["AI News"]
});
let (status, created) = app
.post_with_session("/api/v1/themes", &body, &session)
.await;
assert_eq!(status, StatusCode::CREATED);
let theme_id = created["id"].as_str().unwrap();
// Update the name
let update_body = serde_json::json!({
"name": "New Name"
});
let (status, resp) = app
.put_with_session(
&format!("/api/v1/themes/{}", theme_id),
&update_body,
&session,
)
.await;
assert_eq!(
status,
StatusCode::OK,
"PUT /themes/:id should return 200"
);
assert_eq!(resp["name"], "New Name");
// Other fields should remain unchanged
assert_eq!(resp["theme"], "Intelligence Artificielle");
}
#[tokio::test]
async fn update_nonexistent_theme_returns_404() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-update-404@example.com")
.await;
let random_id = uuid::Uuid::new_v4();
let update_body = serde_json::json!({
"name": "New Name"
});
let (status, _resp) = app
.put_with_session(
&format!("/api/v1/themes/{}", random_id),
&update_body,
&session,
)
.await;
assert_eq!(
status,
StatusCode::NOT_FOUND,
"PUT /themes/:id with nonexistent id should return 404"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// CRUD — Delete
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn delete_theme_returns_204() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-delete@example.com")
.await;
// Create a theme
let body = serde_json::json!({
"name": "To Delete",
"theme": "Intelligence Artificielle",
"categories": ["AI News"]
});
let (status, created) = app
.post_with_session("/api/v1/themes", &body, &session)
.await;
assert_eq!(status, StatusCode::CREATED);
let theme_id = created["id"].as_str().unwrap();
// Delete it
let (status, _resp) = app
.delete_with_session(&format!("/api/v1/themes/{}", theme_id), &session)
.await;
assert_eq!(
status,
StatusCode::NO_CONTENT,
"DELETE /themes/:id should return 204"
);
// Verify it's gone from the list
let (status, resp) = app.get_with_session("/api/v1/themes", &session).await;
assert_eq!(status, StatusCode::OK);
let themes = resp.as_array().expect("Response should be an array");
let found = themes.iter().any(|t| t["id"].as_str() == Some(theme_id));
assert!(!found, "Deleted theme should not appear in list");
}
#[tokio::test]
async fn delete_nonexistent_theme_returns_404() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("themes-delete-404@example.com")
.await;
let random_id = uuid::Uuid::new_v4();
let (status, _resp) = app
.delete_with_session(&format!("/api/v1/themes/{}", random_id), &session)
.await;
assert_eq!(
status,
StatusCode::NOT_FOUND,
"DELETE /themes/:id with nonexistent id should return 404"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Ownership isolation
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn themes_are_isolated_per_user() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
// Create user A with a theme
let (_user_a_id, session_a) = app
.create_authenticated_user("themes-user-a@example.com")
.await;
let body_a = serde_json::json!({
"name": "User A Theme",
"theme": "Intelligence Artificielle",
"categories": ["AI News"]
});
let (status, _) = app
.post_with_session("/api/v1/themes", &body_a, &session_a)
.await;
assert_eq!(status, StatusCode::CREATED);
// Create user B
let (_user_b_id, session_b) = app
.create_authenticated_user("themes-user-b@example.com")
.await;
// User B's theme list should be empty
let (status, resp) = app.get_with_session("/api/v1/themes", &session_b).await;
assert_eq!(status, StatusCode::OK);
let themes = resp.as_array().expect("Response should be an array");
let has_user_a_theme = themes.iter().any(|t| t["name"] == "User A Theme");
assert!(
!has_user_a_theme,
"User B should not see User A's themes"
);
}