a / api /tests /admin.rs
PNRxA
feat: add MDBList score and Roger Ebert rating sources (#42, #35)
79c9393
Raw History Blame Contribute Delete
20.8 kB
mod common;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
#[tokio::test]
async fn stats_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/stats")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn stats_returns_counts() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/stats")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["total_images"], 0);
assert_eq!(json["total_api_keys"], 0);
assert!(json["mem_cache_entries"].is_number());
assert!(json["id_cache_entries"].is_number());
assert!(json["ratings_cache_entries"].is_number());
assert!(json["image_mem_cache_mb"].is_number());
}
#[tokio::test]
async fn list_posters_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/posters")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn list_posters_returns_empty() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/posters?page=1&page_size=10")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["items"].as_array().unwrap().len(), 0);
assert_eq!(json["total"], 0);
assert_eq!(json["page"], 1);
assert_eq!(json["page_size"], 10);
}
#[tokio::test]
async fn list_posters_default_pagination() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/posters")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["page"], 1);
assert_eq!(json["page_size"], 50);
}
#[tokio::test]
async fn stats_reflects_api_key_count() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
// Create an API key
let req = Request::builder()
.method("POST")
.uri("/api/keys")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({"name": "test-key"}).to_string(),
))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// Check stats
let req = Request::builder()
.uri("/api/admin/stats")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["total_api_keys"], 1);
}
#[tokio::test]
async fn poster_image_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/posters/imdb/tt0111161/image")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
// --- Global settings endpoints ---
#[tokio::test]
async fn settings_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/settings")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn get_settings_returns_defaults() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/settings")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["image_source"], "t");
assert_eq!(json["lang"], "en");
assert_eq!(json["textless"], false);
assert_eq!(json["fanart_available"], true);
assert_eq!(json["ratings_limit"], 3);
assert_eq!(json["ratings_order"], "mal,imdb,lb,rt,mc,rta,tmdb,trakt,mdblist,ebert");
}
#[tokio::test]
async fn update_settings_and_read_back() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
// Update
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "f",
"lang": "de",
"textless": true
})
.to_string(),
))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// Read back
let req = Request::builder()
.uri("/api/admin/settings")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["image_source"], "f");
assert_eq!(json["lang"], "de");
assert_eq!(json["textless"], true);
}
#[tokio::test]
async fn update_settings_rejects_invalid_source() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "invalid",
"lang": "en"
})
.to_string(),
))
.unwrap();
let res = app.oneshot(req).await.unwrap();
// Invalid enum values are rejected at deserialization (422), not validation (400)
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn update_settings_rejects_invalid_lang() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "f",
"lang": "../../etc"
})
.to_string(),
))
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn update_settings_with_ratings_and_read_back() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "t",
"ratings_limit": 3,
"ratings_order": "mal,imdb,rta"
})
.to_string(),
))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let req = Request::builder()
.uri("/api/admin/settings")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["ratings_limit"], 3);
assert_eq!(json["ratings_order"], "mal,imdb,rta");
}
#[tokio::test]
async fn update_settings_rejects_invalid_ratings_limit() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "t",
"ratings_limit": 11
})
.to_string(),
))
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
// --- Fetch poster endpoint ---
#[tokio::test]
async fn fetch_poster_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/admin/posters/imdb/tt0111161/fetch")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn fetch_poster_rejects_invalid_id_type() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("POST")
.uri("/api/admin/posters/invalid/tt0111161/fetch")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn fetch_poster_accepts_valid_id_types() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
// These will fail at the generation stage (no real TMDB key) but should not
// be rejected at the id_type validation stage (i.e. not 400).
// tmdb requires a "movie-" or "series-" prefix on the id value.
for (id_type, id_value) in &[("imdb", "tt0012345"), ("tmdb", "movie-12345"), ("tvdb", "12345")] {
let req = Request::builder()
.method("POST")
.uri(format!("/api/admin/posters/{id_type}/{id_value}/fetch"))
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_ne!(
res.status(),
StatusCode::BAD_REQUEST,
"id_type {id_type} should not be rejected as invalid"
);
}
}
#[tokio::test]
async fn update_settings_rejects_invalid_ratings_order() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "t",
"ratings_order": "imdb,bogus"
})
.to_string(),
))
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
// --- Logo admin endpoints ---
#[tokio::test]
async fn list_logos_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/logos")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn list_logos_returns_empty() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/logos?page=1&page_size=10")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["items"].as_array().unwrap().len(), 0);
assert_eq!(json["total"], 0);
assert_eq!(json["page"], 1);
assert_eq!(json["page_size"], 10);
}
#[tokio::test]
async fn logo_image_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/logos/imdb/tt0111161")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn fetch_logo_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/admin/logos/imdb/tt0111161/fetch")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn fetch_logo_rejects_invalid_id_type() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("POST")
.uri("/api/admin/logos/invalid/tt0111161/fetch")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
// --- Backdrop admin endpoints ---
#[tokio::test]
async fn list_backdrops_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/backdrops")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn list_backdrops_returns_empty() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/backdrops?page=1&page_size=10")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["items"].as_array().unwrap().len(), 0);
assert_eq!(json["total"], 0);
assert_eq!(json["page"], 1);
assert_eq!(json["page_size"], 10);
}
#[tokio::test]
async fn backdrop_image_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.uri("/api/admin/backdrops/imdb/tt0111161")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn fetch_backdrop_requires_auth() {
let (app, _state) = common::setup_test_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/admin/backdrops/imdb/tt0111161/fetch")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn fetch_backdrop_rejects_invalid_id_type() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("POST")
.uri("/api/admin/backdrops/invalid/tt0111161/fetch")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn get_settings_returns_new_field_defaults() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.uri("/api/admin/settings")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["poster_position"], "bc");
assert_eq!(json["logo_ratings_limit"], 5);
assert_eq!(json["backdrop_ratings_limit"], 5);
assert_eq!(json["poster_badge_style"], "d");
assert_eq!(json["logo_badge_style"], "v");
assert_eq!(json["backdrop_badge_style"], "v");
}
#[tokio::test]
async fn update_settings_with_poster_position_and_read_back() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "t",
"poster_position": "l",
"logo_ratings_limit": 5,
"backdrop_ratings_limit": 2
})
.to_string(),
))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let req = Request::builder()
.uri("/api/admin/settings")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["poster_position"], "l");
assert_eq!(json["logo_ratings_limit"], 5);
assert_eq!(json["backdrop_ratings_limit"], 2);
}
#[tokio::test]
async fn update_settings_rejects_invalid_poster_position() {
let (app, _state) = common::setup_test_app().await;
let token = common::setup_admin(&app).await;
let req = Request::builder()
.method("PUT")
.uri("/api/admin/settings")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.body(Body::from(
serde_json::json!({
"image_source": "t",
"poster_position": "invalid"
})
.to_string(),
))
.unwrap();
let res = app.oneshot(req).await.unwrap();
// Invalid enum values are rejected at deserialization (422), not validation (400)
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
}