package dao import ( "path/filepath" "strings" "testing" "hy2xs-admin/model/constant" "hy2xs-admin/model/entity" ) const ( testAdminPassword = "bootstrap-admin-password" testBootstrapSecret = "bootstrap-peer-secret-value" testTrafficStatsToken = "traffic-stats-machine-token" ) // startService воспроизводит запуск сервиса на указанной базе. // // Именно последовательность целиком, а не отдельная функция: дефект, ради // которого написаны эти тесты, проявлялся только на ВТОРОМ запуске. func startService(t *testing.T, dbPath string) error { t.Helper() t.Setenv("HY2XS_ADMIN_INITIAL_PASSWORD", testAdminPassword) t.Setenv("HY2XS_ADMIN_CON_PASS", testBootstrapSecret) t.Setenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", testTrafficStatsToken) err := InitSqlAt(dbPath) t.Cleanup(func() { _ = CloseSqliteDB() }) return err } func newInstalledService(t *testing.T) string { t.Helper() dbPath := filepath.Join(t.TempDir(), "hy2xs-admin-test.db") if err := startService(t, dbPath); err != nil { t.Fatalf("первый запуск сервиса не удался: %v", err) } return dbPath } func testPeer(name string, authID string, digest string) entity.Peer { remark := "" cipher := "" quota := int64(-1) zero := int64(0) maxDevices := int64(3) return entity.Peer{ Name: &name, Remark: &remark, AuthId: &authID, SecretDigest: &digest, SecretEncrypted: &cipher, QuotaBytes: "a, DownloadBytes: &zero, UploadBytes: &zero, ExpiresAt: &zero, MaxDevices: &maxDevices, Disabled: &zero, BannedUntil: &zero, LastConnectionAt: &zero, } } func bootstrapPeerExists(t *testing.T) bool { t.Helper() _, err := GetPeer("name = ?", BootstrapPeerName) if err == nil { return true } if !IsNotFound(err) { t.Fatalf("неожиданная ошибка при чтении пира установщика: %v", err) } return false } // Установка создаёт пира установщика и отмечает это в базе. func TestBootstrapPeerIsSeededOnFirstStart(t *testing.T) { newInstalledService(t) if !bootstrapPeerExists(t) { t.Fatal("пир установщика не создан при первом запуске") } seeded, err := bootstrapPeerSeeded() if err != nil { t.Fatalf("чтение отметки: %v", err) } if !seeded { t.Fatal("отметка о создании пира установщика не выставлена") } } // Обычный перезапуск сервиса ничего не меняет: пир на месте, дублей нет. func TestBootstrapPeerSurvivesRestartUntouched(t *testing.T) { dbPath := newInstalledService(t) before, err := GetPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("пир установщика не найден после установки: %v", err) } _ = CloseSqliteDB() if err := startService(t, dbPath); err != nil { t.Fatalf("перезапуск сервиса не удался: %v", err) } after, err := GetPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("пир установщика пропал после перезапуска: %v", err) } if *after.Id != *before.Id { t.Fatalf("пир установщика пересоздан: id %d -> %d", *before.Id, *after.Id) } peers, err := ListPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("список пиров: %v", err) } if len(peers) != 1 { t.Fatalf("после перезапуска пиров установщика %d, ожидался ровно один", len(peers)) } } // Главный инвариант: удаление пира установщика — необратимый отзыв доступа. // // Раньше HY2XS_ADMIN_CON_PASS оставался в /etc/hy2xs/hy2xs.env, и признаком // «создавать или нет» служило наличие строки в таблице. Поэтому // `systemctl restart hy2xs-admin` после удаления возвращал того же пира с тем // же секретом, молча, и отозванные учётные данные снова становились // действующими. func TestDeletedBootstrapPeerIsNotRecreatedOnRestart(t *testing.T) { dbPath := newInstalledService(t) peer, err := GetPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("пир установщика не найден после установки: %v", err) } if err := DeletePeer([]int64{*peer.Id}); err != nil { t.Fatalf("удаление пира установщика отклонено: %v", err) } _ = CloseSqliteDB() // Переменная окружения на месте — ровно то состояние, в котором сервис и // живёт после установки. if err := startService(t, dbPath); err != nil { t.Fatalf("перезапуск сервиса после отзыва не удался: %v", err) } if bootstrapPeerExists(t) { t.Fatal("пир установщика воскрес после перезапуска: отзыв доступа не переживает restart") } } // Отзыв обязан пережить не один перезапуск, а любое их число. func TestDeletedBootstrapPeerStaysGoneAcrossRepeatedRestarts(t *testing.T) { dbPath := newInstalledService(t) peer, err := GetPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("пир установщика не найден: %v", err) } if err := DeletePeer([]int64{*peer.Id}); err != nil { t.Fatalf("удаление: %v", err) } _ = CloseSqliteDB() for i := 0; i < 3; i++ { if err := startService(t, dbPath); err != nil { t.Fatalf("перезапуск #%d не удался: %v", i+1, err) } if bootstrapPeerExists(t) { t.Fatalf("пир установщика воскрес на перезапуске #%d", i+1) } _ = CloseSqliteDB() } } // Отключённый пир остаётся в базе и не переподписывается сам собой. func TestDisabledBootstrapPeerKeepsItsStateAcrossRestart(t *testing.T) { dbPath := newInstalledService(t) peer, err := GetPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("пир установщика не найден: %v", err) } if err := UpdatePeer([]int64{*peer.Id}, map[string]interface{}{"disabled": 1}); err != nil { t.Fatalf("отключение пира установщика: %v", err) } _ = CloseSqliteDB() if err := startService(t, dbPath); err != nil { t.Fatalf("перезапуск: %v", err) } after, err := GetPeer("name = ?", BootstrapPeerName) if err != nil { t.Fatalf("пир установщика пропал: %v", err) } if after.Disabled == nil || *after.Disabled != 1 { t.Fatal("перезапуск снял отключение с пира установщика") } if *after.SecretDigest != *peer.SecretDigest { t.Fatal("перезапуск переподписал пира установщика") } } // Отсутствие HY2XS_ADMIN_CON_PASS на чистой базе — отказ старта, а не повод // придумать секрет: он продублирован в /etc/hy2xs/bootstrap-admin.secret. func TestFirstStartWithoutBootstrapSecretFails(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "hy2xs-admin-test.db") t.Setenv("HY2XS_ADMIN_INITIAL_PASSWORD", testAdminPassword) t.Setenv("HY2XS_ADMIN_CON_PASS", "") t.Setenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", testTrafficStatsToken) err := InitSqlAt(dbPath) t.Cleanup(func() { _ = CloseSqliteDB() }) if err == nil { t.Fatal("запуск без HY2XS_ADMIN_CON_PASS не отклонён") } if !strings.Contains(err.Error(), "HY2XS_ADMIN_CON_PASS") { t.Errorf("отказ не называет переменную: %v", err) } } // Перезапуск уже установленного сервиса без переменной обязан работать: пир // создан однажды, и требовать секрет второй раз незачем. func TestRestartWithoutBootstrapSecretSucceeds(t *testing.T) { dbPath := newInstalledService(t) _ = CloseSqliteDB() t.Setenv("HY2XS_ADMIN_INITIAL_PASSWORD", testAdminPassword) t.Setenv("HY2XS_ADMIN_CON_PASS", "") t.Setenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", testTrafficStatsToken) if err := InitSqlAt(dbPath); err != nil { t.Fatalf("перезапуск установленного сервиса без HY2XS_ADMIN_CON_PASS отклонён: %v", err) } t.Cleanup(func() { _ = CloseSqliteDB() }) if !bootstrapPeerExists(t) { t.Fatal("пир установщика пропал при перезапуске без переменной") } } // Отметка и сам пир пишутся неделимо: иначе падение между двумя операциями // вернуло бы прежнее поведение на следующем старте. func TestBootstrapSeedIsAtomic(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "hy2xs-admin-test.db") if err := InitSqliteDBAt(dbPath); err != nil { t.Fatalf("открытие базы: %v", err) } t.Cleanup(func() { _ = CloseSqliteDB() }) if err := RunMigrations(); err != nil { t.Fatalf("миграции: %v", err) } // Занимаем имя обычной записью: следующая вставка упрётся в UNIQUE(name) // уже ВНУТРИ транзакции. Отметка не имеет права остаться выставленной. if _, err := SavePeer(testPeer(BootstrapPeerName, "some-auth-id", "some-digest")); err != nil { t.Fatalf("подготовка конфликтующей записи: %v", err) } if err := seedBootstrapPeerOnce(testPeer(BootstrapPeerName, BootstrapPeerName, "bootstrap-digest")); err == nil { t.Fatal("вставка с конфликтом UNIQUE(name) не отклонена") } seeded, err := bootstrapPeerSeeded() if err != nil { t.Fatalf("чтение отметки: %v", err) } if seeded { t.Fatal("отметка выставлена, хотя пир не записан: транзакции нет") } } // Токен trafficStats — межкомпонентный контракт: при пустом env и пустой базе // админка обязана отказать, а не сгенерировать значение, которого не знает // Hysteria. func TestTrafficStatsSecretIsNotInvented(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "hy2xs-admin-test.db") t.Setenv("HY2XS_ADMIN_INITIAL_PASSWORD", testAdminPassword) t.Setenv("HY2XS_ADMIN_CON_PASS", testBootstrapSecret) t.Setenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", "") err := InitSqlAt(dbPath) t.Cleanup(func() { _ = CloseSqliteDB() }) if err == nil { t.Fatal("запуск без machine token не отклонён: админка придумала его сама") } if !strings.Contains(err.Error(), "HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET") { t.Errorf("отказ не называет переменную: %v", err) } cfg, cfgErr := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if cfgErr == nil && cfg.Value != nil && strings.TrimSpace(*cfg.Value) != "" { t.Fatal("в базе появился сгенерированный machine token") } } // Уже согласованный токен в базе позволяет запуститься без переменной: это не // изобретение контракта, а использование существующего. func TestTrafficStatsSecretFromDatabaseIsAccepted(t *testing.T) { dbPath := newInstalledService(t) _ = CloseSqliteDB() t.Setenv("HY2XS_ADMIN_INITIAL_PASSWORD", testAdminPassword) t.Setenv("HY2XS_ADMIN_CON_PASS", testBootstrapSecret) t.Setenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", "") if err := InitSqlAt(dbPath); err != nil { t.Fatalf("перезапуск с токеном в базе отклонён: %v", err) } t.Cleanup(func() { _ = CloseSqliteDB() }) cfg, err := GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if err != nil { t.Fatalf("токен пропал из базы: %v", err) } if strings.TrimSpace(*cfg.Value) != testTrafficStatsToken { t.Fatalf("токен подменён: %q", *cfg.Value) } }