caps and delete
This commit is contained in:
+209
-47
@@ -1,12 +1,16 @@
|
||||
mod common;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use axum::http::StatusCode;
|
||||
use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use papercrate::jobs::{mark_job_succeeded, JOB_PURGE_DOCUMENT};
|
||||
use papercrate::models::Job;
|
||||
use papercrate::workers::{purge::PurgeDocumentJob, JobExecution, JobHandler};
|
||||
use std::sync::Arc;
|
||||
#[derive(Deserialize)]
|
||||
struct DocumentDetail {
|
||||
document: DocumentInfo,
|
||||
@@ -17,6 +21,8 @@ struct ApiErrorResponse {
|
||||
error: String,
|
||||
#[serde(default)]
|
||||
code: Option<String>,
|
||||
#[serde(default)]
|
||||
details: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -291,15 +297,16 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
let status = first.status();
|
||||
assert!(
|
||||
status == StatusCode::OK
|
||||
|| status == StatusCode::CREATED
|
||||
|| status == StatusCode::NO_CONTENT
|
||||
);
|
||||
}
|
||||
let first_status = first.status();
|
||||
let first_body = body_to_vec(first.into_body()).await?;
|
||||
assert!(
|
||||
first_status == StatusCode::OK
|
||||
|| first_status == StatusCode::CREATED
|
||||
|| first_status == StatusCode::NO_CONTENT,
|
||||
"unexpected first upload status {} with body {}",
|
||||
first_status,
|
||||
String::from_utf8_lossy(&first_body)
|
||||
);
|
||||
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||
|
||||
let second = app
|
||||
@@ -312,59 +319,69 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
let status = second.status();
|
||||
assert!(
|
||||
status == StatusCode::OK
|
||||
|| status == StatusCode::CREATED
|
||||
|| status == StatusCode::NO_CONTENT
|
||||
);
|
||||
}
|
||||
let second_status = second.status();
|
||||
let second_body = body_to_vec(second.into_body()).await?;
|
||||
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
||||
|
||||
assert_eq!(first_detail.document.id, second_detail.document.id);
|
||||
assert_eq!(second_detail.document.deleted_at, None);
|
||||
assert!(second_detail
|
||||
.document
|
||||
.current_version
|
||||
assert_eq!(second_status, StatusCode::CONFLICT);
|
||||
let second_error: ApiErrorResponse = serde_json::from_slice(&second_body)?;
|
||||
assert_eq!(second_error.code.as_deref(), Some("duplicate_document"));
|
||||
let conflict_id = second_error
|
||||
.details
|
||||
.as_ref()
|
||||
.expect("second current version")
|
||||
.assets
|
||||
.is_empty());
|
||||
.and_then(|details| details.get("conflict_document_id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(|value| Uuid::parse_str(value).ok())
|
||||
.expect("conflict_document_id present");
|
||||
assert_eq!(conflict_id, first_detail.document.id);
|
||||
assert_eq!(app.storage().object_count().await, 1);
|
||||
|
||||
let delete = app
|
||||
.delete(
|
||||
&format!("/api/documents/{}", first_detail.document.id),
|
||||
.post_json(
|
||||
&format!("/api/documents/{}/trash", first_detail.document.id),
|
||||
&json!({}),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(delete.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let third = app
|
||||
.upload_document(
|
||||
.upload_document_with_extras(
|
||||
"/api/documents",
|
||||
"dup.bin",
|
||||
"application/octet-stream",
|
||||
&payload,
|
||||
None,
|
||||
UploadExtras {
|
||||
title: None,
|
||||
metadata_json: None,
|
||||
tag_ids_json: None,
|
||||
correspondents_json: None,
|
||||
issued_at: None,
|
||||
skip_existing: Some(false),
|
||||
},
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
let status = third.status();
|
||||
assert!(
|
||||
status == StatusCode::OK
|
||||
|| status == StatusCode::CREATED
|
||||
|| status == StatusCode::NO_CONTENT
|
||||
);
|
||||
}
|
||||
let third_status = third.status();
|
||||
let third_body = body_to_vec(third.into_body()).await?;
|
||||
assert!(
|
||||
third_status == StatusCode::OK
|
||||
|| third_status == StatusCode::CREATED
|
||||
|| third_status == StatusCode::NO_CONTENT,
|
||||
"unexpected third upload status {} with body {}",
|
||||
third_status,
|
||||
String::from_utf8_lossy(&third_body)
|
||||
);
|
||||
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!(third_detail
|
||||
.document
|
||||
.current_version
|
||||
.as_ref()
|
||||
.expect("third current version")
|
||||
.assets
|
||||
.is_empty());
|
||||
assert_eq!(app.storage().object_count().await, 1);
|
||||
|
||||
app.cleanup().await?;
|
||||
@@ -406,7 +423,7 @@ async fn upload_skips_existing_when_requested() -> Result<()> {
|
||||
tag_ids_json: Some(primary_tag_ids.as_str()),
|
||||
correspondents_json: None,
|
||||
issued_at: None,
|
||||
skip_existing: false,
|
||||
skip_existing: None,
|
||||
};
|
||||
|
||||
let first_upload = app
|
||||
@@ -456,7 +473,7 @@ async fn upload_skips_existing_when_requested() -> Result<()> {
|
||||
tag_ids_json: Some(alt_tag_ids.as_str()),
|
||||
correspondents_json: None,
|
||||
issued_at: None,
|
||||
skip_existing: true,
|
||||
skip_existing: Some(true),
|
||||
};
|
||||
|
||||
let skip_resp = app
|
||||
@@ -470,7 +487,18 @@ async fn upload_skips_existing_when_requested() -> Result<()> {
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(skip_resp.status(), StatusCode::NO_CONTENT);
|
||||
assert_eq!(skip_resp.status(), StatusCode::CONFLICT);
|
||||
let skip_body = body_to_vec(skip_resp.into_body()).await?;
|
||||
let skip_error: ApiErrorResponse = serde_json::from_slice(&skip_body)?;
|
||||
assert_eq!(skip_error.code.as_deref(), Some("duplicate_document"));
|
||||
let conflict_id = skip_error
|
||||
.details
|
||||
.as_ref()
|
||||
.and_then(|details| details.get("conflict_document_id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(|value| Uuid::parse_str(value).ok())
|
||||
.expect("conflict_document_id present");
|
||||
assert_eq!(conflict_id, first_detail.document.id);
|
||||
|
||||
let fetch = app
|
||||
.get(
|
||||
@@ -1407,8 +1435,9 @@ async fn list_documents_by_status_filter() -> Result<()> {
|
||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||
|
||||
let delete_resp = app
|
||||
.delete(
|
||||
&format!("/api/documents/{}", detail.document.id),
|
||||
.post_json(
|
||||
&format!("/api/documents/{}/trash", detail.document.id),
|
||||
&json!({}),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
@@ -1437,6 +1466,137 @@ async fn list_documents_by_status_filter() -> Result<()> {
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn purge_document_removes_data() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "purge";
|
||||
app.insert_user("purger", password, "admin").await?;
|
||||
let token = app.login_token("purger", password).await?;
|
||||
|
||||
let upload = app
|
||||
.upload_document(
|
||||
"/api/documents",
|
||||
"purge.bin",
|
||||
"application/octet-stream",
|
||||
b"permanent",
|
||||
None,
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
let body = body_to_vec(upload.into_body()).await?;
|
||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||
let document_id = detail.document.id;
|
||||
|
||||
assert_eq!(app.storage().object_count().await, 1);
|
||||
|
||||
let trash_resp = app
|
||||
.post_json(
|
||||
&format!("/api/documents/{}/trash", document_id),
|
||||
&json!({}),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(trash_resp.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let delete_resp = app
|
||||
.delete(&format!("/api/documents/{}", document_id), Some(&token))
|
||||
.await?;
|
||||
assert_eq!(delete_resp.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let duplicate_delete = app
|
||||
.delete(&format!("/api/documents/{}", document_id), Some(&token))
|
||||
.await?;
|
||||
assert_eq!(duplicate_delete.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let purge_job_count: i64 = app
|
||||
.with_conn(|conn| {
|
||||
use diesel::dsl::count_star;
|
||||
use diesel::prelude::*;
|
||||
use papercrate::schema::jobs::dsl::*;
|
||||
|
||||
let count: i64 = jobs
|
||||
.filter(job_type.eq(JOB_PURGE_DOCUMENT))
|
||||
.select(count_star())
|
||||
.get_result(conn)?;
|
||||
Ok(count)
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(purge_job_count, 1);
|
||||
|
||||
let job: Job = app
|
||||
.with_conn(|conn| {
|
||||
use diesel::prelude::*;
|
||||
use papercrate::schema::jobs::dsl::*;
|
||||
|
||||
let job = jobs
|
||||
.filter(job_type.eq(JOB_PURGE_DOCUMENT))
|
||||
.order(created_at.desc())
|
||||
.first(conn)?;
|
||||
Ok(job)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let handler = PurgeDocumentJob::new();
|
||||
let state = Arc::new(app.state.clone());
|
||||
let storage = app
|
||||
.state
|
||||
.storage_for_tenant(job.tenant_id)
|
||||
.map_err(|err| anyhow!("tenant storage unavailable: {err:?}"))?;
|
||||
let execution = handler.handle(state, job.clone(), storage).await;
|
||||
assert!(matches!(execution, JobExecution::Success));
|
||||
|
||||
app.with_conn(move |conn| {
|
||||
mark_job_succeeded(conn, job.id)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let fetch = app
|
||||
.get(&format!("/api/documents/{}", document_id), Some(&token))
|
||||
.await?;
|
||||
assert_eq!(fetch.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
assert_eq!(app.storage().object_count().await, 0);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_document_requires_trash() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "conflict";
|
||||
app.insert_user("conflict-user", password, "admin").await?;
|
||||
let token = app.login_token("conflict-user", password).await?;
|
||||
|
||||
let upload = app
|
||||
.upload_document(
|
||||
"/api/documents",
|
||||
"conflict.bin",
|
||||
"application/octet-stream",
|
||||
b"restore",
|
||||
None,
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
let body = body_to_vec(upload.into_body()).await?;
|
||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||
|
||||
let delete_resp = app
|
||||
.delete(
|
||||
&format!("/api/documents/{}", detail.document.id),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(delete_resp.status(), StatusCode::CONFLICT);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restore_document_to_original_and_custom_folder() -> Result<()> {
|
||||
@@ -1461,8 +1621,9 @@ async fn restore_document_to_original_and_custom_folder() -> Result<()> {
|
||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||
|
||||
let delete_resp = app
|
||||
.delete(
|
||||
&format!("/api/documents/{}", detail.document.id),
|
||||
.post_json(
|
||||
&format!("/api/documents/{}/trash", detail.document.id),
|
||||
&json!({}),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
@@ -1503,8 +1664,9 @@ async fn restore_document_to_original_and_custom_folder() -> Result<()> {
|
||||
let folder: FolderResponse = serde_json::from_slice(&folder_body)?;
|
||||
|
||||
let delete_again = app
|
||||
.delete(
|
||||
&format!("/api/documents/{}", detail.document.id),
|
||||
.post_json(
|
||||
&format!("/api/documents/{}/trash", detail.document.id),
|
||||
&json!({}),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user