use anyhow::{anyhow, Result}; use axum::http::StatusCode; use diesel::prelude::*; use papercrate::jobs::{enqueue_job, JOB_DELETE_TENANT}; use papercrate::models::TenantStatus; use papercrate::schema::{documents, tenants, user_memberships}; use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use papercrate::workers::tenants::DeleteTenantJob; use papercrate::workers::JobHandler; use serde_json::json; use std::sync::Arc; use uuid::Uuid; #[tokio::test] async fn delete_tenant_job_keeps_tenant_when_requested() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "delete-keep"; app.insert_user("tenant-keep", TestUserRole::Owner).await?; let token = app.login_token("tenant-keep", password).await?; let upload = app .upload_document( "/api/documents", "keep.pdf", "application/pdf", b"keep", None, &token, ) .await?; assert_eq!(upload.status(), StatusCode::CREATED); body_to_vec(upload.into_body()).await?; let tenant_id = app .state .tenants .get_by_name("test_tenant") .map_err(|err| anyhow!("tenant lookup failed: {err:?}"))? .id; set_tenant_status(&app, tenant_id, TenantStatus::Deleting).await?; let job = enqueue_delete_job(&app, tenant_id, false).await?; let handler = DeleteTenantJob::new(); let state = Arc::new(app.state.clone()); let storage = app .state .storage_for_tenant(tenant_id) .map_err(|err| anyhow!("storage unavailable: {err:?}"))?; let execution = handler.handle(state, job, storage).await; assert!(matches!( execution, papercrate::workers::JobExecution::Success )); assert_eq!(app.storage().object_count().await, 0); let tenant_id_for_check = tenant_id; app.with_conn(move |conn| { use diesel::dsl::count_star; let doc_count: i64 = documents::table.select(count_star()).get_result(conn)?; assert_eq!(doc_count, 0); let membership_count: i64 = user_memberships::table .select(count_star()) .get_result(conn)?; assert!(membership_count > 0, "memberships should remain"); let status: TenantStatus = tenants::table .find(tenant_id_for_check) .select(tenants::status) .first(conn)?; assert_eq!(status, TenantStatus::Suspended); Ok(()) }) .await?; app.cleanup().await?; Ok(()) } #[tokio::test] async fn delete_tenant_job_can_reset_tenant_to_active() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "delete-reset"; app.insert_user("tenant-reset", TestUserRole::Owner).await?; let token = app.login_token("tenant-reset", password).await?; let upload = app .upload_document( "/api/documents", "reset.pdf", "application/pdf", b"reset", None, &token, ) .await?; assert_eq!(upload.status(), StatusCode::CREATED); body_to_vec(upload.into_body()).await?; let tenant_id = app .state .tenants .get_by_name("test_tenant") .map_err(|err| anyhow!("tenant lookup failed: {err:?}"))? .id; set_tenant_status(&app, tenant_id, TenantStatus::Deleting).await?; let job = enqueue_delete_job_with_status(&app, tenant_id, false, Some("active")).await?; let handler = DeleteTenantJob::new(); let state = Arc::new(app.state.clone()); let storage = app .state .storage_for_tenant(tenant_id) .map_err(|err| anyhow!("storage unavailable: {err:?}"))?; let execution = handler.handle(state, job, storage).await; assert!(matches!( execution, papercrate::workers::JobExecution::Success )); assert_eq!(app.storage().object_count().await, 0); let tenant_id_for_check = tenant_id; app.with_conn(move |conn| { use diesel::dsl::count_star; let doc_count: i64 = documents::table.select(count_star()).get_result(conn)?; assert_eq!(doc_count, 0); let membership_count: i64 = user_memberships::table .select(count_star()) .get_result(conn)?; assert!(membership_count > 0, "memberships should remain"); let status: TenantStatus = tenants::table .find(tenant_id_for_check) .select(tenants::status) .first(conn)?; assert_eq!(status, TenantStatus::Active); Ok(()) }) .await?; app.cleanup().await?; Ok(()) } #[tokio::test] async fn delete_tenant_job_removes_tenant_entirely() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "delete-remove"; app.insert_user("tenant-remove", TestUserRole::Owner) .await?; let token = app.login_token("tenant-remove", password).await?; let upload = app .upload_document( "/api/documents", "remove.pdf", "application/pdf", b"remove", None, &token, ) .await?; assert_eq!(upload.status(), StatusCode::CREATED); body_to_vec(upload.into_body()).await?; let tenant_id = app .state .tenants .get_by_name("test_tenant") .map_err(|err| anyhow!("tenant lookup failed: {err:?}"))? .id; set_tenant_status(&app, tenant_id, TenantStatus::Deleting).await?; let job = enqueue_delete_job(&app, tenant_id, true).await?; let handler = DeleteTenantJob::new(); let state = Arc::new(app.state.clone()); let storage = app .state .storage_for_tenant(tenant_id) .map_err(|err| anyhow!("storage unavailable: {err:?}"))?; let execution = handler.handle(state, job, storage).await; assert!(matches!( execution, papercrate::workers::JobExecution::Success )); assert_eq!(app.storage().object_count().await, 0); let tenant_id_for_check = tenant_id; app.with_conn(move |conn| { use diesel::dsl::count_star; let doc_count: i64 = documents::table.select(count_star()).get_result(conn)?; assert_eq!(doc_count, 0); let membership_count: i64 = user_memberships::table .select(count_star()) .get_result(conn)?; assert_eq!(membership_count, 0, "memberships should be removed"); let exists: Option = tenants::table .find(tenant_id_for_check) .select(tenants::status) .first(conn) .optional()?; assert!(exists.is_none(), "tenant row should be deleted"); Ok(()) }) .await?; app.cleanup().await?; Ok(()) } async fn set_tenant_status(app: &TestApp, tenant_id: Uuid, status: TenantStatus) -> Result<()> { app.with_conn(move |conn| { diesel::update(tenants::table.find(tenant_id)) .set(tenants::status.eq(status)) .execute(conn)?; Ok(()) }) .await } async fn enqueue_delete_job( app: &TestApp, tenant_id: Uuid, remove_tenant: bool, ) -> Result { enqueue_delete_job_with_status(app, tenant_id, remove_tenant, None).await } async fn enqueue_delete_job_with_status( app: &TestApp, tenant_id: Uuid, remove_tenant: bool, final_status: Option<&'static str>, ) -> Result { app.with_conn(move |conn| { let tenant_name: String = tenants::table .find(tenant_id) .select(tenants::name) .first(conn) .map_err(|err| anyhow!("tenant lookup failed: {err}"))?; let mut payload = json!({ "remove_tenant": remove_tenant, "tenant_name": tenant_name, }); if let Some(status) = final_status { payload["final_status"] = json!(status); } enqueue_job(conn, tenant_id, JOB_DELETE_TENANT, payload, None) .map_err(|err| anyhow!("enqueue failed: {err}")) }) .await }