Files
transcriber/internal/service/ownership_test.go
T
av 8af8ec2e54 у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы
  `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и
  несуществующая дают один ответ; правило просмотра файлов сужено им же
- приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи
  пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный
  файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается
- удаление учётной записи с записями отвергается стражем, и вешает его сама
  сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
2026-08-14 12:18:11 +03:00

106 lines
5.1 KiB
Go

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, "и файла тоже: отказ наступает раньше укладки")
}