Files
HY2XS_flamy/apps/service/peer_export_backup_test.go
T
founder cf094f6e6f fix(v1): сделать отзыв доступа, бэкап и диагностику соответствующими своим именам
Проход по операциям, которые делают не то, что обещает их имя.

P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать
пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS
продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор
удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же
секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config:
«создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной
транзакцией.

P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и
отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь
недоступный секрет любого пира отклоняет весь запрос с указанием имени.

P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет,
если оно не начиналось с v1: — остаток поколения с открытыми секретами.

P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на
проблему обрывала все живые соединения.

P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в
/etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine
auth переставал совпадать.

P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести
артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и
документации, отсутствие проверяется приёмкой.

P2. UPDATE по отсутствующей строке config считался успехом, и cron
перепланировался при несохранённом значении. Решение по RowsAffected.

P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения
ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage.

P2. Удалены алиасы /:id/client-url и /:id/qr.

Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go —
языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют
среду с versions.env.
2026-08-30 06:48:50 +05:00

126 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"strings"
"testing"
"hy2xs-admin/dao"
"hy2xs-admin/model/entity"
)
// Резервная копия с учётными данными обязана быть полной или отсутствовать.
//
// Раньше ListExportPeer(true) молча пропускала пиров, чей секрет не удалось
// получить, и отдавала успешный ответ с пустым полем secret. Оператор,
// специально выбравший режим «копия с действующими credentials», узнавал о
// потере только после импорта на новом сервере — по отвалившимся клиентам.
func seedPeerWithCipher(t *testing.T, name string, authID string, cipher string) int64 {
t.Helper()
digest, err := PeerSecretDigest(name + "-secret")
if err != nil {
t.Fatalf("digest: %v", err)
}
remark := ""
quota := int64(-1)
zero := int64(0)
maxDevices := int64(3)
id, err := dao.SavePeer(entity.Peer{
Name: &name,
Remark: &remark,
AuthId: &authID,
SecretDigest: &digest,
SecretEncrypted: &cipher,
QuotaBytes: &quota,
DownloadBytes: &zero,
UploadBytes: &zero,
ExpiresAt: &zero,
MaxDevices: &maxDevices,
Disabled: &zero,
BannedUntil: &zero,
LastConnectionAt: &zero,
})
if err != nil {
t.Fatalf("не удалось создать пира %s: %v", name, err)
}
return id
}
func TestBackupExportReturnsEverySecret(t *testing.T) {
newTestDB(t)
seedPeer(t, "alpha1", "alpha-auth")
seedPeer(t, "bravo2", "bravo-auth")
items, err := ListExportPeer(true)
if err != nil {
t.Fatalf("резервная копия отклонена на исправных данных: %v", err)
}
if len(items) != 2 {
t.Fatalf("ожидалось 2 записи, получено %d", len(items))
}
for _, item := range items {
if item.Secret == "" {
t.Errorf("пир %s уехал в копию без секрета", item.Name)
}
}
}
// Повреждённый шифртекст: расшифровка падает, и весь бэкап обязан упасть
// вместе с ней, назвав пира по имени.
func TestBackupExportFailsWhenAnySecretCannotBeDecrypted(t *testing.T) {
newTestDB(t)
seedPeer(t, "alpha1", "alpha-auth")
seedPeerWithCipher(t, "bravo2", "bravo-auth", "not-a-valid-ciphertext")
seedPeer(t, "charl3", "charlie-auth")
items, err := ListExportPeer(true)
if err == nil {
t.Fatal("резервная копия с нерасшифровываемым секретом не отклонена")
}
if items != nil {
t.Error("при отказе не должно возвращаться частичное содержимое файла")
}
if !strings.Contains(err.Error(), "bravo2") {
t.Errorf("отказ не называет проблемного пира: %v", err)
}
}
// Пир вовсе без сохранённого шифртекста — второй молчаливый канал потери.
func TestBackupExportFailsWhenSecretIsMissing(t *testing.T) {
newTestDB(t)
seedPeer(t, "alpha1", "alpha-auth")
seedPeerWithCipher(t, "bravo2", "bravo-auth", "")
_, err := ListExportPeer(true)
if err == nil {
t.Fatal("резервная копия для пира без секрета не отклонена")
}
if !strings.Contains(err.Error(), "bravo2") {
t.Errorf("отказ не называет проблемного пира: %v", err)
}
}
// Безопасная выгрузка шифртекст не трогает вовсе: пустой secret там — не
// потеря, а весь смысл режима. Повреждённые данные не должны ей мешать.
func TestSafeExportIgnoresBrokenSecrets(t *testing.T) {
newTestDB(t)
seedPeer(t, "alpha1", "alpha-auth")
seedPeerWithCipher(t, "bravo2", "bravo-auth", "not-a-valid-ciphertext")
items, err := ListExportPeer(false)
if err != nil {
t.Fatalf("безопасная выгрузка отклонена: %v", err)
}
if len(items) != 2 {
t.Fatalf("ожидалось 2 записи, получено %d", len(items))
}
for _, item := range items {
if item.Secret != "" {
t.Errorf("безопасная выгрузка содержит секрет пира %s", item.Name)
}
}
}