- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
225 lines
10 KiB
Go
225 lines
10 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
httpcontroller "git.vakhrushev.me/av/transcriber/internal/controller/http"
|
|
)
|
|
|
|
// Проверки этого файла судят требование «Настройка, открывающая вход всем,
|
|
// роняет старт». Предмет у них один: сервис, поднявшийся на настройках, при
|
|
// которых отладочный вход становится открытым входом либо не работает вовсе.
|
|
//
|
|
// Имена заголовков берутся у транспорта, а не выписываются здесь: дом у них
|
|
// один, и проверка со своим списком зеленела бы на разошедшемся коде.
|
|
|
|
func acceptedHeaderNames() []string { return httpcontroller.IdentityHeaderNames() }
|
|
|
|
// validateTestHeaders зовёт проверку так же, как её зовёт точка входа.
|
|
func validateTestHeaders(cfg *Config) error {
|
|
return cfg.ValidateTestHeaders(acceptedHeaderNames(), httpcontroller.LoginHeader)
|
|
}
|
|
|
|
// debugConfig собирает настройки отладочного запуска: предохранитель и имитация.
|
|
func debugConfig(debug bool, headers map[string]string) *Config {
|
|
cfg := defaultConfig()
|
|
cfg.Server.Debug = debug
|
|
cfg.Auth.TrustedProxies = []string{"127.0.0.1"}
|
|
cfg.Auth.TestHeaders = headers
|
|
return cfg
|
|
}
|
|
|
|
// Первый отказ: имитация заполнена, предохранителя нет. Состояние не читается
|
|
// никак — либо человек забыл включить предохранитель, либо забыл убрать
|
|
// имитацию из боевого файла, и до открытого архива остаётся одно слово.
|
|
func TestTestHeadersWithoutDebugFailsStartup(t *testing.T) {
|
|
err := validateTestHeaders(debugConfig(false, map[string]string{
|
|
httpcontroller.LoginHeader: "local",
|
|
}))
|
|
if err == nil {
|
|
t.Fatal("имитация без предохранителя принята: сервис поднялся бы никого не узнающим")
|
|
}
|
|
if !strings.Contains(err.Error(), "[server] debug") {
|
|
t.Fatalf("имя ключа предохранителя не названо, чинить нечего: %v", err)
|
|
}
|
|
}
|
|
|
|
// Пустое значение — заполненная имитация, а не отсутствие её: человек, написавший
|
|
// ключ, имитацию завёл, и пустое значение у него вторая поломка, а не первая.
|
|
func TestEmptyLoginValueCountsAsFilledSection(t *testing.T) {
|
|
err := validateTestHeaders(debugConfig(false, map[string]string{
|
|
httpcontroller.LoginHeader: "",
|
|
}))
|
|
if err == nil {
|
|
t.Fatal("секция с пустым значением сочтена пустой")
|
|
}
|
|
if !strings.Contains(err.Error(), "[server] debug") {
|
|
t.Fatalf("имя ключа предохранителя не названо: %v", err)
|
|
}
|
|
}
|
|
|
|
// Второй отказ: имя, которого сервис не читает. Опечатка иначе кончается
|
|
// сервисом, который никого не узнаёт, без единого следа.
|
|
func TestUnknownHeaderNameFailsStartup(t *testing.T) {
|
|
err := validateTestHeaders(debugConfig(true, map[string]string{
|
|
httpcontroller.LoginHeader: "local",
|
|
"Remote-Usr": "local",
|
|
}))
|
|
if err == nil {
|
|
t.Fatal("неизвестное имя заголовка принято")
|
|
}
|
|
message := err.Error()
|
|
if !strings.Contains(message, "Remote-Usr") {
|
|
t.Fatalf("неизвестное имя не названо: %v", err)
|
|
}
|
|
for _, name := range acceptedHeaderNames() {
|
|
if !strings.Contains(message, name) {
|
|
t.Fatalf("принимаемое имя %s не названо, чинить нечего: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Третий отказ, первая его половина: ключа логина нет вовсе. Такая секция
|
|
// подняла бы сервис, который подставит почту, удалит логин и не узнает никого.
|
|
func TestSectionWithoutLoginKeyFailsStartup(t *testing.T) {
|
|
err := validateTestHeaders(debugConfig(true, map[string]string{
|
|
httpcontroller.EmailHeader: "local@example.com",
|
|
}))
|
|
if err == nil {
|
|
t.Fatal("имитация без ключа логина принята")
|
|
}
|
|
if !strings.Contains(err.Error(), httpcontroller.LoginHeader) {
|
|
t.Fatalf("имя недостающего ключа не названо: %v", err)
|
|
}
|
|
}
|
|
|
|
// Третий отказ, вторая половина: логин судится **тем же** приёмом, каким
|
|
// узнавание судит пришедшее значение. Иначе сервис поднимается, ставит
|
|
// заголовок, получает отказ приёма и отвечает неузнанным на всё.
|
|
func TestUnacceptableLoginValueFailsStartup(t *testing.T) {
|
|
cases := map[string]string{
|
|
"пустое": "",
|
|
"пробельное": " ",
|
|
"сверх предела": strings.Repeat("x", 300),
|
|
"с управляющим знаком": "loc\x00al",
|
|
}
|
|
|
|
for name, login := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
err := validateTestHeaders(debugConfig(true, map[string]string{
|
|
httpcontroller.LoginHeader: login,
|
|
}))
|
|
if err == nil {
|
|
t.Fatal("негодное значение логина принято")
|
|
}
|
|
message := err.Error()
|
|
if !strings.Contains(message, httpcontroller.LoginHeader) {
|
|
t.Fatalf("имя ключа не названо, чинить нечего: %v", err)
|
|
}
|
|
// Значение в отказ не идёт: логин — ключ к чужому архиву.
|
|
if login != "" && strings.Contains(message, login) {
|
|
t.Fatalf("значение логина уехало в отказ старта: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Два ключа, различающиеся регистром, назвали бы один заголовок: имя заголовка
|
|
// нечувствительно к регистру, а ключ TOML чувствителен, и одно из значений
|
|
// потерялось бы молча.
|
|
func TestDuplicateHeaderKeyFailsStartup(t *testing.T) {
|
|
err := validateTestHeaders(debugConfig(true, map[string]string{
|
|
"Remote-User": "one",
|
|
"remote-user": "two",
|
|
}))
|
|
if err == nil {
|
|
t.Fatal("два ключа на один заголовок приняты: одно значение потерялось бы молча")
|
|
}
|
|
}
|
|
|
|
// Законный случай: предохранитель включён, имитация пуста. Сам по себе признак
|
|
// ничего не включает, и перечень доверенных адресов ему не судья.
|
|
func TestDebugWithoutTestHeadersStarts(t *testing.T) {
|
|
cfg := debugConfig(true, nil)
|
|
cfg.Auth.TrustedProxies = []string{"172.20.0.0/24"}
|
|
|
|
if err := validateTestHeaders(cfg); err != nil {
|
|
t.Fatalf("включённый предохранитель при пустой имитации уронил старт: %v", err)
|
|
}
|
|
if len(cfg.HeaderSubstitution()) > 0 {
|
|
t.Fatal("пустая имитация включила подстановку")
|
|
}
|
|
}
|
|
|
|
// Законный случай главный: конфиг сегодняшнего дня, не называющий ни одного
|
|
// нового ключа, ведёт себя ровно как вёл.
|
|
func TestConfigWithoutNewKeysStartsUnchanged(t *testing.T) {
|
|
path := writeConfig(t, "[auth]\ntrusted_proxies = [\"172.20.0.0/24\"]\n"+validConfigBody)
|
|
|
|
cfg, err := LoadConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("конфиг без новых ключей не прочитан: %v", err)
|
|
}
|
|
if cfg.Server.Debug {
|
|
t.Fatal("отсутствие ключа предохранителя прочитано как «включено»")
|
|
}
|
|
if len(cfg.Auth.TestHeaders) != 0 {
|
|
t.Fatalf("отсутствие секции имитации прочитано как заполненная: %v", cfg.Auth.TestHeaders)
|
|
}
|
|
if err := validateTestHeaders(cfg); err != nil {
|
|
t.Fatalf("конфиг без новых ключей уронил старт: %v", err)
|
|
}
|
|
if cfg.HeaderSubstitution() != nil {
|
|
t.Fatal("конфиг без новых ключей включил подстановку")
|
|
}
|
|
}
|
|
|
|
// Негодное значение предохранителя роняет старт разбором, а не читается как
|
|
// «включено»: ошибка разбора не вправе открывать вход.
|
|
func TestMalformedDebugValueFailsLoad(t *testing.T) {
|
|
path := writeConfig(t, "[server]\ndebug = \"yes\"\n"+validConfigBody)
|
|
|
|
if _, err := LoadConfig(path); err == nil {
|
|
t.Fatal("строка вместо булева значения принята")
|
|
}
|
|
}
|
|
|
|
// Ключи имитации приезжают из файла, а на выходе приведены к каноническому виду
|
|
// имени заголовка: в HTTP имя нечувствительно к регистру, а ключ TOML — нет.
|
|
func TestHeaderSubstitutionReadsSectionAndCanonicalizes(t *testing.T) {
|
|
path := writeConfig(t, `
|
|
[server]
|
|
debug = true
|
|
|
|
[auth]
|
|
trusted_proxies = ["127.0.0.1"]
|
|
|
|
[auth.test_headers]
|
|
remote-user = "local"
|
|
REMOTE-EMAIL = "local@example.com"
|
|
`+validConfigBody)
|
|
|
|
cfg, err := LoadConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("конфиг с имитацией не прочитан: %v", err)
|
|
}
|
|
if err := validateTestHeaders(cfg); err != nil {
|
|
t.Fatalf("годная имитация уронила старт: %v", err)
|
|
}
|
|
|
|
headers := cfg.HeaderSubstitution()
|
|
if len(headers) == 0 {
|
|
t.Fatal("заполненная имитация при включённом предохранителе не включила подстановку")
|
|
}
|
|
if headers[httpcontroller.LoginHeader] != "local" {
|
|
t.Fatalf("логин не приведён к каноническому имени заголовка: %v", headers)
|
|
}
|
|
if headers[httpcontroller.EmailHeader] != "local@example.com" {
|
|
t.Fatalf("адрес почты не приведён к каноническому имени заголовка: %v", headers)
|
|
}
|
|
if _, named := headers[httpcontroller.NameHeader]; named {
|
|
t.Fatalf("не названный секцией заголовок появился в перечне: %v", headers)
|
|
}
|
|
}
|