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) } }