2
This commit is contained in:
@@ -14,10 +14,7 @@ struct AuthenticatedUser {
|
||||
#[tokio::test]
|
||||
async fn login_and_me_roundtrip() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let Some(app) = TestApp::new().await? else {
|
||||
eprintln!("skipping test: TEST_DATABASE_URL not set");
|
||||
return Ok(());
|
||||
};
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "s3cret";
|
||||
app.insert_user("alice", password, "admin").await?;
|
||||
|
||||
@@ -17,7 +17,7 @@ use once_cell::sync::Lazy;
|
||||
use paperless_backend::auth::jwt::JwtService;
|
||||
use paperless_backend::config::AppConfig;
|
||||
use paperless_backend::db::{self, PgPool};
|
||||
use paperless_backend::models::NewUser;
|
||||
use paperless_backend::models::{Job, NewUser};
|
||||
use paperless_backend::routes;
|
||||
use paperless_backend::state::AppState;
|
||||
use paperless_backend::storage::ObjectStorage;
|
||||
@@ -70,6 +70,14 @@ impl ObjectStorage for FakeStorage {
|
||||
expires_in.as_secs()
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_object(&self, key: &str) -> Result<Vec<u8>> {
|
||||
let guard = self.objects.lock().await;
|
||||
guard
|
||||
.get(key)
|
||||
.map(|obj| obj.bytes.clone())
|
||||
.ok_or_else(|| anyhow!("object {key} missing"))
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeStorage {
|
||||
@@ -93,11 +101,9 @@ pub struct TestApp {
|
||||
}
|
||||
|
||||
impl TestApp {
|
||||
pub async fn new() -> Result<Option<Self>> {
|
||||
let database_url = match env::var("TEST_DATABASE_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
pub async fn new() -> Result<Self> {
|
||||
let database_url = env::var("TEST_DATABASE_URL")
|
||||
.context("TEST_DATABASE_URL must be set for integration tests")?;
|
||||
|
||||
let config = AppConfig {
|
||||
database_url: database_url.clone(),
|
||||
@@ -123,11 +129,11 @@ impl TestApp {
|
||||
let state = AppState::new(pool.clone(), config, storage_for_state, jwt);
|
||||
let router = routes::create_router(state.clone());
|
||||
|
||||
Ok(Some(Self {
|
||||
Ok(Self {
|
||||
state,
|
||||
router,
|
||||
storage,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn cleanup(&self) -> Result<()> {
|
||||
@@ -199,6 +205,34 @@ impl TestApp {
|
||||
Ok(parsed.access_token)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn clear_jobs(&self) -> Result<()> {
|
||||
self.with_conn(|conn| {
|
||||
use paperless_backend::schema::jobs::dsl::jobs as jobs_table;
|
||||
diesel::delete(jobs_table)
|
||||
.execute(conn)
|
||||
.context("failed to clear jobs")?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn jobs_by_type(&self, ty: &str) -> Result<Vec<Job>> {
|
||||
let ty = ty.to_string();
|
||||
self.with_conn(move |conn| {
|
||||
use paperless_backend::schema::jobs::dsl::{
|
||||
job_type as job_type_col, jobs as jobs_table,
|
||||
};
|
||||
let rows = jobs_table
|
||||
.filter(job_type_col.eq(&ty))
|
||||
.load::<Job>(conn)
|
||||
.context("failed to load jobs")?;
|
||||
Ok(rows)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn post_json<T: Serialize + ?Sized>(
|
||||
&self,
|
||||
path: &str,
|
||||
|
||||
@@ -11,6 +11,7 @@ use uuid::Uuid;
|
||||
struct DocumentDetail {
|
||||
document: DocumentInfo,
|
||||
current_version: DocumentVersion,
|
||||
assets: Vec<DocumentAssetInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -24,10 +25,17 @@ struct DocumentInfo {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DocumentVersion {
|
||||
id: Uuid,
|
||||
s3_key: String,
|
||||
size_bytes: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DocumentAssetInfo {
|
||||
id: Uuid,
|
||||
asset_type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DocumentListItem {
|
||||
id: Uuid,
|
||||
@@ -40,13 +48,23 @@ struct DocumentDownload {
|
||||
filename: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BulkReanalyze {
|
||||
queued: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnalyzeJobPayload {
|
||||
document_id: Uuid,
|
||||
document_version_id: Uuid,
|
||||
#[serde(default)]
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_and_list_document() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let Some(app) = TestApp::new().await? else {
|
||||
eprintln!("skipping test: TEST_DATABASE_URL not set");
|
||||
return Ok(());
|
||||
};
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "passw0rd";
|
||||
app.insert_user("dana", password, "admin").await?;
|
||||
@@ -63,7 +81,7 @@ async fn upload_and_list_document() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(upload.status(), StatusCode::OK);
|
||||
assert_eq!(upload.status(), StatusCode::CREATED);
|
||||
let body = body_to_vec(upload.into_body()).await?;
|
||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||
|
||||
@@ -72,6 +90,7 @@ async fn upload_and_list_document() -> Result<()> {
|
||||
assert_eq!(detail.document.deleted_at, None);
|
||||
assert!(detail.document.tags.is_empty());
|
||||
assert_eq!(detail.current_version.size_bytes, file_bytes.len() as i64);
|
||||
assert!(detail.assets.is_empty());
|
||||
|
||||
let stored = app
|
||||
.storage()
|
||||
@@ -79,6 +98,7 @@ async fn upload_and_list_document() -> Result<()> {
|
||||
.await
|
||||
.expect("object stored");
|
||||
assert_eq!(stored.bytes, file_bytes);
|
||||
assert_eq!(app.storage().object_count().await, 1);
|
||||
|
||||
let response = app.get("/api/documents", Some(&token)).await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
@@ -108,10 +128,7 @@ async fn upload_and_list_document() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn duplicate_and_restore_document() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let Some(app) = TestApp::new().await? else {
|
||||
eprintln!("skipping test: TEST_DATABASE_URL not set");
|
||||
return Ok(());
|
||||
};
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "pass1234";
|
||||
app.insert_user("sam", password, "admin").await?;
|
||||
@@ -128,6 +145,7 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(first.status(), StatusCode::CREATED);
|
||||
let first_body = body_to_vec(first.into_body()).await?;
|
||||
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||
|
||||
@@ -147,6 +165,8 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
||||
|
||||
assert_eq!(first_detail.document.id, second_detail.document.id);
|
||||
assert_eq!(second_detail.document.deleted_at, None);
|
||||
assert!(second_detail.assets.is_empty());
|
||||
assert_eq!(app.storage().object_count().await, 1);
|
||||
|
||||
let delete = app
|
||||
.delete(
|
||||
@@ -166,11 +186,89 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(third.status(), StatusCode::OK);
|
||||
let third_body = body_to_vec(third.into_body()).await?;
|
||||
let third_detail: DocumentDetail = serde_json::from_slice(&third_body)?;
|
||||
|
||||
assert_eq!(third_detail.document.id, first_detail.document.id);
|
||||
assert_eq!(third_detail.document.deleted_at, None);
|
||||
assert_eq!(app.storage().object_count().await, 1);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_reanalyze_documents() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "bulkpass";
|
||||
app.insert_user("alex", password, "admin").await?;
|
||||
let token = app.login_token("alex", password).await?;
|
||||
|
||||
app.clear_jobs().await?;
|
||||
|
||||
let first_bytes = b"first doc";
|
||||
let first = app
|
||||
.upload_document(
|
||||
"/api/documents",
|
||||
"first.txt",
|
||||
"text/plain",
|
||||
first_bytes,
|
||||
None,
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(first.status(), StatusCode::CREATED);
|
||||
let first_body = body_to_vec(first.into_body()).await?;
|
||||
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||
|
||||
let second_bytes = b"second doc";
|
||||
let second = app
|
||||
.upload_document(
|
||||
"/api/documents",
|
||||
"second.txt",
|
||||
"text/plain",
|
||||
second_bytes,
|
||||
None,
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(second.status(), StatusCode::CREATED);
|
||||
let second_body = body_to_vec(second.into_body()).await?;
|
||||
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
||||
|
||||
app.clear_jobs().await?;
|
||||
|
||||
let response = app
|
||||
.post_json(
|
||||
"/api/documents/reanalyze",
|
||||
&serde_json::json!({}),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::ACCEPTED);
|
||||
let body = body_to_vec(response.into_body()).await?;
|
||||
let bulk: BulkReanalyze = serde_json::from_slice(&body)?;
|
||||
assert_eq!(bulk.queued, 2);
|
||||
|
||||
let jobs = app.jobs_by_type("analyze-document").await?;
|
||||
assert_eq!(jobs.len(), 2);
|
||||
let mut payload_docs = Vec::new();
|
||||
for job in jobs {
|
||||
let payload: AnalyzeJobPayload = serde_json::from_value(job.payload)?;
|
||||
assert!(payload.force);
|
||||
payload_docs.push((payload.document_id, payload.document_version_id));
|
||||
}
|
||||
|
||||
let mut expected = vec![
|
||||
(first_detail.document.id, first_detail.current_version.id),
|
||||
(second_detail.document.id, second_detail.current_version.id),
|
||||
];
|
||||
payload_docs.sort();
|
||||
expected.sort();
|
||||
assert_eq!(payload_docs, expected);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
|
||||
@@ -33,6 +33,12 @@ struct CreateFolder<'a> {
|
||||
parent_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EnsureFolderPath<'a> {
|
||||
parent_id: Option<Uuid>,
|
||||
segments: &'a [&'a str],
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MoveDocumentRequest {
|
||||
folder_id: Option<Uuid>,
|
||||
@@ -46,10 +52,7 @@ struct DocumentDetail {
|
||||
#[tokio::test]
|
||||
async fn folder_move_and_delete_flow() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let Some(app) = TestApp::new().await? else {
|
||||
eprintln!("skipping test: TEST_DATABASE_URL not set");
|
||||
return Ok(());
|
||||
};
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "folderpass";
|
||||
app.insert_user("folder-admin", password, "admin").await?;
|
||||
@@ -135,3 +138,80 @@ async fn folder_move_and_delete_flow() -> Result<()> {
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_path_creates_nested_folders() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "pathpass";
|
||||
app.insert_user("path-admin", password, "admin").await?;
|
||||
let token = app.login_token("path-admin", password).await?;
|
||||
|
||||
let base_path = EnsureFolderPath {
|
||||
parent_id: None,
|
||||
segments: &["Team", "Engineering", "Backend"],
|
||||
};
|
||||
let first_resp = app
|
||||
.post_json("/api/folders/path", &base_path, Some(&token))
|
||||
.await?;
|
||||
assert_eq!(first_resp.status(), StatusCode::OK);
|
||||
let first_body = body_to_vec(first_resp.into_body()).await?;
|
||||
let first_folder: FolderResponse = serde_json::from_slice(&first_body)?;
|
||||
|
||||
let second_resp = app
|
||||
.post_json("/api/folders/path", &base_path, Some(&token))
|
||||
.await?;
|
||||
assert_eq!(second_resp.status(), StatusCode::OK);
|
||||
let second_body = body_to_vec(second_resp.into_body()).await?;
|
||||
let second_folder: FolderResponse = serde_json::from_slice(&second_body)?;
|
||||
assert_eq!(second_folder.folder.id, first_folder.folder.id);
|
||||
|
||||
let engineering_resp = app
|
||||
.post_json(
|
||||
"/api/folders/path",
|
||||
&EnsureFolderPath {
|
||||
parent_id: None,
|
||||
segments: &["Team", "Engineering"],
|
||||
},
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(engineering_resp.status(), StatusCode::OK);
|
||||
let engineering_body = body_to_vec(engineering_resp.into_body()).await?;
|
||||
let engineering_folder: FolderResponse = serde_json::from_slice(&engineering_body)?;
|
||||
assert_ne!(engineering_folder.folder.id, first_folder.folder.id);
|
||||
|
||||
let infra_resp = app
|
||||
.post_json(
|
||||
"/api/folders/path",
|
||||
&EnsureFolderPath {
|
||||
parent_id: Some(engineering_folder.folder.id),
|
||||
segments: &["Infrastructure"],
|
||||
},
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(infra_resp.status(), StatusCode::OK);
|
||||
let infra_body = body_to_vec(infra_resp.into_body()).await?;
|
||||
let infra_folder: FolderResponse = serde_json::from_slice(&infra_body)?;
|
||||
assert_ne!(infra_folder.folder.id, engineering_folder.folder.id);
|
||||
|
||||
let infra_dupe_resp = app
|
||||
.post_json(
|
||||
"/api/folders/path",
|
||||
&EnsureFolderPath {
|
||||
parent_id: Some(engineering_folder.folder.id),
|
||||
segments: &["Infrastructure"],
|
||||
},
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(infra_dupe_resp.status(), StatusCode::OK);
|
||||
let infra_dupe_body = body_to_vec(infra_dupe_resp.into_body()).await?;
|
||||
let infra_dupe_folder: FolderResponse = serde_json::from_slice(&infra_dupe_body)?;
|
||||
assert_eq!(infra_dupe_folder.folder.id, infra_folder.folder.id);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ struct AssignTagsRequest {
|
||||
#[tokio::test]
|
||||
async fn tag_assignment_flow() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let Some(app) = TestApp::new().await? else {
|
||||
eprintln!("skipping test: TEST_DATABASE_URL not set");
|
||||
return Ok(());
|
||||
};
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "tagpass";
|
||||
app.insert_user("tagger", password, "admin").await?;
|
||||
@@ -55,6 +52,7 @@ async fn tag_assignment_flow() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(upload.status(), StatusCode::CREATED);
|
||||
let upload_body = body_to_vec(upload.into_body()).await?;
|
||||
let detail: DocumentDetail = serde_json::from_slice(&upload_body)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user