//! Shared helpers for the API tests. #![allow(dead_code)] pub use axum::body::Body; pub use axum::http::{Request, StatusCode}; use scopenet_panel::{app, bootstrap_admin, build_state_with_keys, config::Config, db, yggdrasil::keys::Keys}; pub use serde_json::{json, Value}; use std::io::Write; use std::sync::{Arc, OnceLock}; pub use tower::ServiceExt; /// One auth-server key for the whole test run (key generation is slow). pub fn test_keys() -> Arc { static KEYS: OnceLock> = OnceLock::new(); KEYS.get_or_init(|| Arc::new(Keys::from_private(rsa::RsaPrivateKey::new(&mut rand::thread_rng(), 2048).unwrap()).unwrap())).clone() } pub struct TestApp { pub router: axum::Router, _dir: tempfile::TempDir, } pub async fn setup() -> TestApp { let dir = tempfile::tempdir().unwrap(); let cfg = Config { bind: "127.0.0.1:0".into(), data_dir: dir.path().to_path_buf(), web_dir: dir.path().join("web"), admin_username: "admin".into(), admin_password: Some("supersecret".into()), jwt_secret: Some("test-secret-test-secret-test-secret".into()), curseforge_api_key: None, max_upload_mb: 64, public_url: Some("https://panel.test".into()), }; let pool = db::connect_memory().await.unwrap(); let state = build_state_with_keys(cfg, pool, test_keys()).await.unwrap(); bootstrap_admin(&state).await.unwrap(); TestApp { router: app(state), _dir: dir } } impl TestApp { pub async fn call(&self, method: &str, uri: &str, token: Option<&str>, body: Option) -> (StatusCode, Value) { let mut req = Request::builder().method(method).uri(uri); if let Some(t) = token { req = req.header("authorization", format!("Bearer {t}")); } let req = match body { Some(b) => req.header("content-type", "application/json").body(Body::from(b.to_string())).unwrap(), None => req.body(Body::empty()).unwrap(), }; self.send(req).await } pub async fn send(&self, req: Request) -> (StatusCode, Value) { let resp = self.router.clone().oneshot(req).await.unwrap(); let status = resp.status(); let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); (status, serde_json::from_slice(&bytes).unwrap_or(Value::String(String::from_utf8_lossy(&bytes).into()))) } pub async fn login(&self, user: &str, pass: &str) -> String { let (s, v) = self.call("POST", "/api/v1/auth/login", None, Some(json!({"username": user, "password": pass}))).await; assert_eq!(s, StatusCode::OK, "{v}"); v["token"].as_str().unwrap().to_string() } } pub fn multipart(fields: &[(&str, &str)], file: (&str, &[u8])) -> (String, Vec) { let boundary = "----scopenettest"; let mut body = Vec::new(); for (k, v) in fields { write!(body, "--{boundary}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n").unwrap(); } write!( body, "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: application/octet-stream\r\n\r\n", file.0 ) .unwrap(); body.extend_from_slice(file.1); write!(body, "\r\n--{boundary}--\r\n").unwrap(); (format!("multipart/form-data; boundary={boundary}"), body) } pub fn zip_bytes(entries: &[(&str, &[u8])]) -> Vec { let mut buf = std::io::Cursor::new(Vec::new()); { let mut z = zip::ZipWriter::new(&mut buf); let opts = zip::write::SimpleFileOptions::default(); for (name, data) in entries { z.start_file(*name, opts).unwrap(); z.write_all(data).unwrap(); } z.finish().unwrap(); } buf.into_inner() }