у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и несуществующая дают один ответ; правило просмотра файлов сужено им же - приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается - удаление учётной записи с записями отвергается стражем, и вешает его сама сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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, "и файла тоже: отказ наступает раньше укладки")
|
||||
}
|
||||
Reference in New Issue
Block a user