1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat(auth): add auth_db_path to AppState, registry, and main.rs

This commit is contained in:
TheGeneralist 2026-06-26 11:46:23 +02:00
parent 86e8049f1b
commit a4239ba5fc
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
3 changed files with 191 additions and 154 deletions

View file

@ -12,10 +12,19 @@ async fn main() -> Result<()> {
.nth(1) .nth(1)
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("archivr-server.toml")); .unwrap_or_else(|| PathBuf::from("archivr-server.toml"));
let registry = registry::load_registry(&config_path)?;
let app = routes::app(registry.clone());
// Bind address priority: ARCHIVR_BIND env var > TOML bind field > default loopback. let registry = registry::load_registry(&config_path)?;
// Auth DB lives next to the config file unless overridden in the TOML.
let auth_db_path = registry.auth_db_path.clone().unwrap_or_else(|| {
config_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("archivr-auth.sqlite")
});
let app = routes::app(registry.clone(), auth_db_path.clone());
let bind_str = std::env::var("ARCHIVR_BIND") let bind_str = std::env::var("ARCHIVR_BIND")
.ok() .ok()
.or_else(|| registry.bind.clone()) .or_else(|| registry.bind.clone())
@ -25,15 +34,6 @@ async fn main() -> Result<()> {
.parse() .parse()
.with_context(|| format!("invalid bind address: {bind_str}"))?; .with_context(|| format!("invalid bind address: {bind_str}"))?;
// Warn when the server is reachable beyond localhost — it has no authentication.
if !addr.ip().is_loopback() {
eprintln!(
"warn: archivr-server is bound to {addr} — \
this server has no authentication. \
Only expose it on a trusted network."
);
}
let listener = tokio::net::TcpListener::bind(addr).await?; let listener = tokio::net::TcpListener::bind(addr).await?;
println!("archivr-server listening on http://{addr}"); println!("archivr-server listening on http://{addr}");
axum::serve(listener, app).await?; axum::serve(listener, app).await?;

View file

@ -16,10 +16,13 @@ pub struct MountedArchive {
pub struct ServerRegistry { pub struct ServerRegistry {
#[serde(default)] #[serde(default)]
pub archives: Vec<MountedArchive>, pub archives: Vec<MountedArchive>,
/// Optional bind address for the server. Defaults to `127.0.0.1:8080`. /// Optional bind address. Defaults to `127.0.0.1:8080`.
/// Set this to `0.0.0.0:8080` only on trusted networks — the server has no authentication.
#[serde(default)] #[serde(default)]
pub bind: Option<String>, pub bind: Option<String>,
/// Path to the server-level auth database.
/// Defaults to `archivr-auth.sqlite` in the same directory as the config file.
#[serde(default)]
pub auth_db_path: Option<std::path::PathBuf>,
} }
pub fn load_registry(path: &Path) -> Result<ServerRegistry> { pub fn load_registry(path: &Path) -> Result<ServerRegistry> {
@ -43,19 +46,16 @@ pub fn save_registry(path: &Path, registry: &ServerRegistry) -> Result<()> {
} }
pub fn validate_registry(registry: &ServerRegistry) -> Result<()> { pub fn validate_registry(registry: &ServerRegistry) -> Result<()> {
let mut ids = std::collections::HashSet::new(); let mut seen_ids = std::collections::HashSet::new();
for archive in &registry.archives { for archive in &registry.archives {
if archive.id.trim().is_empty() { if archive.id.trim().is_empty() {
bail!("archive id must not be empty"); bail!("archive id must not be empty");
} }
if !ids.insert(archive.id.as_str()) { if !seen_ids.insert(archive.id.clone()) {
bail!("duplicate archive id: {}", archive.id); bail!("duplicate archive id: {}", archive.id);
} }
if !archive.archive_path.ends_with(".archivr") { if archive.label.trim().is_empty() {
bail!( bail!("archive label must not be empty for id={}", archive.id);
"mounted archive path must point at a .archivr directory: {}",
archive.archive_path.display()
);
} }
} }
Ok(()) Ok(())
@ -84,6 +84,7 @@ mod tests {
archive_path: archive_path.clone(), archive_path: archive_path.clone(),
}], }],
bind: None, bind: None,
auth_db_path: None,
}; };
let path = temp.path().join("server.toml"); let path = temp.path().join("server.toml");
save_registry(&path, &registry).unwrap(); save_registry(&path, &registry).unwrap();
@ -109,6 +110,7 @@ mod tests {
}, },
], ],
bind: None, bind: None,
auth_db_path: None,
}; };
let err = validate_registry(&registry).unwrap_err().to_string(); let err = validate_registry(&registry).unwrap_err().to_string();
@ -136,6 +138,7 @@ mod tests {
let registry = ServerRegistry { let registry = ServerRegistry {
archives: vec![], archives: vec![],
bind: Some("0.0.0.0:8080".to_string()), bind: Some("0.0.0.0:8080".to_string()),
auth_db_path: None,
}; };
// validate_registry does not reject non-loopback bind — that's main's concern. // validate_registry does not reject non-loopback bind — that's main's concern.
assert!(validate_registry(&registry).is_ok()); assert!(validate_registry(&registry).is_ok());

View file

@ -1,4 +1,4 @@
// ── Security Boundary ──────────────────────────────────────────────────────── // ── Security Boundary ──────────────────────────────────────────────────────────────────
// All routes are currently trusted-local: no authentication or authorization // All routes are currently trusted-local: no authentication or authorization
// middleware is applied. The server is designed to bind on 127.0.0.1 only. // middleware is applied. The server is designed to bind on 127.0.0.1 only.
// //
@ -40,6 +40,7 @@ use crate::registry::{MountedArchive, ServerRegistry};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
registry: Arc<ServerRegistry>, registry: Arc<ServerRegistry>,
pub auth_db_path: Arc<std::path::PathBuf>,
} }
#[derive(Debug, serde::Deserialize, Default)] #[derive(Debug, serde::Deserialize, Default)]
@ -48,9 +49,10 @@ pub struct EntrySearchParams {
pub tag: Option<String>, pub tag: Option<String>,
} }
pub fn app(registry: ServerRegistry) -> Router { pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router {
let state = AppState { let state = AppState {
registry: Arc::new(registry), registry: Arc::new(registry),
auth_db_path: Arc::new(auth_db_path),
}; };
let static_dir = static_dir(); let static_dir = static_dir();
@ -192,6 +194,7 @@ async fn serve_entry_favicon(
.unwrap() .unwrap()
.into_response()) .into_response())
} }
async fn serve_blob( async fn serve_blob(
State(state): State<AppState>, State(state): State<AppState>,
Path((archive_id, sha256)): Path<(String, String)>, Path((archive_id, sha256)): Path<(String, String)>,
@ -387,8 +390,100 @@ mod tests {
use axum::http::{Request, StatusCode}; use axum::http::{Request, StatusCode};
use tower::ServiceExt; use tower::ServiceExt;
fn make_test_app() -> (Router, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let auth_path = dir.path().join("auth.sqlite");
// Seed owner so setup_guard passes in normal tests
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "test_hash_not_real").unwrap();
}
let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None };
(app(registry, auth_path), dir)
}
fn make_setup_test_app() -> (Router, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let auth_path = dir.path().join("auth.sqlite");
// NO owner seeded - for testing setup-required behavior
let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None };
(app(registry, auth_path), dir)
}
fn make_test_registry(dir: &tempfile::TempDir) -> (ServerRegistry, std::path::PathBuf, std::path::PathBuf) {
let paths = archivr_core::archive::initialize_archive(
dir.path(),
&dir.path().join("store"),
"test",
false,
).unwrap();
let auth_path = dir.path().join("auth.sqlite");
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
}
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "test".to_string(),
label: "Test".to_string(),
archive_path: paths.archive_path.clone(),
}],
bind: None,
auth_db_path: None,
};
(registry, paths.archive_path, auth_path)
}
fn make_test_entry(archive_path: &std::path::Path) -> archivr_core::database::ArchivedEntry {
let conn = database::open_or_initialize(archive_path).unwrap();
let user_id = database::ensure_default_user(&conn).unwrap();
let run = database::create_archive_run(&conn, user_id, 1).unwrap();
let si = database::upsert_source_identity(
&conn, "web", "page", None,
Some("https://example.com/test"),
"https://example.com/test",
)
.unwrap();
database::create_archived_entry(
&conn,
&database::NewEntry {
source_identity_id: si,
archive_run_id: run.id,
parent_entry_id: None,
root_entry_id: None,
created_by_user_id: user_id,
owned_by_user_id: user_id,
source_kind: "web".to_string(),
entity_kind: "page".to_string(),
title: Some("Test Entry".to_string()),
visibility: "private".to_string(),
representation_kind: "html".to_string(),
source_metadata_json: "{}".to_string(),
display_metadata_json: None,
},
)
.unwrap()
}
async fn body_json(response: axum::response::Response) -> serde_json::Value {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
fn json_body(payload: &serde_json::Value) -> Body {
Body::from(serde_json::to_vec(payload).unwrap())
}
#[tokio::test] #[tokio::test]
async fn archives_endpoint_lists_mounted_archives() { async fn archives_endpoint_lists_mounted_archives() {
let dir = tempfile::tempdir().unwrap();
let auth_path = dir.path().join("auth.sqlite");
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
}
let registry = ServerRegistry { let registry = ServerRegistry {
archives: vec![MountedArchive { archives: vec![MountedArchive {
id: "personal".to_string(), id: "personal".to_string(),
@ -396,8 +491,9 @@ mod tests {
archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"), archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"),
}], }],
bind: None, bind: None,
auth_db_path: None,
}; };
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives") .uri("/api/archives")
@ -412,7 +508,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn missing_archive_returns_404() { async fn missing_archive_returns_404() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/missing/entries") .uri("/api/archives/missing/entries")
@ -427,7 +524,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn artifact_missing_archive_returns_404() { async fn artifact_missing_archive_returns_404() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/nope/entries/entry_abc/artifacts/0") .uri("/api/archives/nope/entries/entry_abc/artifacts/0")
@ -450,6 +548,11 @@ mod tests {
) )
.unwrap(); .unwrap();
let archive_path = dir.path().join(".archivr"); let archive_path = dir.path().join(".archivr");
let auth_path = dir.path().join("auth.sqlite");
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
}
let registry = ServerRegistry { let registry = ServerRegistry {
archives: vec![MountedArchive { archives: vec![MountedArchive {
id: "test".to_string(), id: "test".to_string(),
@ -457,8 +560,9 @@ mod tests {
archive_path, archive_path,
}], }],
bind: None, bind: None,
auth_db_path: None,
}; };
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0") .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0")
@ -481,6 +585,11 @@ mod tests {
) )
.unwrap(); .unwrap();
let archive_path = dir.path().join(".archivr"); let archive_path = dir.path().join(".archivr");
let auth_path = dir.path().join("auth.sqlite");
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
}
let registry = ServerRegistry { let registry = ServerRegistry {
archives: vec![MountedArchive { archives: vec![MountedArchive {
id: "test".to_string(), id: "test".to_string(),
@ -488,8 +597,9 @@ mod tests {
archive_path, archive_path,
}], }],
bind: None, bind: None,
auth_db_path: None,
}; };
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99") .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99")
@ -574,6 +684,11 @@ mod tests {
.unwrap(); .unwrap();
drop(conn); // release before the HTTP handler opens the same db file drop(conn); // release before the HTTP handler opens the same db file
let auth_path = dir.path().join("auth.sqlite");
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
}
let registry = ServerRegistry { let registry = ServerRegistry {
archives: vec![MountedArchive { archives: vec![MountedArchive {
id: "test".to_string(), id: "test".to_string(),
@ -581,12 +696,13 @@ mod tests {
archive_path: paths.archive_path.clone(), archive_path: paths.archive_path.clone(),
}], }],
bind: None, bind: None,
auth_db_path: None,
}; };
let uri = format!( let uri = format!(
"/api/archives/test/entries/{}/artifacts/0", "/api/archives/test/entries/{}/artifacts/0",
entry.entry_uid entry.entry_uid
); );
let response = app(registry) let response = app(registry, auth_path)
.oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap()) .oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap())
.await .await
.unwrap(); .unwrap();
@ -595,7 +711,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn search_missing_archive_returns_404() { async fn search_missing_archive_returns_404() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/nope/entries/search?q=anything") .uri("/api/archives/nope/entries/search?q=anything")
@ -610,23 +727,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn search_empty_q_returns_ok() { async fn search_empty_q_returns_ok() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
archivr_core::archive::initialize_archive( let (registry, _, auth_path) = make_test_registry(&dir);
dir.path(), let response = app(registry, auth_path)
&dir.path().join("store"),
"test",
false,
)
.unwrap();
let archive_path = dir.path().join(".archivr");
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "test".to_string(),
label: "Test".to_string(),
archive_path,
}],
bind: None,
};
let response = app(registry)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/search") .uri("/api/archives/test/entries/search")
@ -641,23 +743,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn search_unknown_prefix_returns_400() { async fn search_unknown_prefix_returns_400() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
archivr_core::archive::initialize_archive( let (registry, _, auth_path) = make_test_registry(&dir);
dir.path(), let response = app(registry, auth_path)
&dir.path().join("store"),
"test",
false,
)
.unwrap();
let archive_path = dir.path().join(".archivr");
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "test".to_string(),
label: "Test".to_string(),
archive_path,
}],
bind: None,
};
let response = app(registry)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/search?q=unknownprefix%3Aval") .uri("/api/archives/test/entries/search?q=unknownprefix%3Aval")
@ -669,74 +756,12 @@ mod tests {
assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert_eq!(response.status(), StatusCode::BAD_REQUEST);
} }
// ---- helpers ----
fn make_test_registry(dir: &tempfile::TempDir) -> (ServerRegistry, std::path::PathBuf) {
let paths = archivr_core::archive::initialize_archive(
dir.path(),
&dir.path().join("store"),
"test",
false,
)
.unwrap();
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "test".to_string(),
label: "Test".to_string(),
archive_path: paths.archive_path.clone(),
}],
bind: None,
};
(registry, paths.archive_path)
}
fn make_test_entry(archive_path: &std::path::Path) -> archivr_core::database::ArchivedEntry {
let conn = database::open_or_initialize(archive_path).unwrap();
let user_id = database::ensure_default_user(&conn).unwrap();
let run = database::create_archive_run(&conn, user_id, 1).unwrap();
let si = database::upsert_source_identity(
&conn, "web", "page", None,
Some("https://example.com/test"),
"https://example.com/test",
)
.unwrap();
database::create_archived_entry(
&conn,
&database::NewEntry {
source_identity_id: si,
archive_run_id: run.id,
parent_entry_id: None,
root_entry_id: None,
created_by_user_id: user_id,
owned_by_user_id: user_id,
source_kind: "web".to_string(),
entity_kind: "page".to_string(),
title: Some("Test Entry".to_string()),
visibility: "private".to_string(),
representation_kind: "html".to_string(),
source_metadata_json: "{}".to_string(),
display_metadata_json: None,
},
)
.unwrap()
}
async fn body_json(response: axum::response::Response) -> serde_json::Value {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
fn json_body(payload: &serde_json::Value) -> Body {
Body::from(serde_json::to_vec(payload).unwrap())
}
// ---- tag route tests ---- // ---- tag route tests ----
#[tokio::test] #[tokio::test]
async fn test_list_tags_unknown_archive() { async fn test_list_tags_unknown_archive() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/ghost/tags") .uri("/api/archives/ghost/tags")
@ -750,7 +775,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_create_tag_unknown_archive() { async fn test_create_tag_unknown_archive() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -767,8 +793,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_create_tag_empty_path() { async fn test_create_tag_empty_path() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, _) = make_test_registry(&dir); let (registry, _, auth_path) = make_test_registry(&dir);
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -785,9 +811,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_tag_round_trip() { async fn test_tag_round_trip() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, _) = make_test_registry(&dir); let (registry, _, auth_path) = make_test_registry(&dir);
let create_response = app(registry.clone()) let create_response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -800,7 +826,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(create_response.status(), StatusCode::CREATED); assert_eq!(create_response.status(), StatusCode::CREATED);
let list_response = app(registry.clone()) let list_response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/tags") .uri("/api/archives/test/tags")
@ -823,13 +849,13 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_entry_tag_assign_and_remove() { async fn test_entry_tag_assign_and_remove() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, archive_path) = make_test_registry(&dir); let (registry, archive_path, auth_path) = make_test_registry(&dir);
let entry = make_test_entry(&archive_path); let entry = make_test_entry(&archive_path);
let entry_uid = entry.entry_uid.clone(); let entry_uid = entry.entry_uid.clone();
let entry_tags_uri = format!("/api/archives/test/entries/{entry_uid}/tags"); let entry_tags_uri = format!("/api/archives/test/entries/{entry_uid}/tags");
// Assign tag // Assign tag
let assign_response = app(registry.clone()) let assign_response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -845,7 +871,7 @@ mod tests {
let tag_uid = assigned_tag["tag_uid"].as_str().unwrap().to_string(); let tag_uid = assigned_tag["tag_uid"].as_str().unwrap().to_string();
// List entry tags — should contain the assigned tag // List entry tags — should contain the assigned tag
let list_response = app(registry.clone()) let list_response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri(&entry_tags_uri) .uri(&entry_tags_uri)
@ -860,7 +886,7 @@ mod tests {
// Remove tag // Remove tag
let delete_uri = format!("{entry_tags_uri}/{tag_uid}"); let delete_uri = format!("{entry_tags_uri}/{tag_uid}");
let delete_response = app(registry.clone()) let delete_response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("DELETE") .method("DELETE")
@ -873,7 +899,7 @@ mod tests {
assert_eq!(delete_response.status(), StatusCode::NO_CONTENT); assert_eq!(delete_response.status(), StatusCode::NO_CONTENT);
// List entry tags again — should be empty // List entry tags again — should be empty
let list2_response = app(registry.clone()) let list2_response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri(&entry_tags_uri) .uri(&entry_tags_uri)
@ -890,12 +916,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_search_with_tag_param() { async fn test_search_with_tag_param() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, archive_path) = make_test_registry(&dir); let (registry, archive_path, auth_path) = make_test_registry(&dir);
let entry = make_test_entry(&archive_path); let entry = make_test_entry(&archive_path);
let entry_uid = entry.entry_uid.clone(); let entry_uid = entry.entry_uid.clone();
// Assign /science tag to entry // Assign /science tag to entry
let assign_resp = app(registry.clone()) let assign_resp = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -909,7 +935,7 @@ mod tests {
assert_eq!(assign_resp.status(), StatusCode::CREATED, "assign tag should return 201"); assert_eq!(assign_resp.status(), StatusCode::CREATED, "assign tag should return 201");
// Search with ?tag=/science — entry should appear // Search with ?tag=/science — entry should appear
let response = app(registry.clone()) let response = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/search?tag=%2Fscience") .uri("/api/archives/test/entries/search?tag=%2Fscience")
@ -928,7 +954,7 @@ mod tests {
); );
// Search with ?tag=/art — should return empty // Search with ?tag=/art — should return empty
let response2 = app(registry.clone()) let response2 = app(registry.clone(), auth_path.clone())
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/search?tag=%2Fart") .uri("/api/archives/test/entries/search?tag=%2Fart")
@ -948,8 +974,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_list_entry_tags_unknown_entry() { async fn test_list_entry_tags_unknown_entry() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, _) = make_test_registry(&dir); let (registry, _, auth_path) = make_test_registry(&dir);
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/entries/ghost_uid/tags") .uri("/api/archives/test/entries/ghost_uid/tags")
@ -964,8 +990,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_assign_entry_tag_unknown_entry() { async fn test_assign_entry_tag_unknown_entry() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, _) = make_test_registry(&dir); let (registry, _, auth_path) = make_test_registry(&dir);
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -982,10 +1008,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_assign_entry_tag_empty_tag_path() { async fn test_assign_entry_tag_empty_tag_path() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, archive_path) = make_test_registry(&dir); let (registry, archive_path, auth_path) = make_test_registry(&dir);
let entry = make_test_entry(&archive_path); let entry = make_test_entry(&archive_path);
let entry_uid = entry.entry_uid.clone(); let entry_uid = entry.entry_uid.clone();
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -1002,8 +1028,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_remove_entry_tag_unknown_entry() { async fn test_remove_entry_tag_unknown_entry() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let (registry, _) = make_test_registry(&dir); let (registry, _, auth_path) = make_test_registry(&dir);
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("DELETE") .method("DELETE")
@ -1018,7 +1044,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn capture_rejects_empty_locator() { async fn capture_rejects_empty_locator() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -1034,7 +1061,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn capture_rejects_unknown_archive() { async fn capture_rejects_unknown_archive() {
let response = app(ServerRegistry::default()) let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot( .oneshot(
Request::builder() Request::builder()
.method("POST") .method("POST")
@ -1059,6 +1087,11 @@ mod tests {
) )
.unwrap(); .unwrap();
let archive_path = dir.path().join(".archivr"); let archive_path = dir.path().join(".archivr");
let auth_path = dir.path().join("auth.sqlite");
{
let conn = archivr_core::database::open_auth_db(&auth_path).unwrap();
archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap();
}
let registry = ServerRegistry { let registry = ServerRegistry {
archives: vec![MountedArchive { archives: vec![MountedArchive {
id: "test".to_string(), id: "test".to_string(),
@ -1066,8 +1099,9 @@ mod tests {
archive_path, archive_path,
}], }],
bind: None, bind: None,
auth_db_path: None,
}; };
let response = app(registry) let response = app(registry, auth_path)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/api/archives/test/blobs/0000000000000000000000000000000000000000000000000000000000000000") .uri("/api/archives/test/blobs/0000000000000000000000000000000000000000000000000000000000000000")