package processing import ( "context" "errors" "os" "testing" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) func TestCleanupOrphanProcessedReportAndDelete(t *testing.T) { dsn := os.Getenv("DATABASE_URL") if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatal(err) } defer pg.Close() var companyID uuid.UUID // Prefer a non-A1 company so the A1 cohort guard does not block the delete path. err = pg.QueryRow(ctx, ` SELECT id FROM companies WHERE lower(trim(coalesce(legacy_company_id, ''))) <> lower($1) ORDER BY created_at DESC LIMIT 1`, billing.A1LegacyCompanyID).Scan(&companyID) if errorsIsNoRows(err) { t.Skip("no non-A1 companies available") } if err != nil { t.Fatal(err) } rawID := uuid.New() if _, err := pg.Exec(ctx, ` INSERT INTO raw_products ( id, company_id, gtin, raw_data, mapped_data, is_processed, processing_status ) VALUES ($1, $2, $3, '{}'::jsonb, '{}'::jsonb, false, 'unprocessed')`, rawID, companyID, "orphan-test-"+rawID.String()[:8]); err != nil { t.Fatal(err) } defer func() { _, _ = pg.Exec(context.Background(), `DELETE FROM processed_products WHERE raw_product_id = $1`, rawID) _, _ = pg.Exec(context.Background(), `DELETE FROM raw_products WHERE id = $1`, rawID) }() var processedID uuid.UUID err = pg.QueryRow(ctx, ` INSERT INTO processed_products ( company_id, raw_product_id, product_id, name, status ) VALUES ($1, $2, $3, 'orphan cleanup fixture', 'needs_review') RETURNING id`, companyID, rawID, "orphan-gtin").Scan(&processedID) if err != nil { t.Fatal(err) } report, err := ReportOrphanProcessed(ctx, pg) if err != nil { t.Fatal(err) } if report.Total < 1 { t.Fatalf("report total=%d want >= 1", report.Total) } dry, err := CleanupOrphanProcessed(ctx, pg, false) if err != nil { t.Fatal(err) } if dry.Confirmed { t.Fatal("dry-run should leave confirmed=false") } if dry.Deleted != 0 { t.Fatalf("dry-run deleted=%d want 0", dry.Deleted) } var stillThere int if err := pg.QueryRow(ctx, `SELECT COUNT(*) FROM processed_products WHERE id = $1`, processedID).Scan(&stillThere); err != nil { t.Fatal(err) } if stillThere != 1 { t.Fatalf("fixture row missing before confirm delete") } res, err := CleanupOrphanProcessed(ctx, pg, true) if err != nil { t.Fatal(err) } if res.Deleted < 1 { t.Fatalf("deleted=%d want >= 1", res.Deleted) } if err := pg.QueryRow(ctx, `SELECT COUNT(*) FROM processed_products WHERE id = $1`, processedID).Scan(&stillThere); err != nil { t.Fatal(err) } if stillThere != 0 { t.Fatalf("fixture row still present after confirm delete") } // Fail-closed: confirm with zero remaining orphans must refuse. after, err := ReportOrphanProcessed(ctx, pg) if err != nil { t.Fatal(err) } if after.Total != 0 { t.Logf("skip empty-refuse assert: other orphans remain total=%d", after.Total) return } _, err = CleanupOrphanProcessed(ctx, pg, true) if !errors.Is(err, ErrOrphanCleanupEmpty) { t.Fatalf("empty confirm err=%v want ErrOrphanCleanupEmpty", err) } }