package http import ( "encoding/json" "errors" "log/slog" "net/http" "net/http/httptest" "testing" "github.com/pocketbase/pocketbase/apis" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase" "git.vakhrushev.me/av/transcriber/internal/entity" ) // Ответ об одной записи — то, ради чего эндпойнт и существует; ниже судятся его // ветки: готовый текст, остановленная запись и отказ хранилища на чтении текста. // statusOf спрашивает состояние записи от имени её владельца. func statusOf(t *testing.T, env *testEnv, recordID string) *httptest.ResponseRecorder { t.Helper() w := httptest.NewRecorder() env.serve(w, httptest.NewRequest("GET", "/api/status/"+recordID, http.NoBody)) return w } // Готовая расшифровка доезжает до отправителя полем `transcription_text`, и // уходит в него **сырая** расшифровка: видов текста больше одного, и отдача // «последнего записанного» сделала бы ответ функцией порядка записи. func TestGetTranscribeJobStatus_ReturnsTranscript(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) record := jobWithFile(t, env) texts := pbrepo.NewTextRepository(env.app) transcript, err := texts.Put(record.Id, entity.TextKindTranscript, "сырая расшифровка") require.NoError(t, err) literary, err := texts.Put(record.Id, entity.TextKindLiterary, "вычитанный текст") require.NoError(t, err) record.TranscriptTextID = &transcript.Id record.LiteraryTextID = &literary.Id record.MoveToState(entity.StateDone) require.NoError(t, env.handler.recordRepo.Save(record, "")) w := statusOf(t, env, record.Id) require.Equal(t, http.StatusOK, w.Code) var response GetTranscribeJobResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response)) assert.Equal(t, entity.StateDone, response.State) require.NotNil(t, response.TranscriptionText, "готовый текст доехал до отправителя") assert.Equal(t, "сырая расшифровка", *response.TranscriptionText) assert.NotContains(t, w.Body.String(), "вычитанный текст", "вычитанный текст этим полем не подменяется: значение поля не должно меняться от того, успел ли необязательный шаг") } // Остановленная запись отдаёт рубеж, на котором встала, и признак остановки // отдельным полем: отказ перестал быть состоянием, и без признака такая запись // выглядела бы обычной, стоящей на своём рубеже. func TestGetTranscribeJobStatus_HaltedIsVisible(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) record := jobWithFile(t, env) record.MoveToState(entity.StateNormalized) record.Halt(entity.HaltReasonStepFailed, "сбой конвертации файла") require.NoError(t, env.handler.recordRepo.Save(record, "")) w := statusOf(t, env, record.Id) require.Equal(t, http.StatusOK, w.Code) var response GetTranscribeJobResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response)) assert.Equal(t, entity.StateNormalized, response.State, "рубеж тот, на котором запись встала") assert.True(t, response.Halted, "признак остановки виден отправителю") assert.NotContains(t, w.Body.String(), "сбой конвертации файла", "машинный текст отказа принадлежит журналу владельца, а не ответу отправителю") } // Пока запись не дошла до текста, поля нет вовсе: пустая строка на его месте // читается как «расшифровка пуста». func TestGetTranscribeJobStatus_RunningRecordHasNoHaltedFlag(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) record := jobWithFile(t, env) w := statusOf(t, env, record.Id) require.Equal(t, http.StatusOK, w.Code) var response GetTranscribeJobResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response)) assert.Equal(t, entity.StateUploaded, response.State) assert.False(t, response.Halted, "запись в работе остановленной не значится") assert.Nil(t, response.TranscriptionText) } // failingTextRepo отказывает на чтении текста — так выглядит недоступное // хранилище. Битую ссылку схема завести не даёт (связь проверяется при // сохранении), и это её защита, а не пробел: остаётся отказ самого чтения. type failingTextRepo struct{} func (r *failingTextRepo) Put(string, string, string) (*entity.Text, error) { return nil, errors.New("не зовётся этой проверкой") } func (r *failingTextRepo) GetByID(string) (*entity.Text, error) { return nil, errors.New("хранилище недоступно") } // Отказ чтения текста — это отказ хранилища, а не «записи нет». Отправителю он // приходит своим кодом, и владелец сервиса узнаёт об аварии из журнала — иначе // она читалась бы отправителю как «вашей записи не существует», а владельцем не // замечалась бы вовсе. func TestGetTranscribeJobStatus_TextReadFailureIsNotANotFound(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) record := jobWithFile(t, env) texts := pbrepo.NewTextRepository(env.app) transcript, err := texts.Put(record.Id, entity.TextKindTranscript, "сырая расшифровка") require.NoError(t, err) record.TranscriptTextID = &transcript.Id require.NoError(t, env.handler.recordRepo.Save(record, "")) // Обработчик пересобирается с отказывающим хранилищем текстов: остальная // цепочка та же, что и в проде. journal := &journalBuffer{} handler := NewTranscribeHandler( env.handler.recordRepo, &failingTextRepo{}, env.handler.trsService, slog.New(slog.NewTextHandler(journal, nil)), ) r, err := apis.NewRouter(env.app) require.NoError(t, err) handler.Register(r) mux, err := r.BuildMux() require.NoError(t, err) req := httptest.NewRequest("GET", "/api/status/"+record.Id, http.NoBody) req.AddCookie(&http.Cookie{Name: SessionCookieName, Value: env.session}) w := httptest.NewRecorder() mux.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code, "отказ хранилища не выдаётся за отсутствие записи") assert.NotContains(t, w.Body.String(), "хранилище недоступно", "внутренности наружу не выходят") assert.Contains(t, journal.String(), "Failed to read transcript", "владелец сервиса узнаёт об аварии из журнала") }