package dao import ( "path/filepath" "strings" "testing" "hy2xs-admin/credential" "hy2xs-admin/util" ) // Bootstrap-путь администратора обязан подчиняться ОБЩЕМУ контракту пароля. // // Здесь было два расхождения, и оба ломали установку молча. // // Первое: `strings.TrimSpace(os.Getenv("HY2XS_ADMIN_INITIAL_PASSWORD"))`. Продукт // объявляет пробел по краям частью пароля и не снимает его ни на форме входа, // ни при смене, ни в оркестраторе — а здесь снимал. Учётная запись создавалась // с паролем, отличным от того, который оператор записал в hy2xs.env и которым // он потом пытался войти. // // Второе: контракт не проверялся вовсе, поэтому непригодный для bcrypt пароль // (64 кириллические буквы = 128 байт) доходил до GenerateFromPassword, и старт // службы падал с «bcrypt: password length exceeds 72 bytes» — сообщением, по // которому нельзя понять ни что чинить, ни где. func startAdminBootstrap(t *testing.T, password string) error { t.Helper() t.Setenv("HY2XS_ADMIN_USER", "hy2xsadmin") t.Setenv("HY2XS_ADMIN_INITIAL_PASSWORD", password) t.Setenv("HY2XS_ADMIN_CON_PASS", testBootstrapSecret) t.Setenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", testTrafficStatsToken) err := InitSqlAt(filepath.Join(t.TempDir(), "hy2xs-admin-test.db")) t.Cleanup(func() { _ = CloseSqliteDB() }) return err } // Пароль с краевым пробелом создаёт учётную запись С ЭТИМ пробелом. func TestBootstrapAdminKeepsSurroundingWhitespaceInPassword(t *testing.T) { const password = "bootstrap-admin-password " if err := startAdminBootstrap(t, password); err != nil { t.Fatalf("запуск сервиса не удался: %v", err) } admin, err := GetAdminUser("username = ?", "hy2xsadmin") if err != nil { t.Fatalf("учётная запись администратора не создана: %v", err) } if admin.PasswordHash == nil { t.Fatal("у созданной учётной записи нет хеша пароля") } if !util.VerifyPassword(password, *admin.PasswordHash) { t.Error("вход исходным значением невозможен: пароль обрезан при создании учётной записи") } if util.VerifyPassword(strings.TrimSpace(password), *admin.PasswordHash) { t.Error("вход обрезанным значением возможен: пробел по краям снят вопреки контракту") } } // Пароль, не проходящий контракт, роняет старт с ПРИЧИНОЙ, а не с ошибкой // bcrypt, и учётной записи после этого не остаётся. func TestBootstrapAdminRefusesPasswordOutsideTheContract(t *testing.T) { // 64 символа — в границе; 128 байт — за пределом bcrypt. password := strings.Repeat("я", credential.AdminPasswordMaxLength) if credential.IsValidAdminPassword(password) { t.Fatal("подготовка: контракт принимает значение, проверяется не то") } err := startAdminBootstrap(t, password) if err == nil { t.Fatal("старт с непригодным паролем завершился успешно") } if !strings.Contains(err.Error(), "HY2XS_ADMIN_INITIAL_PASSWORD") { t.Errorf("отказ не называет переменную, которую надо чинить: %v", err) } if !strings.Contains(err.Error(), "hy2xs.env") { t.Errorf("отказ не называет файл, в котором чинится причина: %v", err) } } // Пароль ровно в предел bcrypt (72 байта) — законное значение, и установка с // ним обязана проходить целиком. func TestBootstrapAdminAcceptsPasswordAtBcryptByteLimit(t *testing.T) { password := strings.Repeat("я", 36) if len(password) != credential.AdminPasswordMaxBytes { t.Fatalf("подготовка: %d байт вместо %d", len(password), credential.AdminPasswordMaxBytes) } if err := startAdminBootstrap(t, password); err != nil { t.Fatalf("установка с паролем в 72 байта не удалась: %v", err) } admin, err := GetAdminUser("username = ?", "hy2xsadmin") if err != nil { t.Fatalf("учётная запись администратора не создана: %v", err) } if !util.VerifyPassword(password, *admin.PasswordHash) { t.Error("пароль в 72 байта не проходит проверку собственным хешем") } }