Files
transcriber/internal/service/ownership_test.go
T
av 1576d06735 внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
2026-08-14 20:20:33 +03:00

131 lines
6.3 KiB
Go

package service
import (
"strings"
"testing"
"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 TestWorkerTakesRecordsOfEveryOwner(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 := newTelegramRecord(t, env)
// Захваченная запись остаётся за держателем, и следующий вызов берёт
// следующую, а не ту же самую.
taken := map[string]bool{}
for range 3 {
acquired, err := env.recordRepo.FindAndAcquire(entity.WorkingStages())
require.NoError(t, err, "воркер берёт записи подряд, владельцем не сужаясь")
taken[acquired.ID] = true
}
assert.True(t, taken[first.Id], "запись первого владельца досталась воркеру")
assert.True(t, taken[second.Id], "и второго")
assert.True(t, taken[third.Id], "и запись без владельца")
_, err = env.recordRepo.FindAndAcquire(entity.WorkingStages())
var missing *contract.JobNotFoundError
assert.ErrorAs(t, err, &missing, "больше пригодных к работе записей нет")
}
// Захват отдаёт идентификатор и признак своего захвата — не перечень колонок.
// Колонки шаг читает сам: иначе всякая новая колонка записи попадала бы под
// инвариант проекта о колонках очереди.
func TestAcquireReturnsIdentifierAndHolder(t *testing.T) {
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
owner := newOwner(t, env.app)
record, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "one.mp3", owner)
require.NoError(t, err)
acquired, err := env.recordRepo.FindAndAcquire(entity.WorkingStages())
require.NoError(t, err)
assert.Equal(t, record.Id, acquired.ID, "захват назвал запись")
assert.NotEmpty(t, acquired.Holder, "и признак своего захвата")
// Колонки читаются отдельным чтением, и владелец среди них.
read := readRecord(t, env, record.Id)
require.NotNil(t, read.OwnerID)
assert.Equal(t, owner, *read.OwnerID)
assert.Equal(t, acquired.Holder, *read.AcquisitionID, "признак захвата записан в саму запись")
require.NotNil(t, read.AcquireExpiresAt, "срок протухания приехал с рубежом")
}
// Шаг конвейера владельца не затирает: сохранение кладёт только то, чем
// распоряжается конвейер, и владельца среди этого нет.
func TestPipelineStepKeepsOwner(t *testing.T) {
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
owner := newOwner(t, env.app)
record, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "one.mp3", owner)
require.NoError(t, err)
// Отказ приведения — приговор записи: шаг останавливает её и сохраняет. Это
// сохранение владельца тронуть не должно.
require.NoError(t, env.service.RunStep(t.Context()))
after := readRecord(t, env, record.Id)
require.True(t, after.IsHalted(), "шаг записал свой приговор")
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)
}
// Чужая запись, ничья и несуществующая отвечают одним и тем же: по разнице
// ответов иначе перебирается список заведённых записей.
func TestGetByIDHidesForeignRecords(t *testing.T) {
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
owner := newOwner(t, env.app)
stranger := newOwner(t, env.app)
record, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "one.mp3", owner)
require.NoError(t, err)
// Запись без владельца — принятая ботом.
orphan := newTelegramRecord(t, env)
var missing *contract.JobNotFoundError
_, err = env.recordRepo.GetByID(record.Id, stranger)
require.ErrorAs(t, err, &missing, "чужая запись неотличима от несуществующей")
_, err = env.recordRepo.GetByID(orphan.Id, stranger)
require.ErrorAs(t, err, &missing, "ничья запись не достаётся никому")
_, err = env.recordRepo.GetByID(record.Id, "")
require.ErrorAs(t, err, &missing, "пустой владелец не совпадает ни с чем")
own, err := env.recordRepo.GetByID(record.Id, owner)
require.NoError(t, err, "своя запись отдаётся")
assert.Equal(t, record.Id, own.Id)
}