mod common; use anyhow::Result; use axum::http::StatusCode; use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras}; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Deserialize)] struct DocumentDetail { document: DocumentInfo, } #[derive(Deserialize)] struct DocumentInfo { id: Uuid, title: String, filename: String, original_name: String, deleted_at: Option, issued_at: Option, tags: Vec, #[serde(default)] correspondents: Vec, #[serde(default)] current_version: Option, } #[derive(Deserialize)] struct DocumentVersion { id: Uuid, s3_key: String, size_bytes: i64, version_number: i32, download_path: String, #[serde(default)] assets: Vec, } #[allow(dead_code)] #[derive(Deserialize)] struct DocumentAssetInfo { id: Uuid, asset_type: String, } #[derive(Deserialize)] struct DocumentListItem { id: Uuid, #[serde(default)] current_version: Option, } #[derive(Deserialize)] struct DocumentDownload { url: String, filename: String, } #[derive(Deserialize)] struct BulkReanalyze { queued: usize, } #[derive(Deserialize)] struct BulkMoveResult { updated: usize, } #[derive(Deserialize)] struct BulkTagResult { added: usize, removed: usize, } #[derive(Deserialize)] struct TagSummary { label: String, } #[derive(Deserialize)] struct DocumentCorrespondentInfo { name: String, role: String, } #[derive(Deserialize)] struct CorrespondentSummary { id: Uuid, } #[derive(Deserialize)] struct BulkCorrespondentResult { assigned: usize, removed: usize, } #[derive(Deserialize)] struct AnalyzeJobPayload { document_id: Uuid, document_version_id: Uuid, #[serde(default)] force: bool, } #[derive(Deserialize)] struct FolderResponse { folder: FolderInfo, } #[derive(Deserialize)] struct FolderInfo { id: Uuid, } #[derive(Deserialize)] struct FolderContents { documents: Vec, } #[derive(Deserialize)] struct TagResponse { id: Uuid, } #[derive(Serialize)] struct BulkMoveRequest<'a> { document_ids: &'a [Uuid], folder_id: Option, } #[derive(Serialize)] struct BulkTagRequest<'a> { document_ids: &'a [Uuid], tag_ids: &'a [Uuid], action: &'a str, } #[derive(Serialize)] struct CreateFolderRequest<'a> { name: &'a str, parent_id: Option, } #[derive(Serialize)] struct CreateTagPayload<'a> { label: &'a str, color: Option<&'a str>, } #[tokio::test] async fn upload_and_list_document() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "passw0rd"; app.insert_user("dana", password, "admin").await?; let token = app.login_token("dana", password).await?; let file_bytes = b"example document body".to_vec(); let upload = app .upload_document( "/api/documents", "doc.txt", "text/plain", &file_bytes, None, &token, ) .await?; { let status = upload.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let body = body_to_vec(upload.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&body)?; assert_eq!(detail.document.original_name, "doc.txt"); assert_eq!(detail.document.title, "doc"); assert_eq!(detail.document.deleted_at, None); assert!(detail.document.issued_at.is_none()); assert!(detail.document.tags.is_empty()); let current_version = detail .document .current_version .as_ref() .expect("current version detail"); assert!(current_version.download_path.starts_with("/download/")); assert_eq!(current_version.version_number, 1); assert_eq!(current_version.size_bytes, file_bytes.len() as i64); assert!(current_version.assets.is_empty()); let storage_key = app.storage_key_for(¤t_version.s3_key).await?; let stored = app .storage() .get(&storage_key) .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?; { let status = response.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let body = body_to_vec(response.into_body()).await?; let mut list: Vec = serde_json::from_slice(&body)?; assert_eq!(list.len(), 1); let item = list.pop().unwrap(); assert_eq!(item.id, detail.document.id); assert_eq!( item.current_version .as_ref() .map(|version| version.version_number), Some(1) ); assert!(item .current_version .as_ref() .expect("list current version") .download_path .starts_with("/download/")); let download = app .get( &format!("/api/documents/{}/download", detail.document.id), Some(&token), ) .await?; { let status = download.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let body = body_to_vec(download.into_body()).await?; let download_info: DocumentDownload = serde_json::from_slice(&body)?; assert!(download_info.url.contains(¤t_version.s3_key)); assert_eq!(download_info.filename, "doc.txt"); let redirect = app.get(¤t_version.download_path, None).await?; assert_eq!(redirect.status(), StatusCode::TEMPORARY_REDIRECT); let location = redirect .headers() .get("location") .expect("redirect location header"); let location = location.to_str().expect("location header utf8"); assert!(location.contains(¤t_version.s3_key)); app.cleanup().await?; Ok(()) } #[tokio::test] async fn upload_document_with_custom_title_sets_filename() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "passw0rd"; app.insert_user("nora", password, "admin").await?; let token = app.login_token("nora", password).await?; let file_bytes = b"example contract body".to_vec(); let title = "Vendor Contract"; let original_filename = "scan.pdf"; let upload = app .upload_document_with_options( "/api/documents", original_filename, "application/pdf", &file_bytes, None, Some(title), None, &token, ) .await?; { let status = upload.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let body = body_to_vec(upload.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&body)?; assert_eq!(detail.document.title, title); assert_eq!(detail.document.filename, format!("{title}.pdf")); assert_eq!(detail.document.original_name, original_filename); app.cleanup().await?; Ok(()) } #[tokio::test] async fn duplicate_and_restore_document() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "pass1234"; app.insert_user("sam", password, "admin").await?; let token = app.login_token("sam", password).await?; let payload = b"same bytes".to_vec(); let first = app .upload_document( "/api/documents", "dup.bin", "application/octet-stream", &payload, None, &token, ) .await?; { let status = first.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let first_body = body_to_vec(first.into_body()).await?; let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?; let second = app .upload_document( "/api/documents", "dup.bin", "application/octet-stream", &payload, None, &token, ) .await?; { let status = second.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } 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 .as_ref() .expect("second current version") .assets .is_empty()); assert_eq!(app.storage().object_count().await, 1); let delete = app .delete( &format!("/api/documents/{}", first_detail.document.id), Some(&token), ) .await?; assert_eq!(delete.status(), StatusCode::NO_CONTENT); let third = app .upload_document( "/api/documents", "dup.bin", "application/octet-stream", &payload, None, &token, ) .await?; { let status = third.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } 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 upload_skips_existing_when_requested() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "skip-doc"; app.insert_user("skip", password, "admin").await?; let token = app.login_token("skip", password).await?; let primary_tag_payload = CreateTagPayload { label: "primary", color: None, }; let primary_tag_resp = app .post_json("/api/tags", &primary_tag_payload, Some(&token)) .await?; { let status = primary_tag_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let primary_tag_body = body_to_vec(primary_tag_resp.into_body()).await?; let primary_tag: TagResponse = serde_json::from_slice(&primary_tag_body)?; let payload = b"identical document payload"; let primary_tag_ids = format!("[\"{}\"]", primary_tag.id); let extras = UploadExtras { title: Some("Original"), metadata_json: None, tag_ids_json: Some(primary_tag_ids.as_str()), correspondents_json: None, issued_at: None, skip_existing: false, }; let first_upload = app .upload_document_with_extras( "/api/documents", "original.pdf", "application/pdf", payload, None, extras, &token, ) .await?; { let status = first_upload.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let first_body = body_to_vec(first_upload.into_body()).await?; let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?; let alt_tag_payload = CreateTagPayload { label: "alternate", color: None, }; let alt_tag_resp = app .post_json("/api/tags", &alt_tag_payload, Some(&token)) .await?; { let status = alt_tag_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let alt_tag_body = body_to_vec(alt_tag_resp.into_body()).await?; let alt_tag: TagResponse = serde_json::from_slice(&alt_tag_body)?; let alt_tag_ids = format!("[\"{}\"]", alt_tag.id); let skip_extras = UploadExtras { title: Some("Updated"), metadata_json: None, tag_ids_json: Some(alt_tag_ids.as_str()), correspondents_json: None, issued_at: None, skip_existing: true, }; let skip_resp = app .upload_document_with_extras( "/api/documents", "ignored.pdf", "application/pdf", payload, None, skip_extras, &token, ) .await?; assert_eq!(skip_resp.status(), StatusCode::NO_CONTENT); let fetch = app .get( &format!("/api/documents/{}", first_detail.document.id), Some(&token), ) .await?; { let status = fetch.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let fetch_body = body_to_vec(fetch.into_body()).await?; let fetched: DocumentDetail = serde_json::from_slice(&fetch_body)?; assert_eq!(fetched.document.id, first_detail.document.id); assert_eq!(fetched.document.title, first_detail.document.title); assert_eq!(fetched.document.tags.len(), 1); assert_eq!(fetched.document.tags[0].label, "primary"); app.cleanup().await?; Ok(()) } #[tokio::test] async fn bulk_move_documents_to_folder() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "bulkmove"; app.insert_user("mover", password, "admin").await?; let token = app.login_token("mover", password).await?; let alpha = app .upload_document( "/api/documents", "alpha.txt", "text/plain", b"alpha", None, &token, ) .await?; { let status = alpha.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let alpha_body = body_to_vec(alpha.into_body()).await?; let alpha_detail: DocumentDetail = serde_json::from_slice(&alpha_body)?; let beta = app .upload_document( "/api/documents", "beta.txt", "text/plain", b"beta", None, &token, ) .await?; { let status = beta.status(); assert!(status.is_success(), "status was {}", status); } let beta_body = body_to_vec(beta.into_body()).await?; let beta_detail: DocumentDetail = serde_json::from_slice(&beta_body)?; let folder_resp = app .post_json( "/api/folders", &CreateFolderRequest { name: "Archives", parent_id: None, }, Some(&token), ) .await?; { let status = folder_resp.status(); assert!(status.is_success(), "status was {}", status); } let folder_body = body_to_vec(folder_resp.into_body()).await?; let folder: FolderResponse = serde_json::from_slice(&folder_body)?; let move_resp = app .post_json( "/api/documents/bulk/move", &BulkMoveRequest { document_ids: &[alpha_detail.document.id, beta_detail.document.id], folder_id: Some(folder.folder.id), }, Some(&token), ) .await?; let move_status = move_resp.status(); let move_body = body_to_vec(move_resp.into_body()).await?; assert!( move_status.is_success(), "status was {} body {}", move_status, String::from_utf8_lossy(&move_body) ); let result: BulkMoveResult = serde_json::from_slice(&move_body)?; assert_eq!(result.updated, 2); let folder_contents = app .get( &format!("/api/folders/{}/contents", folder.folder.id), Some(&token), ) .await?; { let status = folder_contents.status(); assert!(status.is_success(), "status was {}", status); } let folder_body = body_to_vec(folder_contents.into_body()).await?; let folder_docs: FolderContents = serde_json::from_slice(&folder_body)?; let moved_ids: Vec<_> = folder_docs.documents.iter().map(|doc| doc.id).collect(); assert!(moved_ids.contains(&alpha_detail.document.id)); assert!(moved_ids.contains(&beta_detail.document.id)); let root_contents = app.get("/api/folders/root/contents", Some(&token)).await?; let root_body = body_to_vec(root_contents.into_body()).await?; let root_docs: FolderContents = serde_json::from_slice(&root_body)?; assert!(root_docs .documents .iter() .all(|doc| doc.id != alpha_detail.document.id && doc.id != beta_detail.document.id)); app.cleanup().await?; Ok(()) } #[tokio::test] async fn bulk_update_tags_for_selection() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "bulktags"; app.insert_user("tagger", password, "admin").await?; let token = app.login_token("tagger", password).await?; let first = app .upload_document( "/api/documents", "notes.txt", "text/plain", b"notes", None, &token, ) .await?; { let status = first.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let first_body = body_to_vec(first.into_body()).await?; let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?; let second = app .upload_document( "/api/documents", "report.txt", "text/plain", b"report", None, &token, ) .await?; { let status = second.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let second_body = body_to_vec(second.into_body()).await?; let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?; let urgent_tag = app .post_json( "/api/tags", &CreateTagPayload { label: "Urgent", color: None, }, Some(&token), ) .await?; { let status = urgent_tag.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let urgent_body = body_to_vec(urgent_tag.into_body()).await?; let urgent: TagResponse = serde_json::from_slice(&urgent_body)?; let review_tag = app .post_json( "/api/tags", &CreateTagPayload { label: "Review", color: None, }, Some(&token), ) .await?; { let status = review_tag.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let review_body = body_to_vec(review_tag.into_body()).await?; let review: TagResponse = serde_json::from_slice(&review_body)?; let add_resp = app .post_json( "/api/documents/bulk/tags", &BulkTagRequest { document_ids: &[first_detail.document.id, second_detail.document.id], tag_ids: &[urgent.id, review.id], action: "add", }, Some(&token), ) .await?; { let status = add_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let add_body = body_to_vec(add_resp.into_body()).await?; let add_result: BulkTagResult = serde_json::from_slice(&add_body)?; assert_eq!(add_result.added, 4); for doc_id in [&first_detail.document.id, &second_detail.document.id] { let refreshed = app .get(&format!("/api/documents/{}", doc_id), Some(&token)) .await?; { let status = refreshed.status(); assert!(status == StatusCode::OK || status == StatusCode::CREATED); } let refreshed_body = body_to_vec(refreshed.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?; let labels: Vec<_> = detail .document .tags .iter() .map(|tag| tag.label.as_str()) .collect(); assert!(labels.contains(&"Urgent")); assert!(labels.contains(&"Review")); } let remove_resp = app .post_json( "/api/documents/bulk/tags", &BulkTagRequest { document_ids: &[first_detail.document.id, second_detail.document.id], tag_ids: &[urgent.id], action: "remove", }, Some(&token), ) .await?; { let status = remove_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let remove_body = body_to_vec(remove_resp.into_body()).await?; let remove_result: BulkTagResult = serde_json::from_slice(&remove_body)?; assert_eq!(remove_result.removed, 2); for doc_id in [&first_detail.document.id, &second_detail.document.id] { let refreshed = app .get(&format!("/api/documents/{}", doc_id), Some(&token)) .await?; let refreshed_body = body_to_vec(refreshed.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?; let labels: Vec<_> = detail .document .tags .iter() .map(|tag| tag.label.as_str()) .collect(); assert!(!labels.contains(&"Urgent")); assert!(labels.contains(&"Review")); } app.cleanup().await?; Ok(()) } #[tokio::test] async fn bulk_assign_correspondents_to_selection() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "bulkcorresp"; app.insert_user("corra", password, "admin").await?; let token = app.login_token("corra", password).await?; let first = app .upload_document( "/api/documents", "letter-one.txt", "text/plain", b"letter one", None, &token, ) .await?; { let status = first.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let first_body = body_to_vec(first.into_body()).await?; let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?; let second = app .upload_document( "/api/documents", "letter-two.txt", "text/plain", b"letter two", None, &token, ) .await?; { let status = second.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let second_body = body_to_vec(second.into_body()).await?; let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?; let sender = app .post_json( "/api/correspondents", &serde_json::json!({ "name": "Acme Corp" }), Some(&token), ) .await?; { let status = sender.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let sender_body = body_to_vec(sender.into_body()).await?; let sender_summary: CorrespondentSummary = serde_json::from_slice(&sender_body)?; let receiver = app .post_json( "/api/correspondents", &serde_json::json!({ "name": "Bank Ltd" }), Some(&token), ) .await?; { let status = receiver.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let receiver_body = body_to_vec(receiver.into_body()).await?; let receiver_summary: CorrespondentSummary = serde_json::from_slice(&receiver_body)?; let assign_payload = serde_json::json!({ "document_ids": [ first_detail.document.id, second_detail.document.id ], "assignments": [ { "correspondent_id": sender_summary.id, "role": "sender" }, { "correspondent_id": receiver_summary.id, "role": "receiver" } ] }); let assign_resp = app .post_json( "/api/documents/bulk/correspondents", &assign_payload, Some(&token), ) .await?; { let status = assign_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let assign_body = body_to_vec(assign_resp.into_body()).await?; let assign_result: BulkCorrespondentResult = serde_json::from_slice(&assign_body)?; assert_eq!(assign_result.assigned, 4); assert_eq!(assign_result.removed, 0); for doc_id in [first_detail.document.id, second_detail.document.id] { let refreshed = app .get(&format!("/api/documents/{doc_id}"), Some(&token)) .await?; { let status = refreshed.status(); assert!(status == StatusCode::OK || status == StatusCode::CREATED); } let refreshed_body = body_to_vec(refreshed.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?; assert_eq!(detail.document.correspondents.len(), 2); assert!(detail .document .correspondents .iter() .any(|entry| entry.role == "sender" && entry.name == "Acme Corp")); assert!(detail .document .correspondents .iter() .any(|entry| entry.role == "receiver" && entry.name == "Bank Ltd")); } let duplicate_resp = app .post_json( "/api/documents/bulk/correspondents", &assign_payload, Some(&token), ) .await?; { let status = duplicate_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let duplicate_body = body_to_vec(duplicate_resp.into_body()).await?; let duplicate_result: BulkCorrespondentResult = serde_json::from_slice(&duplicate_body)?; assert_eq!(duplicate_result.assigned, 0); assert_eq!(duplicate_result.removed, 0); let replacement = app .post_json( "/api/correspondents", &serde_json::json!({ "name": "Charlie" }), Some(&token), ) .await?; { let status = replacement.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let replacement_body = body_to_vec(replacement.into_body()).await?; let replacement_summary: CorrespondentSummary = serde_json::from_slice(&replacement_body)?; let replace_payload = serde_json::json!({ "document_ids": [ first_detail.document.id, second_detail.document.id ], "assignments": [ { "correspondent_id": replacement_summary.id, "role": "sender" } ] }); let replace_resp = app .post_json( "/api/documents/bulk/correspondents", &replace_payload, Some(&token), ) .await?; { let status = replace_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let replace_body = body_to_vec(replace_resp.into_body()).await?; let replace_result: BulkCorrespondentResult = serde_json::from_slice(&replace_body)?; assert_eq!(replace_result.assigned, 2); assert_eq!(replace_result.removed, 2); for doc_id in [first_detail.document.id, second_detail.document.id] { let refreshed = app .get(&format!("/api/documents/{doc_id}"), Some(&token)) .await?; let refreshed_body = body_to_vec(refreshed.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?; assert_eq!(detail.document.correspondents.len(), 2); assert!(detail .document .correspondents .iter() .any(|entry| entry.role == "sender" && entry.name == "Charlie")); assert!(detail .document .correspondents .iter() .any(|entry| entry.role == "receiver" && entry.name == "Bank Ltd")); } let remove_payload = serde_json::json!({ "document_ids": [ first_detail.document.id, second_detail.document.id ], "assignments": [ { "correspondent_id": receiver_summary.id, "role": "receiver" } ], "action": "remove" }); let remove_resp = app .post_json( "/api/documents/bulk/correspondents", &remove_payload, Some(&token), ) .await?; { let status = remove_resp.status(); assert!( status == StatusCode::OK || status == StatusCode::CREATED || status == StatusCode::NO_CONTENT ); } let remove_body = body_to_vec(remove_resp.into_body()).await?; let remove_result: BulkCorrespondentResult = serde_json::from_slice(&remove_body)?; assert_eq!(remove_result.assigned, 0); assert_eq!(remove_result.removed, 2); for doc_id in [first_detail.document.id, second_detail.document.id] { let refreshed = app .get(&format!("/api/documents/{doc_id}"), Some(&token)) .await?; let refreshed_body = body_to_vec(refreshed.into_body()).await?; let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?; assert_eq!(detail.document.correspondents.len(), 1); assert!(detail .document .correspondents .iter() .any(|entry| entry.role == "sender" && entry.name == "Charlie")); assert!(!detail .document .correspondents .iter() .any(|entry| entry.role == "receiver")); } app.cleanup().await?; Ok(()) } #[tokio::test] async fn bulk_reanalyze_selected_documents() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "subsetrean"; app.insert_user("subset", password, "admin").await?; let token = app.login_token("subset", password).await?; app.clear_jobs().await?; let first = app .upload_document( "/api/documents", "doc-one.txt", "text/plain", b"one", None, &token, ) .await?; let first_body = body_to_vec(first.into_body()).await?; let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?; let second = app .upload_document( "/api/documents", "doc-two.txt", "text/plain", b"two", None, &token, ) .await?; let second_body = body_to_vec(second.into_body()).await?; let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?; let third = app .upload_document( "/api/documents", "doc-three.txt", "text/plain", b"three", None, &token, ) .await?; let third_body = body_to_vec(third.into_body()).await?; let third_detail: DocumentDetail = serde_json::from_slice(&third_body)?; app.clear_jobs().await?; let response = app .post_json( "/api/documents/bulk/reanalyze", &serde_json::json!({ "document_ids": [ first_detail.document.id, third_detail.document.id ], "force": true }), 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)); } assert!(payload_docs .iter() .all(|(doc_id, _)| *doc_id != second_detail.document.id)); let mut expected = vec![ ( first_detail.document.id, first_detail .document .current_version .as_ref() .expect("first current version") .id, ), ( third_detail.document.id, third_detail .document .current_version .as_ref() .expect("third current version") .id, ), ]; payload_docs.sort(); expected.sort(); assert_eq!(payload_docs, expected); app.cleanup().await?; Ok(()) }