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: "a, 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) } } }