initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
212
crates/pinakes-server/tests/api_test.rs
Normal file
212
crates/pinakes-server/tests/api_test.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::ConnectInfo;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use http_body_util::BodyExt;
|
||||
use tokio::sync::RwLock;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use pinakes_core::cache::CacheLayer;
|
||||
use pinakes_core::config::{
|
||||
AccountsConfig, Config, DirectoryConfig, JobsConfig, ScanningConfig, ServerConfig,
|
||||
SqliteConfig, StorageBackendType, StorageConfig, ThumbnailConfig, UiConfig, WebhookConfig,
|
||||
};
|
||||
use pinakes_core::jobs::JobQueue;
|
||||
use pinakes_core::storage::StorageBackend;
|
||||
use pinakes_core::storage::sqlite::SqliteBackend;
|
||||
|
||||
/// Fake socket address for tests (governor needs ConnectInfo<SocketAddr>)
|
||||
fn test_addr() -> ConnectInfo<SocketAddr> {
|
||||
ConnectInfo("127.0.0.1:9999".parse().unwrap())
|
||||
}
|
||||
|
||||
/// Build a GET request with ConnectInfo for rate limiter compatibility
|
||||
fn get(uri: &str) -> Request<Body> {
|
||||
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
|
||||
req.extensions_mut().insert(test_addr());
|
||||
req
|
||||
}
|
||||
|
||||
/// Build a POST request with ConnectInfo
|
||||
fn post_json(uri: &str, body: &str) -> Request<Body> {
|
||||
let mut req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(uri)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.to_string()))
|
||||
.unwrap();
|
||||
req.extensions_mut().insert(test_addr());
|
||||
req
|
||||
}
|
||||
|
||||
async fn setup_app() -> axum::Router {
|
||||
let backend = SqliteBackend::in_memory().expect("in-memory SQLite");
|
||||
backend.run_migrations().await.expect("migrations");
|
||||
let storage = Arc::new(backend) as pinakes_core::storage::DynStorageBackend;
|
||||
|
||||
let config = Config {
|
||||
storage: StorageConfig {
|
||||
backend: StorageBackendType::Sqlite,
|
||||
sqlite: Some(SqliteConfig {
|
||||
path: ":memory:".into(),
|
||||
}),
|
||||
postgres: None,
|
||||
},
|
||||
directories: DirectoryConfig { roots: vec![] },
|
||||
scanning: ScanningConfig {
|
||||
watch: false,
|
||||
poll_interval_secs: 300,
|
||||
ignore_patterns: vec![],
|
||||
import_concurrency: 8,
|
||||
},
|
||||
server: ServerConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3000,
|
||||
api_key: None,
|
||||
},
|
||||
ui: UiConfig::default(),
|
||||
accounts: AccountsConfig::default(),
|
||||
jobs: JobsConfig::default(),
|
||||
thumbnails: ThumbnailConfig::default(),
|
||||
webhooks: Vec::<WebhookConfig>::new(),
|
||||
scheduled_tasks: vec![],
|
||||
};
|
||||
|
||||
let job_queue = JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
|
||||
let config = Arc::new(RwLock::new(config));
|
||||
let scheduler = pinakes_core::scheduler::TaskScheduler::new(
|
||||
job_queue.clone(),
|
||||
tokio_util::sync::CancellationToken::new(),
|
||||
config.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let state = pinakes_server::state::AppState {
|
||||
storage,
|
||||
config,
|
||||
config_path: None,
|
||||
scan_progress: pinakes_core::scan::ScanProgress::new(),
|
||||
sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||
job_queue,
|
||||
cache: Arc::new(CacheLayer::new(60)),
|
||||
scheduler: Arc::new(scheduler),
|
||||
};
|
||||
|
||||
pinakes_server::app::create_router(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_media_empty() {
|
||||
let app = setup_app().await;
|
||||
|
||||
let response = app.oneshot(get("/api/v1/media")).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let items: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(items.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_and_list_tags() {
|
||||
let app = setup_app().await;
|
||||
|
||||
// Create a tag
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(post_json("/api/v1/tags", r#"{"name":"Music"}"#))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// List tags
|
||||
let response = app.oneshot(get("/api/v1/tags")).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let tags: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(tags.len(), 1);
|
||||
assert_eq!(tags[0]["name"], "Music");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_empty() {
|
||||
let app = setup_app().await;
|
||||
|
||||
let response = app.oneshot(get("/api/v1/search?q=test")).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let result: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(result["total_count"], 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_media_not_found() {
|
||||
let app = setup_app().await;
|
||||
|
||||
let response = app
|
||||
.oneshot(get("/api/v1/media/00000000-0000-0000-0000-000000000000"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_collections_crud() {
|
||||
let app = setup_app().await;
|
||||
|
||||
// Create collection
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(post_json(
|
||||
"/api/v1/collections",
|
||||
r#"{"name":"Favorites","kind":"manual"}"#,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// List collections
|
||||
let response = app.oneshot(get("/api/v1/collections")).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let cols: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(cols[0]["name"], "Favorites");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_statistics_endpoint() {
|
||||
let app = setup_app().await;
|
||||
|
||||
let response = app.oneshot(get("/api/v1/statistics")).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let stats: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(stats["total_media"], 0);
|
||||
assert_eq!(stats["total_size_bytes"], 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scheduled_tasks_endpoint() {
|
||||
let app = setup_app().await;
|
||||
|
||||
let response = app.oneshot(get("/api/v1/tasks/scheduled")).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let tasks: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
|
||||
assert!(!tasks.is_empty(), "should have default scheduled tasks");
|
||||
// Verify structure of first task
|
||||
assert!(tasks[0]["id"].is_string());
|
||||
assert!(tasks[0]["name"].is_string());
|
||||
assert!(tasks[0]["schedule"].is_string());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue