- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
150 lines
6.6 KiB
Go
150 lines
6.6 KiB
Go
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,
|
|
"строка журнала растёт вместе с длиной запрошенного пути")
|
|
}
|