package service import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.vakhrushev.me/av/transcriber/internal/contract" "git.vakhrushev.me/av/transcriber/internal/entity" ) // Выборка воркера владельцем не сужается: владелец решает, кому запись // показывать, а не кому её считать. Сужение остановило бы расшифровку записей // бота вовсе, а записи остальных поставило бы в зависимость от того, кто первым // завёл учётную запись. func TestWorkerTakesJobsOfEveryOwner(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) first, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("первая"), "one.mp3", newOwner(t, env.app)) require.NoError(t, err) second, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("вторая"), "two.mp3", newOwner(t, env.app)) require.NoError(t, err) // Третья пришла ботом, и владельца у неё нет вовсе. third := newTelegramJob(t, env) // Срок протухания в прошлом: захваченная задача остаётся за держателем, и // следующий вызов берёт следующую, а не ту же самую. taken := map[string]bool{} for _, holder := range []string{"one", "two", "three"} { job, err := env.jobRepo.FindAndAcquire(entity.StateCreated, holder, time.Now().Add(-time.Hour)) require.NoError(t, err, "воркер берёт задачи подряд, владельцем не сужаясь") taken[job.Id] = true } assert.True(t, taken[first.Id], "задача первого владельца досталась воркеру") assert.True(t, taken[second.Id], "и второго") assert.True(t, taken[third.Id], "и задача без владельца") _, err = env.jobRepo.FindAndAcquire(entity.StateCreated, "next", time.Now().Add(-time.Hour)) var missing *contract.JobNotFoundError assert.ErrorAs(t, err, &missing, "больше в этом состоянии никого") } // Захват читает владельца: снимок задачи, в котором он всегда пуст, был бы // ловушкой для первого же шага, начавшего сохранять задачу целиком, — и сегодня // уже ломал бы файл, который шаг заводит. func TestAcquireCarriesOwner(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) owner := newOwner(t, env.app) job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "one.mp3", owner) require.NoError(t, err) acquired, err := env.jobRepo.FindAndAcquire(entity.StateCreated, "holder", time.Now().Add(-time.Hour)) require.NoError(t, err) require.Equal(t, job.Id, acquired.Id) require.NotNil(t, acquired.OwnerID, "владелец приехал из захвата") assert.Equal(t, owner, *acquired.OwnerID) } // Шаг конвейера владельца не затирает: сохранение кладёт только то, чем // распоряжается конвейер, и владельца среди этого нет. func TestPipelineStepKeepsOwner(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) owner := newOwner(t, env.app) job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "one.mp3", owner) require.NoError(t, err) // Отказ конвертации — приговор записи: шаг переводит задачу в `failed` и // сохраняет её. Это сохранение владельца тронуть не должно. require.NoError(t, env.service.FindAndRunConversionJob(t.Context())) after, err := readJob(env.app, job.Id) require.NoError(t, err) require.Equal(t, entity.StateFailed, after.State, "шаг записал свой приговор") require.NotNil(t, after.OwnerID, "владелец пережил шаг") assert.Equal(t, owner, *after.OwnerID) } // Приём из веба без владельца задачи не заводит. Обязательность держит здесь // код, а не схема: колонка допускает пустое значение ради записей бота. func TestCreateJobFromApiRequiresOwner(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) _, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "one.mp3", "") require.ErrorIs(t, err, contract.ErrOwnerRequired) records, err := env.app.FindAllRecords("transcribe_jobs") require.NoError(t, err) assert.Empty(t, records, "задачи не заведено") files, err := env.app.FindAllRecords("files") require.NoError(t, err) assert.Empty(t, files, "и файла тоже: отказ наступает раньше укладки") }