- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
201 lines
9.3 KiB
Go
201 lines
9.3 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
|
)
|
|
|
|
// Проверки разграничения записей по владельцу. Все идут через собранный роутер:
|
|
// сужение живёт в хранилище, но судится по тому, что видит отправитель.
|
|
|
|
// newSecondAccount заводит вторую учётную запись со своим логином. Постоянный
|
|
// адрес почты первой занят, и повторное сохранение отвергается — адрес здесь
|
|
// свой.
|
|
func newSecondAccount(t *testing.T, app core.App) (*core.Record, string) {
|
|
t.Helper()
|
|
|
|
const login = "stranger"
|
|
|
|
users, err := app.FindCollectionByNameOrId("users")
|
|
require.NoError(t, err)
|
|
|
|
record := core.NewRecord(users)
|
|
record.Set(migrations.ProviderLoginField, login)
|
|
record.Set("email", "stranger@example.com")
|
|
record.Set("verified", true)
|
|
record.SetRandomPassword()
|
|
require.NoError(t, app.Save(record))
|
|
|
|
return record, login
|
|
}
|
|
|
|
// serveAs шлёт запрос от имени названного логина, а не логина окружения.
|
|
func serveAs(env *testEnv, login string, w http.ResponseWriter, req *http.Request) {
|
|
env.mux.ServeHTTP(w, asUser(req, login))
|
|
}
|
|
|
|
// Чужая задача неотличима от несуществующей: тот же код и то же тело. Разница
|
|
// ответов обратила бы опрос в перебор — по ней считывается, какие задачи
|
|
// заведены.
|
|
func TestGetTranscribeJobStatus_ForeignJobLooksMissing(t *testing.T) {
|
|
env := setupTestEnv(t, readableMetaViewer())
|
|
|
|
job := jobWithFile(t, env)
|
|
_, stranger := newSecondAccount(t, env.app)
|
|
|
|
foreign := httptest.NewRecorder()
|
|
serveAs(env, stranger, foreign, httptest.NewRequest("GET", "/app/audiorecords/"+job.Id, http.NoBody))
|
|
|
|
unknown := httptest.NewRecorder()
|
|
serveAs(env, stranger, unknown, httptest.NewRequest("GET", "/app/audiorecords/unknown0000000000", http.NoBody))
|
|
|
|
require.Equal(t, http.StatusNotFound, foreign.Code, "чужая задача не отдаётся")
|
|
assert.Equal(t, unknown.Code, foreign.Code, "код тот же, что у неизвестного идентификатора")
|
|
assert.JSONEq(t, unknown.Body.String(), foreign.Body.String(), "и тело то же")
|
|
|
|
// Ни состояния, ни текста расшифровки в теле нет.
|
|
assert.NotContains(t, foreign.Body.String(), entity.StateUploaded)
|
|
assert.NotContains(t, foreign.Body.String(), "transcription_text")
|
|
}
|
|
|
|
// Владельцем принятой записи становится предъявитель сессии — и у задачи, и у
|
|
// её файла.
|
|
func TestCreateTranscribeJob_OwnerIsSession(t *testing.T) {
|
|
env := setupTestEnv(t, readableMetaViewer())
|
|
|
|
w := httptest.NewRecorder()
|
|
env.serve(w, createMultipartRequest(t, "sample.mp3", []byte("запись")))
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
response := intakeItemOf(t, w)
|
|
|
|
record, err := env.app.FindRecordById(migrations.RecordsCollection, response.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, env.account.Id, record.GetString("owner"), "владелец задачи — предъявитель")
|
|
|
|
fileRecord, err := env.app.FindRecordById("files", record.GetString("original_file"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, env.account.Id, fileRecord.GetString("owner"), "владелец файла — он же")
|
|
}
|
|
|
|
// Владельца не задают запросом: своё значение в форме на результат не влияет.
|
|
func TestCreateTranscribeJob_OwnerFieldFromRequestIgnored(t *testing.T) {
|
|
env := setupTestEnv(t, readableMetaViewer())
|
|
|
|
_, stranger := newSecondAccount(t, env.app)
|
|
|
|
req := createMultipartRequest(t, "sample.mp3", []byte("запись"))
|
|
query := req.URL.Query()
|
|
query.Set("owner", stranger)
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
w := httptest.NewRecorder()
|
|
env.serve(w, req)
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
response := intakeItemOf(t, w)
|
|
|
|
record, err := env.app.FindRecordById(migrations.RecordsCollection, response.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, env.account.Id, record.GetString("owner"))
|
|
}
|
|
|
|
// Предъявитель, чья сессия не даёт учётной записи пользователя, получает отказ
|
|
// до чтения тела. Владелец панели — именно такой: узнан он узнан, а записи в
|
|
// коллекции пользователей у него нет, и владельцем записи он стать не может.
|
|
//
|
|
// Отказ **до** укладки обязателен: позже пришлось бы убирать уже сохранённый
|
|
// файл, а уборки файлов сервис не умеет вовсе.
|
|
func TestCreateTranscribeJob_SuperuserSessionRejected(t *testing.T) {
|
|
env := setupTestEnv(t, readableMetaViewer())
|
|
|
|
superusers, err := env.app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
|
|
require.NoError(t, err)
|
|
|
|
admin := core.NewRecord(superusers)
|
|
admin.Set("email", "owner@example.com")
|
|
admin.SetRandomPassword()
|
|
require.NoError(t, env.app.Save(admin))
|
|
|
|
token, err := admin.NewAuthToken()
|
|
require.NoError(t, err)
|
|
|
|
// Владелец панели предъявляет **свой токен**, а не заголовок: заголовок ему
|
|
// никто не ставит, и узнавание по нему его учётной записи не касается.
|
|
req := createMultipartRequest(t, "sample.mp3", []byte("запись"))
|
|
req.Header.Set("Authorization", token)
|
|
|
|
w := httptest.NewRecorder()
|
|
env.mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "узнан, но не запись коллекции пользователей")
|
|
assert.Equal(t, 0, countJobs(t, env), "задачи не заведено")
|
|
assert.Equal(t, 0, countFiles(t, env), "и файла тоже")
|
|
}
|
|
|
|
// Чужой файл не отдаётся по ссылке, а свой отдаётся. Проверяется именно переход
|
|
// по ссылке: токен файла хранилище выдаёт на предъявителя, а не на файл, и отказ
|
|
// наступает на скачивании, где правило просмотра судит владельца. Проверка,
|
|
// написанная на выдачу токена, зеленела бы, не касаясь пути, по которому аудио и
|
|
// уходит.
|
|
func TestFileDownload_NarrowedByOwner(t *testing.T) {
|
|
env := setupTestEnv(t, readableMetaViewer())
|
|
|
|
job := jobWithFile(t, env)
|
|
_, stranger := newSecondAccount(t, env.app)
|
|
|
|
record, err := env.app.FindRecordById("files", *job.OriginalFileID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, env.account.Id, record.GetString("owner"))
|
|
|
|
link := "/api/files/files/" + record.Id + "/" + record.GetString("file")
|
|
|
|
// Переход по ссылке идёт **без** заголовка: файл судится своим коротким
|
|
// токеном, а не узнаванием. Так же по ней пойдёт и браузер.
|
|
mine := httptest.NewRecorder()
|
|
env.mux.ServeHTTP(mine,
|
|
httptest.NewRequest("GET", link+"?token="+fileToken(t, env, env.login), http.NoBody))
|
|
|
|
foreign := httptest.NewRecorder()
|
|
env.mux.ServeHTTP(foreign,
|
|
httptest.NewRequest("GET", link+"?token="+fileToken(t, env, stranger), http.NoBody))
|
|
|
|
require.Equal(t, http.StatusOK, mine.Code, "свой файл отдаётся")
|
|
assert.Equal(t, "запись", mine.Body.String(), "и отдаётся содержимым")
|
|
|
|
assert.NotEqual(t, http.StatusOK, foreign.Code, "чужой файл не отдаётся")
|
|
assert.NotContains(t, foreign.Body.String(), "запись", "содержимого в отказе нет")
|
|
}
|
|
|
|
// fileToken берёт у хранилища токен файла для названного логина. Токен выдаётся
|
|
// на предъявителя: о файле хранилище при выдаче не спрашивает.
|
|
//
|
|
// Адрес выдачи лежит в пространстве хранилища, а не приложения, и узнавание на
|
|
// нём работает поимённо — иначе весь путь «узнавание → токен файла → ссылка»
|
|
// обрывался бы на первом шаге, и файл записи стал бы недостижим для браузера.
|
|
func fileToken(t *testing.T, env *testEnv, login string) string {
|
|
t.Helper()
|
|
|
|
w := httptest.NewRecorder()
|
|
env.mux.ServeHTTP(w, asUser(
|
|
httptest.NewRequest("POST", "/api/files/token", http.NoBody), login))
|
|
require.Equal(t, http.StatusOK, w.Code, "токен файла выдаётся всякому узнанному")
|
|
|
|
var body struct {
|
|
Token string `json:"token"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
|
require.NotEmpty(t, body.Token)
|
|
|
|
return body.Token
|
|
}
|