Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
211 lines
7 KiB
Rust
211 lines
7 KiB
Rust
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, AnalyticsConfig, CloudConfig, Config, DirectoryConfig, EnrichmentConfig,
|
|
JobsConfig, PluginsConfig, ScanningConfig, ServerConfig, SqliteConfig, StorageBackendType,
|
|
StorageConfig, ThumbnailConfig, TranscodingConfig, UiConfig, WebhookConfig,
|
|
};
|
|
use pinakes_core::jobs::JobQueue;
|
|
use pinakes_core::plugin::PluginManager;
|
|
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
|
|
}
|
|
|
|
async fn setup_app_with_plugins() -> (axum::Router, Arc<PluginManager>, tempfile::TempDir) {
|
|
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;
|
|
|
|
// Create temp directories for plugin manager (automatically cleaned up when TempDir drops)
|
|
let temp_dir = tempfile::TempDir::new().expect("create temp dir");
|
|
let data_dir = temp_dir.path().join("data");
|
|
let cache_dir = temp_dir.path().join("cache");
|
|
std::fs::create_dir_all(&data_dir).expect("create data dir");
|
|
std::fs::create_dir_all(&cache_dir).expect("create cache dir");
|
|
|
|
let plugin_config = PluginsConfig {
|
|
enabled: true,
|
|
data_dir: data_dir.clone(),
|
|
cache_dir: cache_dir.clone(),
|
|
plugin_dirs: vec![],
|
|
enable_hot_reload: false,
|
|
allow_unsigned: true,
|
|
max_concurrent_ops: 2,
|
|
plugin_timeout_secs: 10,
|
|
};
|
|
|
|
let plugin_manager = PluginManager::new(data_dir, cache_dir, plugin_config.clone().into())
|
|
.expect("create plugin manager");
|
|
let plugin_manager = Arc::new(plugin_manager);
|
|
|
|
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![],
|
|
plugins: plugin_config,
|
|
transcoding: TranscodingConfig::default(),
|
|
enrichment: EnrichmentConfig::default(),
|
|
cloud: CloudConfig::default(),
|
|
analytics: AnalyticsConfig::default(),
|
|
};
|
|
|
|
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),
|
|
plugin_manager: Some(plugin_manager.clone()),
|
|
transcode_service: None,
|
|
};
|
|
|
|
let router = pinakes_server::app::create_router(state);
|
|
(router, plugin_manager, temp_dir)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_list_plugins_empty() {
|
|
let (app, _pm, _tmp) = setup_app_with_plugins().await;
|
|
|
|
let response = app.oneshot(get("/api/v1/plugins")).await.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
|
let plugins: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
|
|
assert_eq!(plugins.len(), 0, "should start with no plugins loaded");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_plugin_manager_exists() {
|
|
let (app, _pm, _tmp) = setup_app_with_plugins().await;
|
|
|
|
// Verify plugin manager is accessible
|
|
let plugins = _pm.list_plugins().await;
|
|
assert_eq!(plugins.len(), 0);
|
|
|
|
// Verify API endpoint works
|
|
let response = app.oneshot(get("/api/v1/plugins")).await.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_plugin_not_found() {
|
|
let (app, _pm, _tmp) = setup_app_with_plugins().await;
|
|
|
|
let response = app
|
|
.oneshot(get("/api/v1/plugins/nonexistent"))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_plugin_enable_disable() {
|
|
let (app, pm, _tmp) = setup_app_with_plugins().await;
|
|
|
|
// Verify plugin manager is initialized
|
|
assert!(pm.list_plugins().await.is_empty());
|
|
|
|
// For this test, we would need to actually load a plugin first
|
|
// Since we don't have a real WASM plugin loaded, we'll just verify
|
|
// the endpoints exist and return appropriate errors
|
|
|
|
let mut req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/plugins/test-plugin/enable")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
req.extensions_mut().insert(test_addr());
|
|
|
|
let response = app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Should be NOT_FOUND since plugin doesn't exist
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
|
|
// Test disable endpoint
|
|
let mut req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/plugins/test-plugin/disable")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
req.extensions_mut().insert(test_addr());
|
|
|
|
let response = app.oneshot(req).await.unwrap();
|
|
|
|
// Should also be NOT_FOUND
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_plugin_uninstall_not_found() {
|
|
let (app, _pm, _tmp) = setup_app_with_plugins().await;
|
|
|
|
let mut req = Request::builder()
|
|
.method("DELETE")
|
|
.uri("/api/v1/plugins/nonexistent")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
req.extensions_mut().insert(test_addr());
|
|
|
|
let response = app.oneshot(req).await.unwrap();
|
|
|
|
// Expect 400 or 404 when plugin doesn't exist
|
|
assert!(
|
|
response.status() == StatusCode::BAD_REQUEST || response.status() == StatusCode::NOT_FOUND
|
|
);
|
|
}
|