package service import ( "strings" "testing" "hy2xs-admin/model/bo" ) func validPeer(name string) bo.PeerExport { return bo.PeerExport{ Name: name, Remark: "office laptop", QuotaBytes: -1, ExpiresAt: 0, MaxDevices: 3, Disabled: 0, } } func TestValidatePeerImportBatchAcceptsExportShape(t *testing.T) { items := []bo.PeerExport{ validPeer("alpha1"), { Name: "bravo-2", AuthId: "Kf83jd0sLa93kd0sQx", Secret: "s3cret-value", QuotaBytes: 107374182400, DownloadBytes: 12, UploadBytes: 34, ExpiresAt: 1893456000000, MaxDevices: 5, Disabled: 1, BannedUntil: 0, LastConnectionAt: 1893456000000, }, } if err := ValidatePeerImportBatch(items); err != nil { t.Fatalf("валидная выгрузка отклонена: %v", err) } } func TestValidatePeerImportBatchRejectsEmpty(t *testing.T) { if err := ValidatePeerImportBatch(nil); err == nil { t.Fatal("пустой импорт должен быть отклонён") } } // Регрессия: импорт шёл мимо dto.PeerSaveDto, поэтому в базу попадало имя // любой длины и с любыми символами — включая перевод строки, который потом // оказался бы в клиентской ссылке. func TestValidatePeerImportBatchRejectsBadNames(t *testing.T) { cases := map[string]string{ "пустое": "", "только пробелы": " ", "короткое": "abc", "слишком длинное": strings.Repeat("a", 33), "перевод строки": "peer\nname", "пробел внутри": "peer name", "недопустимый слэш": "peer/name", } for label, name := range cases { item := validPeer(name) if err := ValidatePeerImportBatch([]bo.PeerExport{item}); err == nil { t.Errorf("%s: имя %q принято, ожидался отказ", label, name) } } } // Пир установщика продублирован в /etc/hy2xs/bootstrap-admin.secret: // подмена его секрета импортом рассинхронизировала бы файл с базой. func TestValidatePeerImportBatchRejectsReservedBootstrapPeer(t *testing.T) { item := validPeer(ReservedBootstrapPeerName) err := ValidatePeerImportBatch([]bo.PeerExport{item}) if err == nil { t.Fatal("зарезервированное имя bootstrap-пира принято") } if !strings.Contains(err.Error(), ReservedBootstrapPeerName) { t.Fatalf("сообщение не называет зарезервированное имя: %v", err) } byAuthID := validPeer("alpha1") byAuthID.AuthId = ReservedBootstrapPeerName if err := ValidatePeerImportBatch([]bo.PeerExport{byAuthID}); err == nil { t.Fatal("зарезервированный authId принят") } } func TestValidatePeerImportBatchRejectsOutOfRangeNumbers(t *testing.T) { cases := map[string]func(*bo.PeerExport){ "quota < -1": func(p *bo.PeerExport) { p.QuotaBytes = -2 }, "expiresAt < 0": func(p *bo.PeerExport) { p.ExpiresAt = -1 }, "maxDevices < 0": func(p *bo.PeerExport) { p.MaxDevices = -1 }, "disabled = 7": func(p *bo.PeerExport) { p.Disabled = 7 }, "bannedUntil < 0": func(p *bo.PeerExport) { p.BannedUntil = -1 }, "download < 0": func(p *bo.PeerExport) { p.DownloadBytes = -1 }, "upload < 0": func(p *bo.PeerExport) { p.UploadBytes = -1 }, "lastConnection < 0": func(p *bo.PeerExport) { p.LastConnectionAt = -1 }, "remark длиннее 64": func(p *bo.PeerExport) { p.Remark = strings.Repeat("я", 65) }, "secret короче 6": func(p *bo.PeerExport) { p.Secret = "abc" }, "secret длиннее 128": func(p *bo.PeerExport) { p.Secret = strings.Repeat("s", 129) }, "authId с пробелом": func(p *bo.PeerExport) { p.AuthId = "bad id" }, "authId длиннее 64": func(p *bo.PeerExport) { p.AuthId = strings.Repeat("a", 65) }, } for label, mutate := range cases { item := validPeer("alpha1") mutate(&item) if err := ValidatePeerImportBatch([]bo.PeerExport{item}); err == nil { t.Errorf("%s: запись принята, ожидался отказ", label) } } } // -1 (безлимит) и 0 (не задано) — легальные значения, а не пограничный мусор. func TestValidatePeerImportBatchAcceptsSentinelValues(t *testing.T) { item := validPeer("alpha1") item.QuotaBytes = -1 item.MaxDevices = 0 item.ExpiresAt = 0 if err := ValidatePeerImportBatch([]bo.PeerExport{item}); err != nil { t.Fatalf("допустимые sentinel-значения отклонены: %v", err) } } func TestValidatePeerImportBatchRejectsDuplicates(t *testing.T) { dupName := []bo.PeerExport{validPeer("alpha1"), validPeer("alpha1")} if err := ValidatePeerImportBatch(dupName); err == nil { t.Error("дублирующееся имя принято") } first := validPeer("alpha1") first.AuthId = "same-auth-id" second := validPeer("bravo1") second.AuthId = "same-auth-id" if err := ValidatePeerImportBatch([]bo.PeerExport{first, second}); err == nil { t.Error("дублирующийся authId принят") } } func TestValidatePeerImportBatchRejectsOversizedBatch(t *testing.T) { items := make([]bo.PeerExport, MaxPeerImportItems+1) for i := range items { items[i] = validPeer("alpha1") } if err := ValidatePeerImportBatch(items); err == nil { t.Fatal("партия больше лимита принята") } } // Валидация обязана быть полной до первой записи в базу: файл, у которого // невалидна последняя запись, не должен применить первые. func TestValidatePeerImportBatchReportsLastInvalidRecord(t *testing.T) { items := []bo.PeerExport{validPeer("alpha1"), validPeer("bravo1"), validPeer("x")} err := ValidatePeerImportBatch(items) if err == nil { t.Fatal("невалидная последняя запись пропущена") } if !strings.Contains(err.Error(), "#3") { t.Fatalf("сообщение не указывает номер записи: %v", err) } }