package document_test import ( "errors" "testing" "queryorchestration/internal/database/repository" "queryorchestration/internal/document" "queryorchestration/internal/fieldextraction" "queryorchestration/internal/harddelete" "queryorchestration/internal/test" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // createTestDocumentWithDeps creates a document with entries, cleans, and field extractions. // Returns the document ID for use in delete tests. func createTestDocumentWithDeps(t *testing.T, cfg *TestConfig, clientID string, folderID *uuid.UUID, hash string) uuid.UUID { t.Helper() ctx := t.Context() q := cfg.GetDBQueries() filename := hash + ".pdf" docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{ Clientid: clientID, Hash: hash, Filename: &filename, Folderid: folderID, }) require.NoError(t, err) // Add a document entry (S3 reference) err = q.AddDocumentEntry(ctx, &repository.AddDocumentEntryParams{ Documentid: docID, Bucket: "test-bucket", Key: "test-key/" + hash, }) require.NoError(t, err) // Add a document clean (must satisfy location_xor_fail constraint) cleanBucket := "clean-bucket" cleanKey := "clean-key/" + hash cleanHash := "cleanhash-" + hash cleanID, err := q.AddDocumentClean(ctx, &repository.AddDocumentCleanParams{ Documentid: docID, Bucket: &cleanBucket, Key: &cleanKey, Hash: &cleanHash, Mimetype: repository.NullCleanmimetype{ Cleanmimetype: repository.CleanmimetypeApplicationPdf, Valid: true, }, }) require.NoError(t, err) // Add a clean entry err = q.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Cleanid: cleanID, Version: 1, }) require.NoError(t, err) // Add field extraction fieldSvc := fieldextraction.New(cfg) contractTitle := "Test Contract " + hash input := &fieldextraction.CreateFieldExtractionInput{ DocumentID: docID, SingleFields: &repository.AddFieldExtractionParams{ Documentid: docID, Filename: &filename, Contracttitle: &contractTitle, Createdby: "testuser", }, ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, } _, err = fieldSvc.CreateFieldExtraction(ctx, input) require.NoError(t, err) return docID } func TestHardDeleteDocument(t *testing.T) { ctx := t.Context() cfg := &TestConfig{} test.CreateDB(t, cfg) clientID := "test-doc-delete" test.CreateTestClient(t, cfg, clientID, "Test Doc Delete") svc := document.New(cfg) t.Run("deletes document and all dependent rows", func(t *testing.T) { q := cfg.GetDBQueries() rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{ Clientid: clientID, Path: "/", }) require.NoError(t, err) docID := createTestDocumentWithDeps(t, cfg, clientID, &rootFolder.ID, "delete-cascade-test") // Verify document and deps exist before delete _, err = q.GetDocumentSummary(ctx, docID) require.NoError(t, err) s3Rows, err := q.CollectDocumentS3Paths(ctx, docID) require.NoError(t, err) assert.Len(t, s3Rows, 1) // Delete the document s3Paths, err := svc.HardDelete(ctx, docID) require.NoError(t, err) // Verify S3 paths returned assert.Len(t, s3Paths, 1) assert.Equal(t, "test-bucket", s3Paths[0].Bucket) assert.Equal(t, "test-key/delete-cascade-test", s3Paths[0].Key) assert.Equal(t, docID, s3Paths[0].DocumentID) // Verify document is gone _, err = q.GetDocumentSummary(ctx, docID) require.Error(t, err) // Verify document entries are gone s3Rows, err = q.CollectDocumentS3Paths(ctx, docID) require.NoError(t, err) assert.Empty(t, s3Rows) // Verify field extractions are gone extractions, err := q.GetFieldExtractionsByDocumentID(ctx, docID) require.NoError(t, err) assert.Empty(t, extractions) }) t.Run("returns NotFoundError for non-existent document", func(t *testing.T) { fakeID := uuid.New() _, err := svc.HardDelete(ctx, fakeID) require.Error(t, err) var notFoundErr *harddelete.NotFoundError assert.True(t, errors.As(err, ¬FoundErr)) }) t.Run("deletes document that has batch outcome rows", func(t *testing.T) { q := cfg.GetDBQueries() // Use a unique client to avoid collisions with other subtests batchClientID := "test-doc-batch-del" test.CreateTestClient(t, cfg, batchClientID, "Test Doc Batch Del") rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{ Clientid: batchClientID, Path: "/", }) require.NoError(t, err) // Create a batch upload for the client batchID, err := q.CreateBatchUpload(ctx, &repository.CreateBatchUploadParams{ ClientID: batchClientID, OriginalFilename: "test.zip", TotalDocuments: 1, }) require.NoError(t, err) // Create a document linked to the batch filename := "test.pdf" docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{ Clientid: batchClientID, Hash: "batch-doc-del-test", Filename: &filename, Folderid: &rootFolder.ID, BatchID: &batchID, }) require.NoError(t, err) // Insert a submitted outcome referencing this document err = q.InsertBatchDocumentOutcome(ctx, &repository.InsertBatchDocumentOutcomeParams{ BatchID: batchID, Filename: "test.pdf", Column3: repository.BatchOutcomeStatusSubmitted, DocumentID: &docID, }) require.NoError(t, err) // Hard delete the document -- should fail before FK fix s3Paths, err := svc.HardDelete(ctx, docID) require.NoError(t, err) // Verify document is gone _ = s3Paths _, err = q.GetDocumentSummary(ctx, docID) require.Error(t, err) // Verify the batch outcome row still exists but document_id is NULL // (history preserved via SET NULL on FK) rows, err := q.ListBatchDocumentOutcomes(ctx, batchID) require.NoError(t, err) require.Len(t, rows, 1) assert.Nil(t, rows[0].DocumentID) }) t.Run("returns empty S3 paths when document has no entries", func(t *testing.T) { q := cfg.GetDBQueries() // Create a document with no entries docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{ Clientid: clientID, Hash: "no-entries-test", }) require.NoError(t, err) s3Paths, err := svc.HardDelete(ctx, docID) require.NoError(t, err) assert.Empty(t, s3Paths) }) }