diff --git a/backend/tests/auth_flow.rs b/backend/tests/auth_flow.rs index bd11bbd..5f58c8b 100644 --- a/backend/tests/auth_flow.rs +++ b/backend/tests/auth_flow.rs @@ -40,7 +40,7 @@ struct TenantSelectionResponse { #[derive(Deserialize)] struct TenantSummary { - tenant_id: Uuid, + id: Uuid, slug: String, } @@ -225,7 +225,7 @@ async fn login_returns_tenant_selection_when_multiple_memberships() -> Result<() .tenants .iter() .find(|tenant| tenant.slug == secondary_slug) - .map(|t| t.tenant_id) + .map(|t| t.id) .context("secondary tenant missing from selection")?; let select_response = app diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 3653ff0..a14d373 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -322,7 +322,7 @@ impl TestApp { #[derive(Deserialize)] struct TenantSummary { - tenant_id: Uuid, + id: Uuid, _slug: String, } @@ -343,7 +343,7 @@ impl TestApp { tenant_id: Uuid, } - let target_tenant = selection.tenants[0].tenant_id; + let target_tenant = selection.tenants[0].id; let select_response = self .post_json( "/api/auth/select-tenant", diff --git a/backend/tests/documents_flow.rs b/backend/tests/documents_flow.rs index db051c1..7569a29 100644 --- a/backend/tests/documents_flow.rs +++ b/backend/tests/documents_flow.rs @@ -620,7 +620,7 @@ async fn bulk_move_documents_to_folder() -> Result<()> { } #[tokio::test] -async fn bulk_update_tags_for_selection() -> Result<()> { +async fn bulk_add_tags_for_selection() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; @@ -755,6 +755,84 @@ async fn bulk_update_tags_for_selection() -> Result<()> { assert!(labels.contains(&"Review")); } + app.cleanup().await?; + Ok(()) +} + +#[tokio::test] +async fn bulk_remove_tags_from_selection() -> Result<()> { + let _lock = acquire_db_lock().await; + let app = TestApp::new().await?; + + let password = "bulktagremove"; + app.insert_user("tagrem", password, "admin").await?; + let token = app.login_token("tagrem", password).await?; + + let first = app + .upload_document( + "/api/documents", + "meeting-notes.txt", + "text/plain", + b"notes", + 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", + "draft.txt", + "text/plain", + b"draft", + None, + &token, + ) + .await?; + 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 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 review_body = body_to_vec(review_tag.into_body()).await?; + let review: TagResponse = serde_json::from_slice(&review_body)?; + + let seed_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?; + assert!(seed_resp.status().is_success()); + let remove_resp = app .post_json( "/api/documents/bulk/tags", @@ -766,17 +844,11 @@ async fn bulk_update_tags_for_selection() -> Result<()> { Some(&token), ) .await?; - { - let status = remove_resp.status(); - assert!( - status == StatusCode::OK - || status == StatusCode::CREATED - || status == StatusCode::NO_CONTENT - ); - } + assert!(remove_resp.status().is_success()); 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); + assert_eq!(remove_result.added, 0); for doc_id in [&first_detail.document.id, &second_detail.document.id] { let refreshed = app @@ -794,6 +866,23 @@ async fn bulk_update_tags_for_selection() -> Result<()> { assert!(labels.contains(&"Review")); } + let idempotent_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?; + assert!(idempotent_resp.status().is_success()); + let idempotent_body = body_to_vec(idempotent_resp.into_body()).await?; + let idempotent_result: BulkTagResult = serde_json::from_slice(&idempotent_body)?; + assert_eq!(idempotent_result.removed, 0); + assert_eq!(idempotent_result.added, 0); + app.cleanup().await?; Ok(()) } diff --git a/frontend/docker-entrypoint.sh b/frontend/docker-entrypoint.sh index 5209f5d..1709544 100644 --- a/frontend/docker-entrypoint.sh +++ b/frontend/docker-entrypoint.sh @@ -27,6 +27,14 @@ cat <> /etc/nginx/conf.d/default.conf proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } + + location /download/ { + proxy_pass ${API_PROXY_PASS_TRIMMED}; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } PROXY fi diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 9a2feb8..993118a 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -48,7 +48,7 @@ module.exports = { open: true, proxy: [ { - context: ['/api'], + context: ['/api', '/download'], target: 'http://127.0.0.1:3000', changeOrigin: true, secure: false,