Merged in feature/batch-status (pull request #215)
Implement and test the batch status feature * working
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// Package outcome provides a shared reporter that runners use to write
|
||||
// pipeline stage outcomes to the batch_document_outcomes table.
|
||||
// For non-batch documents (no outcome row exists), updates are a natural
|
||||
// no-op (0 rows affected).
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Reporter writes pipeline stage outcomes to batch_document_outcomes.
|
||||
// Used by runners to report their processing results for batch-uploaded documents.
|
||||
// For non-batch documents (no outcome row exists), updates are a no-op (0 rows affected).
|
||||
//
|
||||
// Fields:
|
||||
// - queries: SQLC-generated database queries
|
||||
type Reporter struct {
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
// New creates an outcome Reporter.
|
||||
//
|
||||
// Parameters:
|
||||
// - q: SQLC-generated queries instance (from cfg.GetDBQueries())
|
||||
//
|
||||
// Returns:
|
||||
// - *Reporter: the reporter instance
|
||||
func New(q *repository.Queries) *Reporter {
|
||||
return &Reporter{queries: q}
|
||||
}
|
||||
|
||||
// Resolve sets the document_id on a batch outcome row and updates the outcome.
|
||||
// Called by docInitRunner after creating or finding the document.
|
||||
// Matches on batch_id + filename since document_id is NULL before init.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - batchID: the batch this file belongs to (from the document's batch_id)
|
||||
// - filename: original filename from the ZIP (from the document record)
|
||||
// - documentID: the created or found document ID
|
||||
// - outcomeStatus: the init-stage outcome ("init_complete" or "init_duplicate")
|
||||
// - errorDetail: optional error message (nil for success)
|
||||
//
|
||||
// Returns:
|
||||
// - error: database error, or nil
|
||||
//
|
||||
// If batchID is nil, this is a no-op (document was not from a batch upload).
|
||||
func (r *Reporter) Resolve(ctx context.Context, batchID *uuid.UUID, filename string,
|
||||
documentID uuid.UUID, outcomeStatus repository.BatchOutcomeStatus, errorDetail *string) error {
|
||||
if batchID == nil {
|
||||
return nil
|
||||
}
|
||||
return r.queries.ResolveBatchDocumentOutcome(ctx, &repository.ResolveBatchDocumentOutcomeParams{
|
||||
BatchID: *batchID,
|
||||
Filename: filename,
|
||||
DocumentID: &documentID,
|
||||
Column4: outcomeStatus,
|
||||
ErrorDetail: errorDetail,
|
||||
})
|
||||
}
|
||||
|
||||
// Update updates the outcome for a document that already has document_id set.
|
||||
// Called by docSyncRunner and docCleanRunner after processing.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - documentID: the document being processed
|
||||
// - outcomeStatus: the pipeline outcome (e.g. "sync_complete", "clean_passed")
|
||||
// - errorDetail: optional error message (nil for success)
|
||||
//
|
||||
// Returns:
|
||||
// - error: database error, or nil
|
||||
//
|
||||
// If no outcome row exists for this document_id (non-batch document), this
|
||||
// affects 0 rows and is a no-op.
|
||||
func (r *Reporter) Update(ctx context.Context, documentID uuid.UUID,
|
||||
outcomeStatus repository.BatchOutcomeStatus, errorDetail *string) error {
|
||||
return r.queries.UpdateBatchDocumentOutcomeByDocumentID(ctx, &repository.UpdateBatchDocumentOutcomeByDocumentIDParams{
|
||||
DocumentID: &documentID,
|
||||
Column2: outcomeStatus,
|
||||
ErrorDetail: errorDetail,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Package outcome_test contains integration tests for the batch outcome reporter.
|
||||
// Tests use real database via testcontainers (no mocks).
|
||||
package outcome_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/document/batch/outcome"
|
||||
"queryorchestration/internal/serviceconfig"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestConfig struct {
|
||||
serviceconfig.BaseConfig
|
||||
}
|
||||
|
||||
// createBatchWithOutcome is a helper that creates a client, batch, and inserts
|
||||
// a submitted outcome row. Returns the batchID and the unique clientID used.
|
||||
func createBatchWithOutcome(t *testing.T, cfg *TestConfig, clientPrefix, filename string) (uuid.UUID, string) {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
|
||||
// Use a unique clientID to avoid collisions when tests share the DB
|
||||
clientID := clientPrefix + "_" + uuid.New().String()[:8]
|
||||
err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{
|
||||
Clientid: clientID,
|
||||
Name: "Test Client " + clientID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
batchID, err := cfg.GetDBQueries().CreateBatchUpload(ctx, &repository.CreateBatchUploadParams{
|
||||
ClientID: clientID,
|
||||
OriginalFilename: "test.zip",
|
||||
TotalDocuments: 5,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.GetDBQueries().InsertBatchDocumentOutcome(ctx, &repository.InsertBatchDocumentOutcomeParams{
|
||||
BatchID: batchID,
|
||||
Filename: filename,
|
||||
Column3: repository.BatchOutcomeStatusSubmitted,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return batchID, clientID
|
||||
}
|
||||
|
||||
// TestResolve_SetsDocumentIDAndOutcome inserts a submitted row, resolves it
|
||||
// with a document_id, and verifies the update took effect.
|
||||
func TestResolve_SetsDocumentIDAndOutcome(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
filename := "invoice.pdf"
|
||||
batchID, clientID := createBatchWithOutcome(t, cfg, "test_resolve_sets", filename)
|
||||
|
||||
// Create a real document so FK constraint is satisfied
|
||||
docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: "hash_resolve_test",
|
||||
BatchID: &batchID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
reporter := outcome.New(cfg.GetDBQueries())
|
||||
|
||||
err = reporter.Resolve(ctx, &batchID, filename, docID,
|
||||
repository.BatchOutcomeStatusInitComplete, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the row was updated
|
||||
rows, err := cfg.GetDBQueries().ListBatchDocumentOutcomes(ctx, batchID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 1)
|
||||
|
||||
assert.Equal(t, repository.BatchOutcomeStatusInitComplete, rows[0].Outcome)
|
||||
require.NotNil(t, rows[0].DocumentID)
|
||||
assert.Equal(t, docID, *rows[0].DocumentID)
|
||||
}
|
||||
|
||||
// TestResolve_NoOpForNilBatchID verifies that calling Resolve with a nil batchID
|
||||
// returns no error and makes no DB changes.
|
||||
func TestResolve_NoOpForNilBatchID(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
reporter := outcome.New(cfg.GetDBQueries())
|
||||
|
||||
// Call with nil batchID -- should be a no-op
|
||||
err := reporter.Resolve(ctx, nil, "anything.pdf", uuid.New(),
|
||||
repository.BatchOutcomeStatusInitComplete, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestUpdate_UpdatesOutcomeByDocumentID inserts a resolved row (with document_id),
|
||||
// then updates it to sync_complete and verifies.
|
||||
func TestUpdate_UpdatesOutcomeByDocumentID(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
filename := "receipt.pdf"
|
||||
batchID, clientID := createBatchWithOutcome(t, cfg, "test_update_docid", filename)
|
||||
|
||||
// Create a real document so FK constraint is satisfied
|
||||
docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: "hash_update_test",
|
||||
BatchID: &batchID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
reporter := outcome.New(cfg.GetDBQueries())
|
||||
|
||||
// Resolve first (set document_id and advance to init_complete)
|
||||
err = reporter.Resolve(ctx, &batchID, filename, docID,
|
||||
repository.BatchOutcomeStatusInitComplete, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now update to sync_complete
|
||||
err = reporter.Update(ctx, docID, repository.BatchOutcomeStatusSyncComplete, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the outcome was advanced
|
||||
rows, err := cfg.GetDBQueries().ListBatchDocumentOutcomes(ctx, batchID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, repository.BatchOutcomeStatusSyncComplete, rows[0].Outcome)
|
||||
}
|
||||
|
||||
// TestUpdate_NoOpForNonBatchDocument verifies that calling Update for a
|
||||
// document_id that has no outcome row produces no error (0 rows affected).
|
||||
func TestUpdate_NoOpForNonBatchDocument(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
reporter := outcome.New(cfg.GetDBQueries())
|
||||
|
||||
// Call Update with a random document_id that has no outcome row
|
||||
err := reporter.Update(ctx, uuid.New(), repository.BatchOutcomeStatusSyncComplete, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package batch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -36,13 +38,40 @@ type BatchUploadSummary struct {
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
// BatchUploadDetails includes full details with failed filenames
|
||||
// BatchDocumentOutcome represents the per-file result from batch extraction
|
||||
// and pipeline processing. This is the single source of truth for a file's
|
||||
// status within a batch.
|
||||
//
|
||||
// Fields:
|
||||
// - ID: unique outcome record ID
|
||||
// - BatchID: the batch this file belongs to
|
||||
// - Filename: original filename from the ZIP
|
||||
// - Outcome: current status (extraction outcome or pipeline stage)
|
||||
// - ErrorDetail: human-readable error/failure message, nil for success states
|
||||
// - DocumentID: resolved document ID (nil if document not yet created)
|
||||
// - CleanFail: specific clean failure reason from documentCleans table (nil if N/A)
|
||||
// - CreatedAt: when the outcome row was created (extraction time)
|
||||
// - UpdatedAt: when the outcome was last changed (pipeline progression)
|
||||
type BatchDocumentOutcome struct {
|
||||
ID uuid.UUID
|
||||
BatchID uuid.UUID
|
||||
Filename string
|
||||
Outcome string
|
||||
ErrorDetail *string
|
||||
DocumentID *uuid.UUID
|
||||
CleanFail *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// BatchUploadDetails includes full details with failed filenames and document outcomes
|
||||
type BatchUploadDetails struct {
|
||||
BatchUploadSummary
|
||||
FailedFilenames []string `json:"failed_filenames"`
|
||||
ArchiveBucket string `json:"archive_bucket,omitempty"`
|
||||
ArchiveKey string `json:"archive_key,omitempty"`
|
||||
FileSizeBytes int64 `json:"file_size_bytes,omitempty"`
|
||||
FailedFilenames []string `json:"failed_filenames"`
|
||||
DocumentOutcomes []*BatchDocumentOutcome `json:"document_outcomes,omitempty"`
|
||||
ArchiveBucket string `json:"archive_bucket,omitempty"`
|
||||
ArchiveKey string `json:"archive_key,omitempty"`
|
||||
FileSizeBytes int64 `json:"file_size_bytes,omitempty"`
|
||||
}
|
||||
|
||||
// Service handles batch upload operations
|
||||
@@ -84,9 +113,18 @@ func (s *Service) CreateWithStorage(ctx context.Context, clientID string, filena
|
||||
return result.ID, nil
|
||||
}
|
||||
|
||||
// Get retrieves batch upload details
|
||||
// Get retrieves batch upload details including per-file document outcomes.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - clientID: the client identifier
|
||||
// - batchID: the batch UUID
|
||||
//
|
||||
// Returns:
|
||||
// - *BatchUploadDetails: full batch details with document outcomes
|
||||
// - error: database error, or nil
|
||||
func (s *Service) Get(ctx context.Context, clientID string, batchID uuid.UUID) (*BatchUploadDetails, error) {
|
||||
batch, err := s.cfg.GetDBQueries().GetBatchUploadWithStorage(ctx, &repository.GetBatchUploadWithStorageParams{
|
||||
batchRow, err := s.cfg.GetDBQueries().GetBatchUploadWithStorage(ctx, &repository.GetBatchUploadWithStorageParams{
|
||||
ID: batchID,
|
||||
ClientID: clientID,
|
||||
})
|
||||
@@ -94,7 +132,16 @@ func (s *Service) Get(ctx context.Context, clientID string, batchID uuid.UUID) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToDetailsWithStorage(batch), nil
|
||||
details := s.convertToDetailsWithStorage(batchRow)
|
||||
|
||||
// Populate document outcomes
|
||||
outcomes, err := s.ListOutcomes(ctx, batchID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list batch outcomes: %w", err)
|
||||
}
|
||||
details.DocumentOutcomes = outcomes
|
||||
|
||||
return details, nil
|
||||
}
|
||||
|
||||
// List retrieves all batch uploads for a client with pagination
|
||||
@@ -272,6 +319,94 @@ func (s *Service) convertToDetailsWithStorage(batch *repository.GetBatchUploadWi
|
||||
return details
|
||||
}
|
||||
|
||||
// RecordOutcome inserts or upserts a per-file outcome row for a batch document.
|
||||
// Called by the batch worker during ZIP extraction to record the initial outcome
|
||||
// for each file.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - batchID: the batch this file belongs to
|
||||
// - filename: original filename from the ZIP
|
||||
// - outcome: the typed outcome status (e.g. repository.BatchOutcomeStatusSubmitted)
|
||||
// - errorDetail: optional error message for failure outcomes
|
||||
// - documentID: optional document ID (set for duplicates)
|
||||
//
|
||||
// Returns:
|
||||
// - error: database error, or nil
|
||||
func (s *Service) RecordOutcome(ctx context.Context, batchID uuid.UUID, filename string,
|
||||
outcome repository.BatchOutcomeStatus, errorDetail *string, documentID *uuid.UUID) error {
|
||||
return s.cfg.GetDBQueries().InsertBatchDocumentOutcome(ctx, &repository.InsertBatchDocumentOutcomeParams{
|
||||
BatchID: batchID,
|
||||
Filename: filename,
|
||||
Column3: outcome,
|
||||
ErrorDetail: errorDetail,
|
||||
DocumentID: documentID,
|
||||
})
|
||||
}
|
||||
|
||||
// ListOutcomes retrieves all per-file outcome rows for a batch, with clean
|
||||
// failure detail joined from the currentCleanEntries view.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - batchID: the batch UUID
|
||||
//
|
||||
// Returns:
|
||||
// - []*BatchDocumentOutcome: list of outcome rows ordered by creation time
|
||||
// - error: database error, or nil
|
||||
func (s *Service) ListOutcomes(ctx context.Context, batchID uuid.UUID) ([]*BatchDocumentOutcome, error) {
|
||||
rows, err := s.cfg.GetDBQueries().ListBatchDocumentOutcomes(ctx, batchID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outcomes := make([]*BatchDocumentOutcome, len(rows))
|
||||
for i, row := range rows {
|
||||
o := &BatchDocumentOutcome{
|
||||
ID: row.ID,
|
||||
BatchID: row.BatchID,
|
||||
Filename: row.Filename,
|
||||
Outcome: string(row.Outcome),
|
||||
ErrorDetail: row.ErrorDetail,
|
||||
DocumentID: row.DocumentID,
|
||||
CreatedAt: s.pgTimestampToTime(row.CreatedAt),
|
||||
UpdatedAt: s.pgTimestampToTime(row.UpdatedAt),
|
||||
}
|
||||
if row.CleanFail.Valid {
|
||||
failStr := string(row.CleanFail.Cleanfailtype)
|
||||
o.CleanFail = &failStr
|
||||
}
|
||||
outcomes[i] = o
|
||||
}
|
||||
|
||||
return outcomes, nil
|
||||
}
|
||||
|
||||
// CheckDuplicate checks if a document with the given hash already exists for
|
||||
// the specified client. Returns the existing document's UUID if found, nil otherwise.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - clientID: the client identifier
|
||||
// - hash: the SHA-256 hash of the file content
|
||||
//
|
||||
// Returns:
|
||||
// - *uuid.UUID: existing document ID if a duplicate exists, nil otherwise
|
||||
// - error: database error (other than not-found), or nil
|
||||
func (s *Service) CheckDuplicate(ctx context.Context, clientID string, hash string) (*uuid.UUID, error) {
|
||||
id, err := s.cfg.GetDBQueries().GetDocumentIDByHash(ctx, &repository.GetDocumentIDByHashParams{
|
||||
Clientid: clientID,
|
||||
Hash: hash,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
// ListUnprocessed retrieves all unprocessed batch uploads across all clients
|
||||
func (s *Service) ListUnprocessed(ctx context.Context) ([]*BatchUploadSummary, error) {
|
||||
batches, err := s.cfg.GetDBQueries().GetUnprocessedBatches(ctx)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -559,3 +560,194 @@ func TestListUnprocessed_DatabaseError(t *testing.T) {
|
||||
assert.Nil(t, unprocessedBatches)
|
||||
assert.Contains(t, err.Error(), "failed to query unprocessed batches")
|
||||
}
|
||||
|
||||
// TestRecordOutcome verifies that RecordOutcome inserts a per-file outcome row
|
||||
// and that it can be read back via ListOutcomes.
|
||||
func TestRecordOutcome(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
service := documentbatch.New(cfg)
|
||||
|
||||
clientID := "test_record_outcome_" + uuid.New().String()[:8]
|
||||
err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{
|
||||
Clientid: clientID,
|
||||
Name: "Test Client Record Outcome",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
batchID, err := service.Create(ctx, clientID, "outcomes.zip", 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert multiple outcomes
|
||||
err = service.RecordOutcome(ctx, batchID, "good.pdf", repository.BatchOutcomeStatusSubmitted, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
errMsg := "file is not a PDF"
|
||||
err = service.RecordOutcome(ctx, batchID, "notes.txt", repository.BatchOutcomeStatusInvalidType, &errMsg, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify persistence via ListOutcomes
|
||||
outcomes, err := service.ListOutcomes(ctx, batchID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outcomes, 2)
|
||||
|
||||
// Verify the outcome details
|
||||
found := map[string]string{}
|
||||
for _, o := range outcomes {
|
||||
found[o.Filename] = o.Outcome
|
||||
}
|
||||
assert.Equal(t, "submitted", found["good.pdf"])
|
||||
assert.Equal(t, "invalid_type", found["notes.txt"])
|
||||
}
|
||||
|
||||
// TestListOutcomes inserts outcomes for various states and verifies the listing.
|
||||
func TestListOutcomes(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
service := documentbatch.New(cfg)
|
||||
|
||||
clientID := "test_list_outcomes_" + uuid.New().String()[:8]
|
||||
err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{
|
||||
Clientid: clientID,
|
||||
Name: "Test Client List Outcomes",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
batchID, err := service.Create(ctx, clientID, "list.zip", 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert a variety of outcomes
|
||||
err = service.RecordOutcome(ctx, batchID, "a.pdf", repository.BatchOutcomeStatusSubmitted, nil, nil)
|
||||
require.NoError(t, err)
|
||||
err = service.RecordOutcome(ctx, batchID, "b.pdf", repository.BatchOutcomeStatusSubmitted, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
errOpen := "cannot open"
|
||||
err = service.RecordOutcome(ctx, batchID, "c.pdf", repository.BatchOutcomeStatusFailedOpen, &errOpen, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.RecordOutcome(ctx, batchID, "d.txt", repository.BatchOutcomeStatusInvalidType, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dupID := uuid.New()
|
||||
// Create a real document for the duplicate reference
|
||||
realDocID, createErr := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: "hash_dup",
|
||||
BatchID: &batchID,
|
||||
})
|
||||
require.NoError(t, createErr)
|
||||
_ = dupID // not needed since we use the real ID
|
||||
err = service.RecordOutcome(ctx, batchID, "e.pdf", repository.BatchOutcomeStatusDuplicate, nil, &realDocID)
|
||||
require.NoError(t, err)
|
||||
|
||||
outcomes, err := service.ListOutcomes(ctx, batchID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outcomes, 5)
|
||||
|
||||
// Verify ordering is by created_at ASC
|
||||
expectedFilenames := []string{"a.pdf", "b.pdf", "c.pdf", "d.txt", "e.pdf"}
|
||||
for i, o := range outcomes {
|
||||
assert.Equal(t, expectedFilenames[i], o.Filename)
|
||||
}
|
||||
|
||||
// Verify the duplicate has document_id set
|
||||
assert.NotNil(t, outcomes[4].DocumentID)
|
||||
assert.Equal(t, realDocID, *outcomes[4].DocumentID)
|
||||
}
|
||||
|
||||
// TestListOutcomes_WithCleanFail verifies that the clean_fail column from
|
||||
// the currentCleanEntries view is joined when outcome is clean_failed.
|
||||
func TestListOutcomes_WithCleanFail(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
service := documentbatch.New(cfg)
|
||||
|
||||
clientID := "test_clean_fail_" + uuid.New().String()[:8]
|
||||
err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{
|
||||
Clientid: clientID,
|
||||
Name: "Test Client Clean Fail",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
batchID, err := service.Create(ctx, clientID, "cleanfail.zip", 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a real document
|
||||
docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: "hash_clean_fail",
|
||||
BatchID: &batchID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert a clean_failed outcome
|
||||
err = service.RecordOutcome(ctx, batchID, "corrupt.pdf", repository.BatchOutcomeStatusCleanFailed, nil, &docID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a documentClean with fail type and a documentCleanEntry to populate
|
||||
// the currentCleanEntries view
|
||||
cleanID, err := cfg.GetDBQueries().AddDocumentClean(ctx, &repository.AddDocumentCleanParams{
|
||||
Documentid: docID,
|
||||
Fail: repository.NullCleanfailtype{
|
||||
Cleanfailtype: repository.CleanfailtypeInvalidMimetype,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = cfg.GetDBQueries().AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{
|
||||
Cleanid: cleanID,
|
||||
Version: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
outcomes, err := service.ListOutcomes(ctx, batchID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outcomes, 1)
|
||||
|
||||
assert.Equal(t, "clean_failed", outcomes[0].Outcome)
|
||||
require.NotNil(t, outcomes[0].CleanFail)
|
||||
assert.Equal(t, "invalid_mimetype", *outcomes[0].CleanFail)
|
||||
}
|
||||
|
||||
// TestCheckDuplicate creates a document, then verifies that CheckDuplicate
|
||||
// returns its ID when the hash matches.
|
||||
func TestCheckDuplicate(t *testing.T) {
|
||||
cfg := &TestConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
ctx := t.Context()
|
||||
|
||||
service := documentbatch.New(cfg)
|
||||
|
||||
clientID := "test_check_dup_" + uuid.New().String()[:8]
|
||||
err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{
|
||||
Clientid: clientID,
|
||||
Name: "Test Client Check Dup",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a document with a known hash
|
||||
knownHash := "sha256_known_hash_abcdef"
|
||||
docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{
|
||||
Clientid: clientID,
|
||||
Hash: knownHash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check duplicate with the same hash -- should find it
|
||||
existingID, err := service.CheckDuplicate(ctx, clientID, knownHash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, existingID)
|
||||
assert.Equal(t, docID, *existingID)
|
||||
|
||||
// Check duplicate with a different hash -- should return nil
|
||||
noMatch, err := service.CheckDuplicate(ctx, clientID, "totally_different_hash")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, noMatch)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user