a / api /tests /common /mod.rs
PNRxA
merge main
5fac6c0
Raw History Blame Contribute Delete
10.1 kB
use std::sync::Arc;
use std::time::Duration;
use ab_glyph::FontArc;
use dashmap::DashMap;
use sea_orm::{ConnectionTrait, DatabaseConnection, SqlxSqliteConnector};
use zeroize::Zeroizing;
use openposterdb_api::config::Config;
use openposterdb_api::services::fanart::FanartClient;
use openposterdb_api::services::tmdb::TmdbClient;
use openposterdb_api::{build_app, AppState, FONT_BYTES, MIGRATIONS, SCHEMA_SQL};
pub struct TestAppOptions {
pub cors_origin: Option<String>,
pub secure_cookies: bool,
pub enable_cdn_redirects: bool,
pub external_cache_only: bool,
pub cache_dir_override: Option<String>,
pub free_key_enabled: Option<bool>,
pub disable_fanart: bool,
}
impl Default for TestAppOptions {
fn default() -> Self {
Self {
cors_origin: None,
secure_cookies: false,
enable_cdn_redirects: false,
external_cache_only: false,
cache_dir_override: None,
free_key_enabled: None,
disable_fanart: false,
}
}
}
pub async fn setup_test_app_with_options(opts: TestAppOptions) -> (axum::Router, Arc<AppState>) {
let cors_origin = opts.cors_origin;
let secure_cookies = opts.secure_cookies;
let enable_cdn_redirects = opts.enable_cdn_redirects;
let external_cache_only = opts.external_cache_only;
let cache_dir_override = opts.cache_dir_override;
let free_key_enabled = opts.free_key_enabled;
let disable_fanart = opts.disable_fanart;
_setup_test_app(cors_origin, secure_cookies, enable_cdn_redirects, external_cache_only, cache_dir_override, free_key_enabled, disable_fanart).await
}
pub async fn setup_test_app_with_cors(cors_origin: Option<String>) -> (axum::Router, Arc<AppState>) {
_setup_test_app(cors_origin, false, false, false, None, None, false).await
}
async fn _setup_test_app(cors_origin: Option<String>, secure_cookies: bool, enable_cdn_redirects: bool, external_cache_only: bool, cache_dir_override: Option<String>, free_key_enabled: Option<bool>, disable_fanart: bool) -> (axum::Router, Arc<AppState>) {
let sqlite_opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(":memory:")
.create_if_missing(true)
.pragma("foreign_keys", "ON");
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect_with(sqlite_opts)
.await
.expect("failed to connect to test database");
let db: DatabaseConnection = SqlxSqliteConnector::from_sqlx_sqlite_pool(pool);
for sql in SCHEMA_SQL {
db.execute_unprepared(sql)
.await
.expect("failed to create table");
}
for (sql, expected_err) in MIGRATIONS {
match db.execute_unprepared(sql).await {
Ok(_) => {}
Err(e) if e.to_string().to_lowercase().contains(expected_err) => {}
Err(e) => panic!("migration failed: {e}\n SQL: {sql}"),
}
}
let jwt_secret = Zeroizing::new(vec![0xAB; 32]);
let http = reqwest::Client::new();
let font = FontArc::try_from_slice(FONT_BYTES).expect("failed to load font");
let api_key_cache = moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(300))
.build();
let image_inflight = moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(30))
.build();
let id_cache = moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(3600))
.build();
let ratings_cache = moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(1800))
.build();
let image_mem_cache = moka::future::Cache::builder()
.max_capacity(1024 * 1024)
.time_to_live(Duration::from_secs(3600))
.build();
let refresh_locks = moka::sync::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(300))
.build();
let state = Arc::new(AppState {
config: Config {
tmdb_api_key: "test".into(),
omdb_api_key: None,
cache_dir: cache_dir_override.unwrap_or_else(|| "/tmp/openposterdb-test".into()),
db_dir: "/tmp/openposterdb-test".into(),
listen_addr: "127.0.0.1:0".into(),
ratings_min_stale_secs: 86400,
ratings_max_age_secs: 31_536_000,
image_stale_secs: 0,
image_quality: 85,
mdblist_api_key: None,
image_mem_cache_mb: 1,
static_dir: None,
cors_origin,
fanart_api_key: if disable_fanart { None } else { Some("test".into()) },
trakt_client_id: None,
enable_cdn_redirects,
external_cache_only,
free_key_enabled,
disable_public_pages: false,
},
tmdb: TmdbClient::new("test".into(), http.clone()),
omdb: None,
mdblist: None,
trakt: None,
font,
refresh_locks,
db,
jwt_secret,
secure_cookies,
api_key_cache,
image_inflight,
id_cache,
ratings_cache,
image_mem_cache,
pending_last_used: Arc::new(DashMap::new()),
fanart: if disable_fanart { None } else { Some(FanartClient::new("test".into(), http)) },
fanart_cache: moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(3600))
.build(),
fanart_negative: moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(3600))
.build(),
tmdb_images_cache: moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(1800))
.build(),
settings_cache: moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(300))
.build(),
global_settings_cache: moka::future::Cache::builder()
.max_capacity(1)
.time_to_live(Duration::from_secs(300))
.build(),
preview_cache: moka::future::Cache::builder()
.max_capacity(500)
.time_to_live(Duration::from_secs(300))
.build(),
free_api_key_cache: moka::future::Cache::builder()
.max_capacity(1)
.time_to_live(Duration::from_secs(60))
.build(),
render_semaphore: Arc::new(tokio::sync::Semaphore::new(4)),
cross_id_semaphore: Arc::new(tokio::sync::Semaphore::new(8)),
settings_hash_registry: moka::future::Cache::builder()
.max_capacity(100)
.time_to_live(Duration::from_secs(300))
.build(),
available_ratings_cache: moka::future::Cache::builder()
.max_capacity(1000)
.time_to_live(Duration::from_secs(300))
.build(),
});
let app = build_app(state.clone());
(app, state)
}
pub async fn setup_test_app() -> (axum::Router, Arc<AppState>) {
setup_test_app_with_cors(None).await
}
/// Helper to perform setup and get back an access token.
pub async fn setup_admin(app: &axum::Router) -> String {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
let req = Request::builder()
.method("POST")
.uri("/api/auth/setup")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"username": "admin", "password": "testpassword123"}).to_string(),
))
.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();
json["token"].as_str().unwrap().to_string()
}
/// Helper: set up admin, create an API key, log in with it, and return the session JWT.
pub async fn setup_api_key_session(app: &axum::Router) -> String {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
let admin_token = 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 {admin_token}"))
.body(Body::from(
serde_json::json!({"name": "preview-test-key"}).to_string(),
))
.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();
let raw_key = json["key"].as_str().unwrap().to_string();
// Log in with the API key
let req = Request::builder()
.method("POST")
.uri("/api/auth/key-login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"api_key": raw_key}).to_string(),
))
.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();
json["token"].as_str().unwrap().to_string()
}
/// Extract Set-Cookie header value from a response.
pub fn extract_set_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
headers
.get(axum::http::header::SET_COOKIE)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
/// Extract the refresh_token value from a Set-Cookie header.
pub fn extract_refresh_token(set_cookie: &str) -> Option<String> {
set_cookie
.split(';')
.next()
.and_then(|s| s.strip_prefix("refresh_token="))
.map(|s| s.to_string())
}