package repository import ( "errors" "testing" "time" "github.com/vaultdrop/backend/dbtest" ) func newTestRepo(t *testing.T) *Repository { t.Helper() conn := dbtest.OpenTestDatabase(t, repositoryTestURL) return NewRepository(conn) } func seedOcrJobFixture(t *testing.T) (*Repository, string, string, string) { t.Helper() repo := newTestRepo(t) userID, err := repo.Users.Create("ocr-user", "ocr-user", "hash", false) if err != nil { t.Fatalf("create user: %v", err) } deviceID := NewID() if err := repo.Devices.Upsert(deviceID); err != nil { t.Fatalf("register device: %v", err) } fileID := NewID() if err := repo.Resources.InsertFile(userID, fileID, "scan.png", "", 10, nil, nil); err != nil { t.Fatalf("insert file: %v", err) } return repo, userID, deviceID, fileID } func TestOcrJobsLifecycleTransitions(t *testing.T) { repo, _, deviceID, fileID := seedOcrJobFixture(t) jobID := NewID() if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil { t.Fatalf("create: %v", err) } row, err := repo.OcrJobs.Get(deviceID, jobID) if err != nil || row.Status != "queued" { t.Fatalf("get (queued): %+v err=%v", row, err) } if err := repo.OcrJobs.TouchProcessing(deviceID, jobID); err != nil { t.Fatalf("touch processing: %v", err) } row, _ = repo.OcrJobs.Get(deviceID, jobID) if row.Status != "processing" { t.Errorf("status attendu processing, got %q", row.Status) } if err := repo.OcrJobs.Complete(deviceID, jobID, "HELLO"); err != nil { t.Fatalf("complete: %v", err) } row, _ = repo.OcrJobs.Get(deviceID, jobID) if row.Status != "done" || row.Text == nil || *row.Text != "HELLO" { t.Errorf("after complete: %+v", row) } } func TestOcrJobsFail(t *testing.T) { repo, _, deviceID, fileID := seedOcrJobFixture(t) jobID := NewID() if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil { t.Fatalf("create: %v", err) } if err := repo.OcrJobs.Fail(deviceID, jobID, "tesseract exploded"); err != nil { t.Fatalf("fail: %v", err) } row, err := repo.OcrJobs.Get(deviceID, jobID) if err != nil || row.Status != "failed" || row.Error == nil || *row.Error != "tesseract exploded" { t.Errorf("après fail : %+v err=%v", row, err) } } func TestOcrJobsGetScopedByDevice(t *testing.T) { repo, _, deviceID, fileID := seedOcrJobFixture(t) jobID := NewID() if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil { t.Fatalf("create: %v", err) } otherDevice := NewID() if err := repo.Devices.Upsert(otherDevice); err != nil { t.Fatalf("register other device: %v", err) } if _, err := repo.OcrJobs.Get(otherDevice, jobID); !errors.Is(err, ErrJobNotFound) { t.Errorf("get par un autre device : attendu ErrJobNotFound, got %v", err) } } func TestOcrJobsClaimNextFifo(t *testing.T) { repo, _, deviceID, fileID := seedOcrJobFixture(t) var jobIDs []string for i := 0; i < 3; i++ { jobID := NewID() if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil { t.Fatalf("create %d: %v", i, err) } jobIDs = append(jobIDs, jobID) } for i, want := range jobIDs { row, err := repo.OcrJobs.ClaimNext() if err != nil { t.Fatalf("claim %d: %v", i, err) } if row.ID != want { t.Errorf("claim %d = %q, attendu %q (FIFO)", i, row.ID, want) } if row.Status != "processing" { t.Errorf("claim %d status = %q, attendu processing", i, row.Status) } } if _, err := repo.OcrJobs.ClaimNext(); !errors.Is(err, ErrNoQueuedJobs) { t.Errorf("queue vide : attendu ErrNoQueuedJobs, got %v", err) } } func TestOcrJobsClaimNextSkipsProcessingAndDone(t *testing.T) { repo, _, deviceID, fileID := seedOcrJobFixture(t) job1 := NewID() job2 := NewID() if err := repo.OcrJobs.Create(job1, deviceID, fileID); err != nil { t.Fatalf("create job1: %v", err) } if err := repo.OcrJobs.Create(job2, deviceID, fileID); err != nil { t.Fatalf("create job2: %v", err) } if err := repo.OcrJobs.TouchProcessing(deviceID, job2); err != nil { t.Fatalf("touch job2 processing: %v", err) } row, err := repo.OcrJobs.ClaimNext() if err != nil { t.Fatalf("claim: %v", err) } if row.ID != job1 { t.Errorf("claim = %q, attendu job1 %q", row.ID, job1) } } func TestOcrJobsResetStaleProcessing(t *testing.T) { repo, _, deviceID, fileID := seedOcrJobFixture(t) jobID := NewID() if err := repo.OcrJobs.Create(jobID, deviceID, fileID); err != nil { t.Fatalf("create: %v", err) } if err := repo.OcrJobs.TouchProcessing(deviceID, jobID); err != nil { t.Fatalf("touch processing: %v", err) } // Started_at = NOW : pas encore stale → aucun requeue. if n, err := repo.OcrJobs.ResetStaleProcessing(time.Minute); err != nil { t.Fatalf("reset: %v", err) } else if n != 0 { t.Errorf("reset récent : attendu 0, got %d", n) } // Vieillit le started_at puis re-reset → 1 requeue. if _, err := repo.OcrJobs.DB.Exec(`UPDATE ocr_jobs SET started_at = NOW() - interval '2 minutes' WHERE job_id = $1`, jobID); err != nil { t.Fatalf("vieillir started_at: %v", err) } if n, err := repo.OcrJobs.ResetStaleProcessing(time.Minute); err != nil { t.Fatalf("reset stale: %v", err) } else if n != 1 { t.Errorf("reset stale : attendu 1, got %d", n) } row, err := repo.OcrJobs.Get(deviceID, jobID) if err != nil { t.Fatalf("get: %v", err) } if row.Status != "queued" { t.Errorf("status après reset = %q, attendu queued", row.Status) } }