This commit is contained in:
2025-11-09 18:20:29 +01:00
parent 81571e4c64
commit da0f2ae8ab
10 changed files with 147 additions and 63 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ use axum::body::Body;
use axum::http::{header, Method, Request, StatusCode};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use common::{acquire_db_lock, body_to_vec, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use diesel::prelude::*;
use papercrate::models::ApiToken;
use papercrate::routes::webdav;
@@ -72,7 +72,7 @@ async fn api_token_crud_flow() -> Result<()> {
let username = "alice";
let password = "correct horse battery";
app.insert_user(username, password, "admin").await?;
app.insert_user(username, TestUserRole::Owner).await?;
let access_token = app.login_token(username, password).await?;
let legacy_set_id =
@@ -141,7 +141,7 @@ async fn webdav_basic_auth_uses_api_tokens() -> Result<()> {
let username = "bruce";
let password = "wayne";
app.insert_user(username, password, "admin").await?;
app.insert_user(username, TestUserRole::Owner).await?;
let access_token = app.login_token(username, password).await?;
let legacy_set_id =
+12 -12
View File
@@ -3,7 +3,7 @@ mod common;
use anyhow::{anyhow, Context, Result};
use axum::http::{header::SET_COOKIE, StatusCode};
use chrono::{Duration as ChronoDuration, Utc};
use common::{acquire_db_lock, body_to_vec, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use diesel::prelude::*;
use papercrate::auth::capability_sets::{ensure_capability_set, owner_capabilities};
use papercrate::auth::jwt::{AccessTokenContext, PrincipalKind};
@@ -80,7 +80,7 @@ async fn login_and_me_roundtrip() -> Result<()> {
let app = TestApp::new().await?;
let password = "s3cret";
app.insert_user("alice", password, "admin").await?;
app.insert_user("alice", TestUserRole::Owner).await?;
let (login, _) = login_with_session(&app, "alice", password).await?;
@@ -156,7 +156,7 @@ async fn passkey_register_start_creates_challenge() -> Result<()> {
let app = TestApp::new().await?;
let password = "secret";
app.insert_user("passkey-user", password, "admin").await?;
app.insert_user("passkey-user", TestUserRole::Owner).await?;
let (login, _) = login_with_session(&app, "passkey-user", password).await?;
@@ -197,7 +197,7 @@ async fn passkey_register_finish_rejects_unknown_challenge() -> Result<()> {
let app = TestApp::new().await?;
let password = "secret";
app.insert_user("passkey-register", password, "admin")
app.insert_user("passkey-register", TestUserRole::Owner)
.await?;
let (login, _) = login_with_session(&app, "passkey-register", password).await?;
@@ -227,7 +227,7 @@ async fn passkey_login_start_requires_passkey() -> Result<()> {
let app = TestApp::new().await?;
let password = "secret";
app.insert_user("passkey-login", password, "admin").await?;
app.insert_user("passkey-login", TestUserRole::Owner).await?;
let payload = PasskeyLoginStartPayload {
username: "passkey-login".to_string(),
@@ -288,7 +288,7 @@ async fn list_passkeys_returns_entries() -> Result<()> {
let app = TestApp::new().await?;
let password = "secret";
let user_id = app.insert_user("passkey-owner", password, "admin").await?;
let user_id = app.insert_user("passkey-owner", TestUserRole::Owner).await?;
app.insert_passkey(user_id, Some("Laptop")).await?;
let (session, _) = login_with_session(&app, "passkey-owner", password).await?;
@@ -314,7 +314,7 @@ async fn delete_passkey_soft_revokes() -> Result<()> {
let app = TestApp::new().await?;
let password = "secret";
let user_id = app.insert_user("passkey-delete", password, "admin").await?;
let user_id = app.insert_user("passkey-delete", TestUserRole::Owner).await?;
let passkey_id = app.insert_passkey(user_id, Some("Phone")).await?;
app.insert_passkey(user_id, Some("Backup")).await?;
let (session, _) = login_with_session(&app, "passkey-delete", password).await?;
@@ -349,7 +349,7 @@ async fn delete_passkey_prevents_last() -> Result<()> {
let app = TestApp::new().await?;
let password = "secret";
let user_id = app.insert_user("passkey-guard", password, "admin").await?;
let user_id = app.insert_user("passkey-guard", TestUserRole::Owner).await?;
let first_id = app.insert_passkey(user_id, Some("Key A")).await?;
let last_id = app.insert_passkey(user_id, Some("Key B")).await?;
let (session, _) = login_with_session(&app, "passkey-guard", password).await?;
@@ -409,7 +409,7 @@ async fn login_rejects_invalid_password() -> Result<()> {
let app = TestApp::new().await?;
let password = "valid";
app.insert_user("robin", password, "admin").await?;
app.insert_user("robin", TestUserRole::Owner).await?;
let payload = json!({ "username": "robin", "password": "wrong" });
let response = app.post_json("/api/auth/login", &payload, None).await?;
@@ -428,7 +428,7 @@ async fn refresh_rotates_refresh_token() -> Result<()> {
let app = TestApp::new().await?;
let password = "rotate";
app.insert_user("rita", password, "admin").await?;
app.insert_user("rita", TestUserRole::Owner).await?;
let (login, refresh_cookie) = login_with_session(&app, "rita", password).await?;
@@ -464,7 +464,7 @@ async fn logout_revokes_refresh_token() -> Result<()> {
let app = TestApp::new().await?;
let password = "logout";
app.insert_user("logan", password, "admin").await?;
app.insert_user("logan", TestUserRole::Owner).await?;
let (login, refresh_cookie) = login_with_session(&app, "logan", password).await?;
@@ -510,7 +510,7 @@ async fn login_returns_tenant_selection_when_multiple_memberships() -> Result<()
let app = TestApp::new().await?;
let password = "multipass";
let user_id = app.insert_user("multipass", password, "admin").await?;
let user_id = app.insert_user("multipass", TestUserRole::Owner).await?;
let secondary_name = "secondary".to_string();
let name_for_insert = secondary_name.clone();
+3 -3
View File
@@ -2,7 +2,7 @@ mod common;
use anyhow::{anyhow, Result};
use axum::http::StatusCode;
use common::TestApp;
use common::{TestApp, TestUserRole};
use diesel::prelude::*;
use papercrate::models::ApiCapability;
use serde_json::json;
@@ -42,7 +42,7 @@ async fn documents_routes_enforce_capabilities() -> Result<()> {
let app = TestApp::new().await?;
let password = "limited-docs";
let user_id = app.insert_user("limited-docs", password, "admin").await?;
let user_id = app.insert_user("limited-docs", TestUserRole::Owner).await?;
set_user_capabilities(&app, user_id, &[ApiCapability::DocumentsRead]).await?;
let token = app.login_token("limited-docs", password).await?;
@@ -79,7 +79,7 @@ async fn capability_set_routes_require_write_privilege() -> Result<()> {
let app = TestApp::new().await?;
let password = "caps-reader";
let user_id = app.insert_user("caps-reader", password, "admin").await?;
let user_id = app.insert_user("caps-reader", TestUserRole::Owner).await?;
set_user_capabilities(&app, user_id, &[ApiCapability::CapabilitySetsRead]).await?;
let token = app.login_token("caps-reader", password).await?;
+2 -2
View File
@@ -2,7 +2,7 @@ mod common;
use anyhow::Result;
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;
@@ -23,7 +23,7 @@ async fn capability_set_crud_flow() -> Result<()> {
let app = TestApp::new().await?;
let password = "caps-admin";
app.insert_user("caps", password, "admin").await?;
app.insert_user("caps", TestUserRole::Owner).await?;
let token = app.login_token("caps", password).await?;
// Initial list should contain system sets.
+14 -8
View File
@@ -51,6 +51,13 @@ static DB_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
const TEST_TENANT_NAME: &str = "test_tenant";
#[derive(Clone, Copy, Debug)]
pub enum TestUserRole {
Owner,
Member,
WebDav,
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct StoredObject {
@@ -241,9 +248,8 @@ impl TestApp {
Ok(format!("{}{}", root, key))
}
pub async fn insert_user(&self, username: &str, _password: &str, role: &str) -> Result<Uuid> {
pub async fn insert_user(&self, username: &str, role: TestUserRole) -> Result<Uuid> {
let username = username.to_string();
let role = role.to_string();
let tenant_id = self.ensure_default_tenant().await?;
let user_id = self
.with_conn(move |conn| {
@@ -256,10 +262,10 @@ impl TestApp {
.execute(conn)
.context("failed to insert user")?;
let capabilities = match role.as_str() {
"admin" => owner_capabilities(),
"webdav" => webdav_capabilities(),
_ => user_capabilities(),
let capabilities = match role {
TestUserRole::Owner => owner_capabilities(),
TestUserRole::Member => user_capabilities(),
TestUserRole::WebDav => webdav_capabilities(),
};
let capability_set = ensure_capability_set(conn, tenant_id, capabilities)
@@ -788,7 +794,7 @@ mod helper_tests {
let username = "helper-login";
let password = "irrelevant";
app.insert_user(username, password, "admin").await?;
app.insert_user(username, TestUserRole::Owner).await?;
let (access, refresh, refresh_id) = app.create_session(username).await?;
assert!(!access.is_empty(), "access token should not be empty");
@@ -812,7 +818,7 @@ mod helper_tests {
let app = TestApp::new().await?;
let username = "helper-passkey";
let password = "unused";
let user_id = app.insert_user(username, password, "admin").await?;
let user_id = app.insert_user(username, TestUserRole::Owner).await?;
let passkey_id = app.insert_passkey(user_id, Some("Laptop")).await?;
assert_ne!(passkey_id, Uuid::nil());
+2 -2
View File
@@ -1,7 +1,7 @@
mod common;
use anyhow::Result;
use common::{acquire_db_lock, body_to_vec, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;
@@ -53,7 +53,7 @@ impl TestContext {
let app = TestApp::new().await?;
let username = format!("{prefix}_user");
let password = format!("{prefix}_pw");
app.insert_user(&username, &password, "admin").await?;
app.insert_user(&username, TestUserRole::Owner).await?;
let token = app.login_token(&username, &password).await?;
let first_id =
+22 -22
View File
@@ -2,7 +2,7 @@ mod common;
use anyhow::{anyhow, Result};
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras};
use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras, TestUserRole};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -166,7 +166,7 @@ async fn upload_and_list_document() -> Result<()> {
let app = TestApp::new().await?;
let password = "passw0rd";
app.insert_user("dana", password, "admin").await?;
app.insert_user("dana", TestUserRole::Owner).await?;
let token = app.login_token("dana", password).await?;
let file_bytes = b"example document body".to_vec();
@@ -254,7 +254,7 @@ async fn upload_document_with_custom_title_sets_filename() -> Result<()> {
let app = TestApp::new().await?;
let password = "passw0rd";
app.insert_user("nora", password, "admin").await?;
app.insert_user("nora", TestUserRole::Owner).await?;
let token = app.login_token("nora", password).await?;
let file_bytes = b"example contract body".to_vec();
@@ -300,7 +300,7 @@ async fn asset_detail_uses_proxy_urls_when_configured() -> Result<()> {
let username = "proxy-assets";
let password = "secret";
app.insert_user(username, password, "admin").await?;
app.insert_user(username, TestUserRole::Owner).await?;
let token = app.login_token(username, password).await?;
let upload = app
@@ -382,7 +382,7 @@ async fn document_list_sorting_controls() -> Result<()> {
let app = TestApp::new().await?;
let password = "passw0rd";
app.insert_user("sorting", password, "admin").await?;
app.insert_user("sorting", TestUserRole::Owner).await?;
let token = app.login_token("sorting", password).await?;
let first = app
@@ -461,7 +461,7 @@ async fn duplicate_and_restore_document() -> Result<()> {
let app = TestApp::new().await?;
let password = "pass1234";
app.insert_user("sam", password, "admin").await?;
app.insert_user("sam", TestUserRole::Owner).await?;
let token = app.login_token("sam", password).await?;
let payload = b"same bytes".to_vec();
@@ -572,7 +572,7 @@ async fn upload_skips_existing_when_requested() -> Result<()> {
let app = TestApp::new().await?;
let password = "skip-doc";
app.insert_user("skip", password, "admin").await?;
app.insert_user("skip", TestUserRole::Owner).await?;
let token = app.login_token("skip", password).await?;
let primary_tag_payload = CreateTagPayload {
@@ -710,7 +710,7 @@ async fn filter_documents_without_tags() -> Result<()> {
let app = TestApp::new().await?;
let password = "tagfilter";
app.insert_user("tagfilter", password, "admin").await?;
app.insert_user("tagfilter", TestUserRole::Owner).await?;
let token = app.login_token("tagfilter", password).await?;
// Create a tag and upload a document that uses it.
@@ -785,7 +785,7 @@ async fn bulk_move_documents_to_folder() -> Result<()> {
let app = TestApp::new().await?;
let password = "bulkmove";
app.insert_user("mover", password, "admin").await?;
app.insert_user("mover", TestUserRole::Owner).await?;
let token = app.login_token("mover", password).await?;
let alpha = app
@@ -898,7 +898,7 @@ async fn bulk_add_tags_for_selection() -> Result<()> {
let app = TestApp::new().await?;
let password = "bulktags";
app.insert_user("tagger", password, "admin").await?;
app.insert_user("tagger", TestUserRole::Owner).await?;
let token = app.login_token("tagger", password).await?;
let first = app
@@ -1038,7 +1038,7 @@ async fn bulk_remove_tags_from_selection() -> Result<()> {
let app = TestApp::new().await?;
let password = "bulktagremove";
app.insert_user("tagrem", password, "admin").await?;
app.insert_user("tagrem", TestUserRole::Owner).await?;
let token = app.login_token("tagrem", password).await?;
let first = app
@@ -1166,7 +1166,7 @@ async fn bulk_reanalyze_selected_documents() -> Result<()> {
let app = TestApp::new().await?;
let password = "subsetrean";
app.insert_user("subset", password, "admin").await?;
app.insert_user("subset", TestUserRole::Owner).await?;
let token = app.login_token("subset", password).await?;
app.clear_jobs().await?;
@@ -1277,7 +1277,7 @@ async fn patch_document_updates_title_and_handles_conflict() -> Result<()> {
let app = TestApp::new().await?;
let password = "patch-title";
app.insert_user("editor", password, "admin").await?;
app.insert_user("editor", TestUserRole::Owner).await?;
let token = app.login_token("editor", password).await?;
let first_upload = app
@@ -1345,7 +1345,7 @@ async fn patch_document_updates_and_clears_issued_at() -> Result<()> {
let app = TestApp::new().await?;
let password = "patch-issued";
app.insert_user("scheduler", password, "admin").await?;
app.insert_user("scheduler", TestUserRole::Owner).await?;
let token = app.login_token("scheduler", password).await?;
let upload = app
@@ -1410,7 +1410,7 @@ async fn patch_document_metadata_merge_and_replace() -> Result<()> {
let app = TestApp::new().await?;
let password = "patch-meta";
app.insert_user("curator", password, "admin").await?;
app.insert_user("curator", TestUserRole::Owner).await?;
let token = app.login_token("curator", password).await?;
let initial_metadata = r#"{"existing":{"keep":true},"other":1}"#;
@@ -1489,7 +1489,7 @@ async fn patch_document_validation_errors() -> Result<()> {
let app = TestApp::new().await?;
let password = "patch-errors";
app.insert_user("auditor", password, "admin").await?;
app.insert_user("auditor", TestUserRole::Owner).await?;
let token = app.login_token("auditor", password).await?;
let upload = app
@@ -1612,7 +1612,7 @@ async fn patch_document_updates_multiple_fields() -> Result<()> {
let app = TestApp::new().await?;
let password = "patch-multi";
app.insert_user("planner", password, "admin").await?;
app.insert_user("planner", TestUserRole::Owner).await?;
let token = app.login_token("planner", password).await?;
let upload = app
@@ -1671,7 +1671,7 @@ async fn list_documents_by_status_filter() -> Result<()> {
let app = TestApp::new().await?;
let password = "statusfilter";
app.insert_user("statususer", password, "admin").await?;
app.insert_user("statususer", TestUserRole::Owner).await?;
let token = app.login_token("statususer", password).await?;
let upload = app
@@ -1725,7 +1725,7 @@ async fn purge_document_removes_data() -> Result<()> {
let app = TestApp::new().await?;
let password = "purge";
app.insert_user("purger", password, "admin").await?;
app.insert_user("purger", TestUserRole::Owner).await?;
let token = app.login_token("purger", password).await?;
let upload = app
@@ -1823,7 +1823,7 @@ async fn delete_document_requires_trash() -> Result<()> {
let app = TestApp::new().await?;
let password = "conflict";
app.insert_user("conflict-user", password, "admin").await?;
app.insert_user("conflict-user", TestUserRole::Owner).await?;
let token = app.login_token("conflict-user", password).await?;
let upload = app
@@ -1857,7 +1857,7 @@ async fn restore_document_to_original_and_custom_folder() -> Result<()> {
let app = TestApp::new().await?;
let password = "restoretest";
app.insert_user("restorer", password, "admin").await?;
app.insert_user("restorer", TestUserRole::Owner).await?;
let token = app.login_token("restorer", password).await?;
let upload = app
@@ -1960,7 +1960,7 @@ async fn list_document_versions_and_fetch_detail() -> Result<()> {
let app = TestApp::new().await?;
let password = "versionlist";
app.insert_user("versions", password, "admin").await?;
app.insert_user("versions", TestUserRole::Owner).await?;
let token = app.login_token("versions", password).await?;
let upload = app
+78
View File
@@ -0,0 +1,78 @@
mod common;
use anyhow::Result;
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use serde::Deserialize;
#[derive(Deserialize)]
struct DocumentDetail {
document: DocumentInfo,
}
#[derive(Deserialize)]
struct DocumentInfo {
current_version: Option<DocumentVersion>,
}
#[derive(Deserialize)]
struct DocumentVersion {
download_path: String,
}
#[tokio::test]
async fn document_download_redirects_when_proxy_disabled() -> Result<()> {
let _lock = acquire_db_lock().await;
let app = TestApp::new().await?;
let username = "download-user";
let password = "secret";
app.insert_user(username, TestUserRole::Owner).await?;
let token = app.login_token(username, password).await?;
let upload = app
.upload_document(
"/api/documents",
"download.pdf",
"application/pdf",
b"dummy",
None,
&token,
)
.await?;
assert_eq!(upload.status(), StatusCode::CREATED);
let body = body_to_vec(upload.into_body()).await?;
let detail: DocumentDetail = serde_json::from_slice(&body)?;
let download_path = detail
.document
.current_version
.as_ref()
.expect("missing version")
.download_path
.clone();
let redirect = app.get(&download_path, None).await?;
assert_eq!(redirect.status(), StatusCode::TEMPORARY_REDIRECT);
let location = redirect
.headers()
.get("location")
.expect("redirect location header")
.to_str()
.expect("location utf8");
assert!(location.starts_with("https://fake-storage/"));
app.cleanup().await?;
Ok(())
}
#[tokio::test]
async fn download_with_invalid_token_is_rejected() -> Result<()> {
let _lock = acquire_db_lock().await;
let app = TestApp::new().await?;
let response = app.get("/download/not-a-token", None).await?;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
app.cleanup().await?;
Ok(())
}
+8 -8
View File
@@ -2,7 +2,7 @@ mod common;
use anyhow::Result;
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
@@ -66,7 +66,7 @@ async fn folder_move_and_delete_flow() -> Result<()> {
let app = TestApp::new().await?;
let password = "folderpass";
app.insert_user("folder-admin", password, "admin").await?;
app.insert_user("folder-admin", TestUserRole::Owner).await?;
let token = app.login_token("folder-admin", password).await?;
let folder_resp = app
@@ -156,7 +156,7 @@ async fn folder_tree_lists_hierarchy() -> Result<()> {
let app = TestApp::new().await?;
let password = "folderpass";
app.insert_user("folder-tree", password, "admin").await?;
app.insert_user("folder-tree", TestUserRole::Owner).await?;
let token = app.login_token("folder-tree", password).await?;
let alpha_resp = app
@@ -221,7 +221,7 @@ async fn update_folder_parent_to_root() -> Result<()> {
let app = TestApp::new().await?;
let password = "rootpass";
app.insert_user("root-admin", password, "admin").await?;
app.insert_user("root-admin", TestUserRole::Owner).await?;
let token = app.login_token("root-admin", password).await?;
// Create a parent folder under root
@@ -293,7 +293,7 @@ async fn ensure_path_creates_nested_folders() -> Result<()> {
let app = TestApp::new().await?;
let password = "pathpass";
app.insert_user("path-admin", password, "admin").await?;
app.insert_user("path-admin", TestUserRole::Owner).await?;
let token = app.login_token("path-admin", password).await?;
let base_path = EnsureFolderPath {
@@ -370,7 +370,7 @@ async fn create_folder_is_idempotent() -> Result<()> {
let app = TestApp::new().await?;
let password = "idempotent";
app.insert_user("folders-idem", password, "admin").await?;
app.insert_user("folders-idem", TestUserRole::Owner).await?;
let token = app.login_token("folders-idem", password).await?;
let payload = CreateFolder {
@@ -414,7 +414,7 @@ async fn ensure_folder_path_is_idempotent() -> Result<()> {
let app = TestApp::new().await?;
let password = "pathpass";
app.insert_user("path-admin", password, "admin").await?;
app.insert_user("path-admin", TestUserRole::Owner).await?;
let token = app.login_token("path-admin", password).await?;
let segments = ["500 Immobilien", "501 Kreuzweg 2", "501.01 Rechtliches"];
@@ -482,7 +482,7 @@ async fn folder_rename_updates_name_and_child_paths() -> Result<()> {
let app = TestApp::new().await?;
let password = "renamepass";
app.insert_user("rename-admin", password, "admin").await?;
app.insert_user("rename-admin", TestUserRole::Owner).await?;
let token = app.login_token("rename-admin", password).await?;
let parent_resp = app
+3 -3
View File
@@ -2,7 +2,7 @@ mod common;
use anyhow::{anyhow, Result};
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
use diesel::prelude::*;
use papercrate::auth::capability_sets::{ensure_capability_set, owner_capabilities};
use papercrate::models::{NewUser, NewUserMembership, Tag, TenantStatus};
@@ -57,7 +57,7 @@ async fn tag_assignment_flow() -> Result<()> {
let app = TestApp::new().await?;
let password = "tagpass";
app.insert_user("tagger", password, "admin").await?;
app.insert_user("tagger", TestUserRole::Owner).await?;
let token = app.login_token("tagger", password).await?;
let upload = app
@@ -186,7 +186,7 @@ async fn tags_are_isolated_between_tenants() -> Result<()> {
let app = TestApp::new().await?;
let password_a = "tenant-a";
app.insert_user("alice", password_a, "admin").await?;
app.insert_user("alice", TestUserRole::Owner).await?;
let token_a = app.login_token("alice", password_a).await?;
let shared_label = "Shared Label";