хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// journalMounts — перечень адресного пространства для проверок журнала.
|
||||
// Обработчиков он здесь не вешает: журналу нужны только границы, а не то, что
|
||||
// стоит за ними.
|
||||
func journalMounts() []Mount {
|
||||
return []Mount{
|
||||
{Path: AppRoot},
|
||||
{Path: HealthPath, Exact: true},
|
||||
{Path: MetricsPath, Exact: true},
|
||||
}
|
||||
}
|
||||
|
||||
// journalRouteOf — маршрут строки журнала для названного пути.
|
||||
func journalRouteOf(path string) string {
|
||||
return JournalRoute(httptest.NewRequest(http.MethodGet, path, http.NoBody), journalMounts())
|
||||
}
|
||||
|
||||
// Путь, которым распоряжается спрашивающий, в журнал дословно не идёт — и
|
||||
// принадлежность сервису тут ничего не меняет: путь под корнем приложения
|
||||
// выбирает тот же аноним, что и путь вне корней.
|
||||
func TestJournalRouteHidesRequestedPath(t *testing.T) {
|
||||
cases := []string{
|
||||
"/",
|
||||
"/records/abc123def456ghi",
|
||||
"/" + strings.Repeat("a", 1024),
|
||||
"/api/files/files/rec0000000000000/0f7b8dd3-d1cc-424c.mp3",
|
||||
"/_/",
|
||||
// Под корнем приложения — то же самое: образца с таким хвостом нет.
|
||||
"/app",
|
||||
"/app/",
|
||||
"/app/" + strings.Repeat("b", 1024),
|
||||
"/app/audiorecords/abc123def456ghi/file/" + strings.Repeat("c", 512),
|
||||
"/app/audiorecords/abc/def/ghi",
|
||||
}
|
||||
|
||||
for _, path := range cases {
|
||||
assert.Equal(t, webappRoute, journalRouteOf(path), path)
|
||||
}
|
||||
}
|
||||
|
||||
// Дословно пишется закрытый перечень: точные адреса наблюдения и образцы
|
||||
// адресов приложения. Идентификатор записи в образец не входит — он приходит
|
||||
// строкой обработчика полем `record_id`.
|
||||
func TestJournalRouteKeepsClosedList(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"/app/audiorecords": AppRouteRecords,
|
||||
"/app/audiorecords/abc123def456ghi": AppRouteRecord,
|
||||
"/app/audiorecords/abc123def456ghi/text": AppRouteRecordText,
|
||||
"/app/audiorecords/abc123def456ghi/file": AppRouteRecordFile,
|
||||
"/app/me": AppRouteMe,
|
||||
"/app/config": AppRouteConfig,
|
||||
HealthPath: HealthPath,
|
||||
MetricsPath: MetricsPath,
|
||||
}
|
||||
|
||||
for path, want := range cases {
|
||||
assert.Equal(t, want, journalRouteOf(path), path)
|
||||
}
|
||||
}
|
||||
|
||||
// Значение маршрута берётся из перечня образцов и ничего сверх него не
|
||||
// возвращает: путь, приведённый маршрутизатором к другому виду, дословно уехать
|
||||
// не может.
|
||||
func TestJournalRouteAnswersOnlyFromClosedList(t *testing.T) {
|
||||
allowed := map[string]bool{webappRoute: true, HealthPath: true, MetricsPath: true}
|
||||
for _, pattern := range AppRoutePatterns {
|
||||
allowed[pattern] = true
|
||||
}
|
||||
|
||||
cases := []string{
|
||||
"/app/me/",
|
||||
"/app/audiorecords/../me",
|
||||
"/app//me",
|
||||
"/app/audiorecords/{id}",
|
||||
"/app/me/" + strings.Repeat("d", 256),
|
||||
}
|
||||
|
||||
for _, path := range cases {
|
||||
route := journalRouteOf(path)
|
||||
assert.True(t, allowed[route], "маршрут %q не принадлежит закрытому перечню (путь %q)", route, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Имя, под которым копия легла в каталог данных, в журнал не идёт: строка
|
||||
// журнала иначе стала бы бессрочным ключом к чужой записи. Сегодня оно не
|
||||
// попадает туда по построению — адрес копии назван идентификатором самой записи,
|
||||
// — и проверка сторожит именно это.
|
||||
func TestJournalCarriesNoStoredFileName(t *testing.T) {
|
||||
env := setupTestEnv(t, readableMetaViewer())
|
||||
|
||||
created := httptest.NewRecorder()
|
||||
env.serve(created, createMultipartRequest(t, "разговор.mp3", []byte("данные")))
|
||||
require.Equal(t, http.StatusCreated, created.Code)
|
||||
|
||||
recordID := intakeItemOf(t, created).ID
|
||||
require.Equal(t, http.StatusOK,
|
||||
env.getOwn("/app/audiorecords/"+recordID+"/file?"+CopyParam+"="+CopyOriginal).Code)
|
||||
|
||||
names := storedFileNames(t, env)
|
||||
require.Len(t, names, 1)
|
||||
|
||||
journal := env.journal.String()
|
||||
assert.NotContains(t, journal, names[0], "имя файла в каталоге данных уехало в журнал")
|
||||
assert.Contains(t, journal, recordID, "идентификатор записи остаётся: по нему прослеживается путь")
|
||||
}
|
||||
|
||||
// Путь, отданный приложению, журнал заменяет исходом и длиной: строка о нём не
|
||||
// растёт вместе с длиной пути.
|
||||
func TestJournalWebappOutcomeInsteadOfPath(t *testing.T) {
|
||||
env := setupTestEnv(t, readableMetaViewer())
|
||||
|
||||
long := "/" + strings.Repeat("щ", 500)
|
||||
require.Equal(t, http.StatusOK, env.get(long).Code)
|
||||
|
||||
journal := env.journal.String()
|
||||
assert.NotContains(t, journal, strings.Repeat("щ", 500), "путь уехал в журнал дословно")
|
||||
assert.Contains(t, journal, "webapp.outcome="+OutcomeMarkup)
|
||||
assert.Contains(t, journal, "http.path_length=")
|
||||
}
|
||||
|
||||
// Неузнанный запрос под корнем приложения журнал тоже не пишет дословно, а
|
||||
// строка о нём не растёт вместе с длиной пути. Прежде путь под корнем считался
|
||||
// принадлежащим сервису и уезжал в строку целиком — аноним писал в журнал
|
||||
// владельца свой текст произвольной длины.
|
||||
func TestJournalHidesAnonymousPathUnderAppRoot(t *testing.T) {
|
||||
env := setupTestEnv(t, readableMetaViewer())
|
||||
|
||||
const filler = 4096
|
||||
long := "/app/" + strings.Repeat("щ", filler)
|
||||
require.Equal(t, http.StatusUnauthorized, env.get(long).Code)
|
||||
|
||||
journal := env.journal.String()
|
||||
assert.NotContains(t, journal, strings.Repeat("щ", filler), "путь уехал в журнал дословно")
|
||||
assert.Contains(t, journal, "http.route="+webappRoute)
|
||||
assert.Contains(t, journal, "http.path_length=")
|
||||
assert.Less(t, len(journal), filler,
|
||||
"строка журнала растёт вместе с длиной запрошенного пути")
|
||||
}
|
||||
Reference in New Issue
Block a user