This commit is contained in:
2025-10-31 02:19:38 +01:00
parent f796932cd8
commit eacab2ced6
7 changed files with 462 additions and 17 deletions
+8 -1
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, ApiErrorResponse, TestApp};
use common::{acquire_db_lock, body_to_vec, TestApp};
use diesel::prelude::*;
use papercrate::auth::passkeys::{
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
@@ -28,6 +28,13 @@ struct AuthenticatedUser {
username: String,
}
#[derive(Deserialize)]
struct ApiErrorResponse {
error: String,
#[serde(default)]
_code: Option<String>,
}
#[derive(Deserialize)]
struct LoginTenant {
id: Uuid,
+8 -8
View File
@@ -29,7 +29,7 @@ use papercrate::state::AppState;
use papercrate::storage::ObjectStorage;
use rand::rngs::OsRng;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use serde_json::{self, json};
use sha2::{Digest, Sha256};
use tokio::sync::Mutex;
@@ -122,13 +122,6 @@ pub struct TestApp {
storage: Arc<FakeStorage>,
}
#[derive(Debug, Deserialize)]
pub struct ApiErrorResponse {
pub error: String,
#[serde(default)]
pub code: Option<String>,
}
impl TestApp {
pub async fn new() -> Result<Self> {
let database_url = env::var("TEST_DATABASE_URL")
@@ -625,6 +618,13 @@ impl TestApp {
body.extend(b"\r\n");
}
if let Some(document_type_id) = extras.document_type_id {
body.extend(format!("--{boundary}\r\n").as_bytes());
body.extend(b"Content-Disposition: form-data; name=\"document_type_id\"\r\n\r\n");
body.extend(document_type_id.to_string().as_bytes());
body.extend(b"\r\n");
}
if extras.skip_existing {
body.extend(format!("--{boundary}\r\n").as_bytes());
body.extend(b"Content-Disposition: form-data; name=\"skip_existing\"\r\n\r\ntrue\r\n");
+127
View File
@@ -0,0 +1,127 @@
mod common;
use anyhow::Result;
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, TestApp};
use serde_json::json;
use uuid::Uuid;
#[derive(serde::Deserialize)]
struct DocumentTypeInfo {
id: Uuid,
name: String,
}
#[derive(serde::Deserialize)]
struct ApiErrorResponse {
error: String,
#[serde(default)]
code: Option<String>,
}
#[tokio::test]
async fn document_type_crud_flow() -> Result<()> {
let _lock = acquire_db_lock().await;
let app = TestApp::new().await?;
let password = "doctypecrud";
let username = "doctype_admin";
app.insert_user(username, password, "admin").await?;
let token = app.login_token(username, password).await?;
let create_invoices = app
.post_json(
"/api/document-types",
&json!({"name": "Invoices"}),
Some(&token),
)
.await?;
assert_eq!(create_invoices.status(), StatusCode::CREATED);
let invoices_body = body_to_vec(create_invoices.into_body()).await?;
let invoices: DocumentTypeInfo = serde_json::from_slice(&invoices_body)?;
let invoices_id = invoices.id;
let create_receipts = app
.post_json(
"/api/document-types",
&json!({"name": "Receipts"}),
Some(&token),
)
.await?;
assert_eq!(create_receipts.status(), StatusCode::CREATED);
let receipts_body = body_to_vec(create_receipts.into_body()).await?;
let receipts: DocumentTypeInfo = serde_json::from_slice(&receipts_body)?;
let list_resp = app.get("/api/document-types", Some(&token)).await?;
assert!(list_resp.status().is_success());
let list_body = body_to_vec(list_resp.into_body()).await?;
let mut all_types: Vec<DocumentTypeInfo> = serde_json::from_slice(&list_body)?;
all_types.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(all_types.len(), 2);
assert_eq!(all_types[0].name, "Invoices");
assert_eq!(all_types[1].name, "Receipts");
let update_resp = app
.patch_json(
&format!("/api/document-types/{}", invoices_id),
&json!({"name": "Bills"}),
Some(&token),
)
.await?;
assert_eq!(update_resp.status(), StatusCode::OK);
let update_body = body_to_vec(update_resp.into_body()).await?;
let updated: DocumentTypeInfo = serde_json::from_slice(&update_body)?;
assert_eq!(updated.id, invoices_id);
assert_eq!(updated.name, "Bills");
let conflict_resp = app
.patch_json(
&format!("/api/document-types/{}", receipts.id),
&json!({"name": "Bills"}),
Some(&token),
)
.await?;
assert_eq!(conflict_resp.status(), StatusCode::CONFLICT);
let conflict_body = body_to_vec(conflict_resp.into_body()).await?;
let conflict_json: ApiErrorResponse = serde_json::from_slice(&conflict_body)?;
assert_eq!(
conflict_json.error,
"a document type with that name already exists"
);
assert_eq!(
conflict_json.code.as_deref(),
Some("duplicate_document_type")
);
let delete_receipts = app
.delete(
&format!("/api/document-types/{}", receipts.id),
Some(&token),
)
.await?;
assert_eq!(delete_receipts.status(), StatusCode::NO_CONTENT);
let list_after_resp = app.get("/api/document-types", Some(&token)).await?;
assert!(list_after_resp.status().is_success());
let list_after_body = body_to_vec(list_after_resp.into_body()).await?;
let list_after: Vec<DocumentTypeInfo> = serde_json::from_slice(&list_after_body)?;
assert_eq!(list_after.len(), 1);
assert_eq!(list_after[0].id, invoices_id);
assert_eq!(list_after[0].name, "Bills");
let delete_bills = app
.delete(
&format!("/api/document-types/{}", invoices_id),
Some(&token),
)
.await?;
assert_eq!(delete_bills.status(), StatusCode::NO_CONTENT);
let final_list_resp = app.get("/api/document-types", Some(&token)).await?;
let final_list_body = body_to_vec(final_list_resp.into_body()).await?;
let final_list: Vec<DocumentTypeInfo> = serde_json::from_slice(&final_list_body)?;
assert!(final_list.is_empty());
app.cleanup().await?;
Ok(())
}
+145 -7
View File
@@ -2,7 +2,7 @@ mod common;
use anyhow::Result;
use axum::http::StatusCode;
use common::{acquire_db_lock, body_to_vec, ApiErrorResponse, TestApp, UploadExtras};
use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use uuid::Uuid;
@@ -12,6 +12,13 @@ struct DocumentDetail {
document: DocumentInfo,
}
#[derive(Deserialize)]
struct ApiErrorResponse {
error: String,
#[serde(default)]
code: Option<String>,
}
#[derive(Deserialize)]
struct DocumentInfo {
id: Uuid,
@@ -25,11 +32,19 @@ struct DocumentInfo {
metadata: Value,
#[serde(default)]
document_type_id: Option<Uuid>,
#[serde(default)]
document_type: Option<DocumentTypeInfo>,
tags: Vec<TagSummary>,
#[serde(default)]
current_version: Option<DocumentVersionPayload>,
}
#[derive(Deserialize)]
struct DocumentTypeInfo {
id: Uuid,
name: String,
}
#[derive(Deserialize)]
struct DocumentVersionPayload {
id: Uuid,
@@ -83,12 +98,6 @@ struct TagSummary {
label: String,
}
#[derive(Deserialize)]
struct DocumentTypeInfo {
id: Uuid,
name: String,
}
#[derive(Deserialize)]
struct AnalyzeJobPayload {
document_id: Uuid,
@@ -411,6 +420,7 @@ async fn upload_skips_existing_when_requested() -> Result<()> {
correspondents_json: None,
issued_at: None,
skip_existing: false,
document_type_id: None,
};
let first_upload = app
@@ -461,6 +471,7 @@ async fn upload_skips_existing_when_requested() -> Result<()> {
correspondents_json: None,
issued_at: None,
skip_existing: true,
document_type_id: None,
};
let skip_resp = app
@@ -1442,6 +1453,133 @@ async fn list_documents_by_status_filter() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn document_upload_and_patch_document_type() -> Result<()> {
let _lock = acquire_db_lock().await;
let app = TestApp::new().await?;
let password = "doctypeupload";
let username = "doctype_user";
app.insert_user(username, password, "admin").await?;
let token = app.login_token(username, password).await?;
let invoices_resp = app
.post_json(
"/api/document-types",
&json!({"name": "Invoices"}),
Some(&token),
)
.await?;
assert_eq!(invoices_resp.status(), StatusCode::CREATED);
let invoices_body = body_to_vec(invoices_resp.into_body()).await?;
let invoices: DocumentTypeInfo = serde_json::from_slice(&invoices_body)?;
let receipts_resp = app
.post_json(
"/api/document-types",
&json!({"name": "Receipts"}),
Some(&token),
)
.await?;
assert_eq!(receipts_resp.status(), StatusCode::CREATED);
let receipts_body = body_to_vec(receipts_resp.into_body()).await?;
let receipts: DocumentTypeInfo = serde_json::from_slice(&receipts_body)?;
let upload_extras = UploadExtras {
title: Some("Invoice #1"),
metadata_json: None,
tag_ids_json: None,
correspondents_json: None,
issued_at: None,
skip_existing: false,
document_type_id: Some(invoices.id),
};
let upload_resp = app
.upload_document_with_extras(
"/api/documents",
"invoice.pdf",
"application/pdf",
b"invoice-bytes",
None,
upload_extras,
&token,
)
.await?;
assert!(upload_resp.status().is_success());
let upload_body = body_to_vec(upload_resp.into_body()).await?;
let mut detail: DocumentDetail = serde_json::from_slice(&upload_body)?;
assert_eq!(detail.document.document_type_id, Some(invoices.id));
let doc_type_info = detail
.document
.document_type
.as_ref()
.expect("document type present");
assert_eq!(doc_type_info.name, "Invoices");
let document_id = detail.document.id;
let update_resp = app
.patch_json(
&format!("/api/documents/{}", document_id),
&json!({"document_type_id": receipts.id}),
Some(&token),
)
.await?;
assert!(update_resp.status().is_success());
let update_body = body_to_vec(update_resp.into_body()).await?;
detail = serde_json::from_slice(&update_body)?;
assert_eq!(detail.document.document_type_id, Some(receipts.id));
assert_eq!(
detail
.document
.document_type
.as_ref()
.map(|info| info.name.as_str()),
Some("Receipts")
);
let clear_resp = app
.patch_json(
&format!("/api/documents/{}", document_id),
&json!({"document_type_id": null}),
Some(&token),
)
.await?;
assert!(clear_resp.status().is_success());
let clear_body = body_to_vec(clear_resp.into_body()).await?;
detail = serde_json::from_slice(&clear_body)?;
assert!(detail.document.document_type_id.is_none());
assert!(detail.document.document_type.is_none());
let invalid_id = Uuid::new_v4();
let invalid_resp = app
.patch_json(
&format!("/api/documents/{}", document_id),
&json!({"document_type_id": invalid_id}),
Some(&token),
)
.await?;
assert_eq!(invalid_resp.status(), StatusCode::BAD_REQUEST);
let invalid_body = body_to_vec(invalid_resp.into_body()).await?;
let invalid_error: ApiErrorResponse = serde_json::from_slice(&invalid_body)?;
assert_eq!(
invalid_error.error,
"document_type_id does not exist for this tenant"
);
let final_detail_resp = app
.get(&format!("/api/documents/{}", document_id), Some(&token))
.await?;
let final_detail_body = body_to_vec(final_detail_resp.into_body()).await?;
let final_detail: DocumentDetail = serde_json::from_slice(&final_detail_body)?;
assert!(final_detail.document.document_type_id.is_none());
assert!(final_detail.document.document_type.is_none());
app.cleanup().await?;
Ok(())
}
#[tokio::test]
async fn list_documents_filtered_by_document_type() -> Result<()> {
let _lock = acquire_db_lock().await;