pinakes/crates/pinakes-server/tests/plugin.rs
NotAShelf 3ccddce7fd
treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
2026-03-06 18:29:33 +03:00

243 lines
7.1 KiB
Rust

use std::{net::SocketAddr, sync::Arc};
use axum::{
body::Body,
extract::ConnectInfo,
http::{Request, StatusCode},
};
use http_body_util::BodyExt;
use pinakes_core::{
cache::CacheLayer,
config::{
AccountsConfig,
AnalyticsConfig,
CloudConfig,
Config,
DirectoryConfig,
EnrichmentConfig,
JobsConfig,
ManagedStorageConfig,
PhotoConfig,
PluginsConfig,
ScanningConfig,
ServerConfig,
SharingConfig,
SqliteConfig,
StorageBackendType,
StorageConfig,
SyncConfig,
ThumbnailConfig,
TlsConfig,
TranscodingConfig,
UiConfig,
WebhookConfig,
},
jobs::JobQueue,
plugin::PluginManager,
storage::{StorageBackend, sqlite::SqliteBackend},
};
use tokio::sync::RwLock;
use tower::ServiceExt;
/// 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,
tls: TlsConfig::default(),
authentication_disabled: true,
},
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(),
photos: PhotoConfig::default(),
managed_storage: ManagedStorageConfig::default(),
sync: SyncConfig::default(),
sharing: SharingConfig::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(),
job_queue,
cache: Arc::new(CacheLayer::new(60)),
scheduler: Arc::new(scheduler),
plugin_manager: Some(plugin_manager.clone()),
transcode_service: None,
managed_storage: None,
chunked_upload_manager: 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
);
}