package controller import ( "strings" "testing" "hy2xs-admin/service" ) // Набор символов логина администратора закреплён ФАКТИЧЕСКИМ множеством. // // Прежняя запись класса `[a-zA-Z0-9!@#$%^&*()_+-=]` содержала неэкранированный // дефис, из-за чего `+-=` образовывал диапазон и впускал `, - . / 0-9 : ; < =`. // Действующая запись перечисляет эти символы явно и НЕ сужает множество: имя // администратора приходит из HY2XS_ADMIN_USER в hy2xs.env, и сужение правила // означало бы, что установка с логином вроде `admin.ops` перестаёт пускать // оператора в панель. // // Тест существует, чтобы это решение было явным: попытка «навести порядок» в // классе символов уронит его, а не вход администратора на живом сервере. // // Спрашивается service.IsValidAdminUsername, а не регексп рядом: правило // объявлено там один раз, и проверка обязана спрашивать ровно то, что работает // в бою. Прежняя версия обращалась к копии внутри слоя контроллеров — то есть // не увидела бы расхождения между копией и правилом. func TestAdminUsernameCharsetIsUnchanged(t *testing.T) { const historical = "abcXYZ019" + "!@#$%^&*()_" + "+,-./:;<=" for _, symbol := range strings.Split(historical, "") { candidate := "admin" + symbol if !service.IsValidAdminUsername(candidate) { t.Errorf("символ %q больше не принимается логином: сужение набора ломает вход существующей установки", symbol) } } for _, rejected := range []string{ "", // не задано "admi", // короче шести символов strings.Repeat("a", 33), // длиннее тридцати двух "admin пробел", // пробел "админ1", // кириллица "admin\n1", // перевод строки "admin'1", // апостроф вне набора } { if service.IsValidAdminUsername(rejected) { t.Errorf("значение %q принято логином, ожидался отказ", rejected) } } } // Границы длины логина принадлежат ОДНОМУ правилу, и человекочитаемый набор // обязан описывать тот же класс, что и регексп. // // Расхождение здесь тихо превращает сообщение об отказе в дезинформацию: // оператор читает набор, которого правило не принимает. func TestAdminUsernameBoundsAgreeWithRule(t *testing.T) { if service.IsValidAdminUsername(strings.Repeat("a", service.AdminUsernameMinLength-1)) { t.Errorf("логин длиной %d принят при минимуме %d", service.AdminUsernameMinLength-1, service.AdminUsernameMinLength) } if !service.IsValidAdminUsername(strings.Repeat("a", service.AdminUsernameMinLength)) { t.Errorf("логин длиной ровно %d отклонён", service.AdminUsernameMinLength) } if !service.IsValidAdminUsername(strings.Repeat("a", service.AdminUsernameMaxLength)) { t.Errorf("логин длиной ровно %d отклонён", service.AdminUsernameMaxLength) } if service.IsValidAdminUsername(strings.Repeat("a", service.AdminUsernameMaxLength+1)) { t.Errorf("логин длиной %d принят при максимуме %d", service.AdminUsernameMaxLength+1, service.AdminUsernameMaxLength) } // Человекочитаемый набор перечисляет ровно те же знаки препинания, что и // класс регекспа. Пробелы в нём — разделители групп, а не допустимый символ. punctuation := strings.Fields(service.AdminUsernameCharset) for _, symbol := range strings.Split(punctuation[len(punctuation)-1], "") { if !service.IsValidAdminUsername("admin" + symbol) { t.Errorf("набор из сообщения обещает символ %q, но правило его не принимает", symbol) } } } // Длина пароля считается в РУНАХ — ровно так её считает go-playground/validator // в правилах `min`/`max` на строке. // // Расхождение означало бы, что пароль с кириллицей проходит проверку на одной // двери и отказывается на другой: 32 кириллические буквы — это 64 БАЙТА. func TestAdminPasswordLengthIsCountedInRunes(t *testing.T) { if !service.IsValidAdminPassword(strings.Repeat("я", service.AdminPasswordMaxLength)) { t.Errorf("пароль из %d кириллических букв отклонён: длина считается в байтах", service.AdminPasswordMaxLength) } if service.IsValidAdminPassword(strings.Repeat("я", service.AdminPasswordMaxLength+1)) { t.Error("пароль длиннее максимума принят") } if service.IsValidAdminPassword(strings.Repeat("a", service.AdminPasswordMinLength-1)) { t.Error("пароль короче минимума принят") } if !service.IsValidAdminPassword(strings.Repeat("a", service.AdminPasswordMinLength)) { t.Error("пароль длиной ровно в минимум отклонён") } } // Имя пира проверяется ОДНИМ правилом на весь продукт: панель и импорт ведут в // одну таблицу и не имеют права требовать разного. func TestPeerNameRuleIsSharedWithImport(t *testing.T) { accepted := []string{ "client-01", "alpha1", "bootstrap-admin-peer", strings.Repeat("a", 6), strings.Repeat("a", 32), } for _, name := range accepted { if !service.IsValidPeerName(name) { t.Errorf("имя %q отклонено, ожидался приём", name) } } rejected := []string{ "", " ", "pc1", strings.Repeat("a", 33), "peer name", "peer\nname", "peer/name", "peer:name", "peer.name", "пир-01", } for _, name := range rejected { if service.IsValidPeerName(name) { t.Errorf("имя %q принято, ожидался отказ", name) } } }