package service import ( "errors" "fmt" "io" "log/slog" "testing" "time" "git.vakhrushev.me/av/transcriber/internal/contract" "git.vakhrushev.me/av/transcriber/internal/entity" ) // Путь признака «работы нет» состоит из двух звеньев: репозиторий рождает // «подходящей задачи не нашлось», сервис переводит это в «работы нет», и уже // его читает воркер. Проверки воркера подменяют работу целиком и второе звено // не видят — без этого файла правку в сервисе принимал бы только линтер, а он // судит форму записи, а не то, узнаётся ли признак на самом деле. // stubJobRepo отдаёт заданную ошибку на запрос задачи. Прочих методов запроса // задачи проверки этого файла не зовут. type stubJobRepo struct { err error } func (r *stubJobRepo) Create(*entity.TranscribeJob) error { return nil } func (r *stubJobRepo) Save(*entity.TranscribeJob) error { return nil } func (r *stubJobRepo) GetByID(string) (*entity.TranscribeJob, error) { return nil, errors.New("не зовётся этими проверками") } func (r *stubJobRepo) FindAndAcquire(string, string, time.Time) (*entity.TranscribeJob, error) { return nil, r.err } func serviceWithRepo(repo contract.TranscriptJobRepository) *TranscribeService { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) return NewTranscribeService(repo, nil, nil, nil, nil, nil, "", logger) } // Репозиторий вправе добавить своему отказу пояснение — соседние ветки того же // метода уже оборачивают ошибки `%w` подряд. Пока признак узнавался приведением // типа, первая такая обёртка превратила бы пустой прогон в отказ: воркер начал // бы писать в журнал раз в секунду на каждом из трёх воркеров. func TestFindJobTranslatesWrappedNotFoundToNoop(t *testing.T) { svc := serviceWithRepo(&stubJobRepo{ err: fmt.Errorf("find and acquire job: %w", &contract.JobNotFoundError{State: "created", Message: "appropriate job not found"}), }) _, err := svc.findJob("created", time.Minute) var noop *contract.NoopJobError if !errors.As(err, &noop) { t.Fatalf("обёрнутое «задачи нет» не переведено в пустой прогон: получено %v", err) } if noop.State != "created" { t.Errorf("состояние потеряно при переводе: %q", noop.State) } } // Оборотная сторона: настоящий отказ хранилища пустым прогоном считаться не // должен, иначе задача молча крутилась бы в цикле без единой записи. func TestFindJobKeepsRealFailure(t *testing.T) { svc := serviceWithRepo(&stubJobRepo{err: errors.New("database is gone")}) _, err := svc.findJob("created", time.Minute) var noop *contract.NoopJobError if errors.As(err, &noop) { t.Fatalf("отказ хранилища зачтён пустым прогоном: %v", err) } if err == nil { t.Fatal("отказ хранилища потерян") } }