package processing import ( "context" "encoding/json" "os" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) func TestRetryJobResetsCompletedJobForFullRerun(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 err = pg.QueryRow(ctx, ` SELECT company_id FROM raw_products WHERE company_id IS NOT NULL ORDER BY updated_at DESC LIMIT 1`).Scan(&companyID) if errorsIsNoRows(err) { t.Skip("no raw_products rows available") } if err != nil { t.Fatal(err) } var userID uuid.UUID err = pg.QueryRow(ctx, `SELECT id FROM users ORDER BY created_at DESC LIMIT 1`).Scan(&userID) if errorsIsNoRows(err) { t.Skip("no users rows available") } if err != nil { t.Fatal(err) } rows, err := pg.Query(ctx, ` SELECT id FROM raw_products WHERE company_id = $1 ORDER BY updated_at DESC LIMIT 2`, companyID) if err != nil { t.Fatal(err) } defer rows.Close() rawIDs := make([]uuid.UUID, 0, 2) for rows.Next() { var rawID uuid.UUID if err := rows.Scan(&rawID); err != nil { t.Fatal(err) } rawIDs = append(rawIDs, rawID) } if err := rows.Err(); err != nil { t.Fatal(err) } if len(rawIDs) == 0 { t.Skip("no raw_products for selected company") } progress := InitialStepProgress("full") progressJSON, err := json.Marshal(progress) if err != nil { t.Fatal(err) } firstStep := "" if len(progress) > 0 { firstStep = progress[0].Step } var jobID uuid.UUID err = pg.QueryRow(ctx, ` INSERT INTO processing_jobs ( company_id, user_id, status, total_products, processed_products, processing_type, current_step, step_progress, started_at, completed_at ) VALUES ($1, $2, 'completed', $3, $3, 'full', $4, $5::jsonb, now(), now()) RETURNING id`, companyID, userID, len(rawIDs), firstStep, progressJSON, ).Scan(&jobID) if err != nil { t.Fatal(err) } defer func() { _, _ = pg.Exec(context.Background(), `DELETE FROM processing_job_products WHERE job_id = $1`, jobID) _, _ = pg.Exec(context.Background(), `DELETE FROM processing_jobs WHERE id = $1`, jobID) }() for _, rawID := range rawIDs { if _, err := pg.Exec(ctx, ` INSERT INTO processing_job_products (job_id, raw_product_id, status) VALUES ($1, $2, 'processed')`, jobID, rawID); err != nil { t.Fatal(err) } } p := NewPipeline(pg) p.Billing = nil job, err := p.RetryJob(ctx, companyID, jobID) if err != nil { t.Fatal(err) } if job.Status != "pending" { t.Fatalf("status=%q", job.Status) } if job.ProcessedProducts != 0 { t.Fatalf("processed_products=%d", job.ProcessedProducts) } if job.StartedAt != nil { t.Fatalf("started_at should be reset, got %v", *job.StartedAt) } if job.CompletedAt != nil { t.Fatalf("completed_at should be reset, got %v", *job.CompletedAt) } var pendingCount, processedCount int err = pg.QueryRow(ctx, ` SELECT COUNT(*) FILTER (WHERE status = 'pending'), COUNT(*) FILTER (WHERE status = 'processed') FROM processing_job_products WHERE job_id = $1`, jobID).Scan(&pendingCount, &processedCount) if err != nil { t.Fatal(err) } if pendingCount != len(rawIDs) { t.Fatalf("pending_count=%d want %d", pendingCount, len(rawIDs)) } if processedCount != 0 { t.Fatalf("processed_count=%d want 0", processedCount) } } func TestCancelJobCancelsPendingProducts(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 err = pg.QueryRow(ctx, ` SELECT company_id FROM raw_products WHERE company_id IS NOT NULL ORDER BY updated_at DESC LIMIT 1`).Scan(&companyID) if errorsIsNoRows(err) { t.Skip("no raw_products rows available") } if err != nil { t.Fatal(err) } var userID uuid.UUID err = pg.QueryRow(ctx, `SELECT id FROM users ORDER BY created_at DESC LIMIT 1`).Scan(&userID) if errorsIsNoRows(err) { t.Skip("no users rows available") } if err != nil { t.Fatal(err) } var rawID uuid.UUID err = pg.QueryRow(ctx, ` SELECT id FROM raw_products WHERE company_id = $1 ORDER BY updated_at DESC LIMIT 1`, companyID).Scan(&rawID) if errorsIsNoRows(err) { t.Skip("no raw_products for selected company") } if err != nil { t.Fatal(err) } progress := InitialStepProgress("full") progressJSON, err := marshalStepProgress(progress) if err != nil { t.Fatal(err) } firstStep := "" if len(progress) > 0 { firstStep = progress[0].Step } var jobID uuid.UUID err = pg.QueryRow(ctx, ` INSERT INTO processing_jobs ( company_id, user_id, status, total_products, processed_products, processing_type, current_step, step_progress, started_at ) VALUES ($1, $2, 'running', 1, 0, 'full', $3, $4::jsonb, now()) RETURNING id`, companyID, userID, firstStep, progressJSON, ).Scan(&jobID) if err != nil { t.Fatal(err) } defer func() { _, _ = pg.Exec(context.Background(), `DELETE FROM processing_job_products WHERE job_id = $1`, jobID) _, _ = pg.Exec(context.Background(), `DELETE FROM processing_jobs WHERE id = $1`, jobID) }() if _, err := pg.Exec(ctx, ` INSERT INTO processing_job_products (job_id, raw_product_id, status) VALUES ($1, $2, 'pending')`, jobID, rawID); err != nil { t.Fatal(err) } p := NewPipeline(pg) p.Billing = nil job, err := p.CancelJob(ctx, companyID, jobID) if err != nil { t.Fatal(err) } if job.Status != "cancelled" { t.Fatalf("status=%q", job.Status) } var productStatus string err = pg.QueryRow(ctx, ` SELECT status FROM processing_job_products WHERE job_id = $1`, jobID).Scan(&productStatus) if err != nil { t.Fatal(err) } if productStatus != "cancelled" { t.Fatalf("product status=%q want cancelled", productStatus) } } func errorsIsNoRows(err error) bool { return err == pgx.ErrNoRows }