- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
140 lines
7.4 KiB
Go
140 lines
7.4 KiB
Go
package pocketbase
|
|
|
|
import (
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
// newTestStorage поднимает хранилище на пустом каталоге и накатывает схему —
|
|
// тем же путём, каким это делает сервис при старте.
|
|
func newTestStorage(t *testing.T) core.App {
|
|
t.Helper()
|
|
|
|
app, err := New(t.TempDir())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if err := app.ResetBootstrapState(); err != nil {
|
|
t.Logf("не удалось закрыть хранилище: %v", err)
|
|
}
|
|
})
|
|
|
|
return app
|
|
}
|
|
|
|
// Критерий приёмки 10. Содержимое записи закрыто во всех коллекциях, куда оно
|
|
// переехало.
|
|
//
|
|
// Прежде содержимое лежало одной колонкой задачи, и закрывала его одна норма про
|
|
// файл записи. Теперь оно живёт в шести коллекциях, и реализация, следующая
|
|
// только прежней норме, завела бы поле вложения с умолчанием библиотеки: ссылка
|
|
// на сырой ответ провайдера — а это полный текст речи — отдавала бы его любому,
|
|
// кто её знает, без сессии.
|
|
func TestRecordContentIsClosedEverywhere(t *testing.T) {
|
|
app := newTestStorage(t)
|
|
|
|
// Правило просмотра остаётся незаданным, то есть «только владелец панели».
|
|
// Содержимое отдаёт собственный адрес сервиса, а не поверхность хранилища;
|
|
// непустое правило открыло бы перечисление коллекции впрок.
|
|
for _, name := range []string{
|
|
migrations.RecordsCollection,
|
|
migrations.TextsCollection,
|
|
migrations.StructuresCollection,
|
|
migrations.RecognitionsCollection,
|
|
migrations.RecordEventsCollection,
|
|
migrations.TopicsCollection,
|
|
} {
|
|
collection, err := app.FindCollectionByNameOrId(name)
|
|
require.NoError(t, err, "коллекция %s заведена шагом схемы", name)
|
|
|
|
assert.Nil(t, collection.ListRule, "перечисление %s закрыто", name)
|
|
assert.Nil(t, collection.ViewRule, "чтение %s закрыто", name)
|
|
assert.Nil(t, collection.CreateRule, "заведение записи в %s закрыто", name)
|
|
assert.Nil(t, collection.UpdateRule, "правка %s закрыта", name)
|
|
assert.Nil(t, collection.DeleteRule, "удаление из %s закрыто", name)
|
|
}
|
|
|
|
// А поле вложения помечено защищённым: без пометки ссылка открывает
|
|
// содержимое любому, кто её знает, и знание ссылки становится правом.
|
|
recognitions, err := app.FindCollectionByNameOrId(migrations.RecognitionsCollection)
|
|
require.NoError(t, err)
|
|
|
|
field := recognitions.Fields.GetByName("payload")
|
|
require.NotNil(t, field, "поле сохранённого ответа заведено")
|
|
|
|
file, ok := field.(*core.FileField)
|
|
require.True(t, ok, "сохранённый ответ лежит вложением, а не колонкой")
|
|
assert.True(t, file.Protected, "поле вложения защищено")
|
|
}
|
|
|
|
// Прежняя коллекция задач уходит вместе с моделью: данных под ней не было, а
|
|
// пустая копия висела бы в панели вторым домом для понятия, которого больше нет.
|
|
// Поверхность коллекции пользователей закрыта наглухо — все пять правил.
|
|
//
|
|
// Проверка стоит отдельно от соседней намеренно: та сторожит коллекции, которые
|
|
// заводит наш шаг схемы, а эту заводит системный шаг библиотеки, и её умолчания
|
|
// открывают владельцу записи чтение, правку и удаление собственной строки. Пока
|
|
// узнавание жило под корнем приложения, до этой поверхности браузер не
|
|
// дотягивался вовсе; с узнаванием по заголовку она достижима, а ключ учётной
|
|
// записи лежит здесь обычной колонкой — правка своей записи и есть захват чужого
|
|
// имени.
|
|
func TestUsersCollectionSurfaceIsClosed(t *testing.T) {
|
|
app := newTestStorage(t)
|
|
|
|
users, err := app.FindCollectionByNameOrId(migrations.UsersCollection)
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, users.ListRule, "перечисление учётных записей закрыто")
|
|
assert.Nil(t, users.ViewRule, "чтение учётной записи закрыто")
|
|
assert.Nil(t, users.CreateRule, "заведение учётной записи снаружи закрыто")
|
|
assert.Nil(t, users.UpdateRule, "правка учётной записи снаружи закрыта")
|
|
assert.Nil(t, users.DeleteRule, "удаление учётной записи снаружи закрыто")
|
|
|
|
// Собственные способы войти выключены там же: без этого узнавание по
|
|
// заголовку обходится двумя запросами — завести себе запись и войти паролем.
|
|
assert.False(t, users.PasswordAuth.Enabled, "вход по паролю выключен")
|
|
assert.False(t, users.OTP.Enabled, "вход по одноразовому коду выключен")
|
|
assert.False(t, users.OAuth2.Enabled, "обмен кода у внешнего провайдера выключен")
|
|
assert.Empty(t, users.OAuth2.Providers, "настроенных провайдеров не осталось")
|
|
}
|
|
|
|
func TestFormerJobsCollectionIsGone(t *testing.T) {
|
|
app := newTestStorage(t)
|
|
|
|
_, err := app.FindCollectionByNameOrId(migrations.JobsCollection)
|
|
assert.Error(t, err, "прежней коллекции задач не осталось")
|
|
}
|
|
|
|
// Пара «запись и вид» уникальна: повтор прерванного шага не заводит второго
|
|
// комплекта строк, и вопрос «какой текст отдавать человеку» не становится
|
|
// вопросом порядка записи.
|
|
func TestAppendicesAreUniquePerRecord(t *testing.T) {
|
|
app := newTestStorage(t)
|
|
|
|
indexes := map[string][]string{
|
|
migrations.TextsCollection: {"idx_texts_record_kind"},
|
|
migrations.StructuresCollection: {"idx_structures_record_version"},
|
|
migrations.TopicsCollection: {"idx_topics_owner_name"},
|
|
}
|
|
|
|
for name, expected := range indexes {
|
|
collection, err := app.FindCollectionByNameOrId(name)
|
|
require.NoError(t, err)
|
|
|
|
for _, index := range expected {
|
|
var found bool
|
|
for _, declared := range collection.Indexes {
|
|
if strings.Contains(declared, index) && strings.Contains(declared, "UNIQUE") {
|
|
found = true
|
|
}
|
|
}
|
|
assert.Truef(t, found, "у %s есть уникальный индекс %s", name, index)
|
|
}
|
|
}
|
|
}
|