package dto import "testing" func strPtr(v string) *string { return &v } func i64Ptr(v int64) *int64 { return &v } func intPtr(v int) *int { return &v } // Граница проходит по КАЖДОМУ полю отдельно, и это главное свойство // нормализации. // // Общее правило «пусто → не задано» выглядит соблазнительно и молча ломает // смысл: у комментария пустая строка означает «убрать пометку», у флага // disabled ноль — «включён», у квоты ноль — «нулевая квота». Тест закрепляет, // что эти три случая не попали под общий гребень. func TestPeerSaveNormalizeTreatsBlankSecretAsAbsent(t *testing.T) { for _, blank := range []string{"", " ", "\t", "\n", " \t\n "} { d := PeerSaveDto{Name: strPtr("client-01"), Secret: strPtr(blank)} d.Normalize() if d.Secret != nil { t.Errorf("секрет %q не приведён к «не задано»: %q", blank, *d.Secret) } } } func TestPeerSaveNormalizeKeepsManualSecretTrimmed(t *testing.T) { d := PeerSaveDto{Name: strPtr("client-01"), Secret: strPtr(" s3cret-value ")} d.Normalize() if d.Secret == nil { t.Fatal("заданный секрет потерян") } if *d.Secret != "s3cret-value" { t.Fatalf("секрет не обрезан по краям: %q", *d.Secret) } } func TestPeerSaveNormalizeKeepsBlankRemarkAsValue(t *testing.T) { d := PeerSaveDto{Name: strPtr("client-01"), Remark: strPtr(" ")} d.Normalize() if d.Remark == nil { t.Fatal("пустая пометка превращена в «не задано»: очистить комментарий станет нечем") } if *d.Remark != "" { t.Fatalf("пометка не обрезана: %q", *d.Remark) } } func TestPeerUpdateNormalizeTreatsBlankIdentityFieldsAsAbsent(t *testing.T) { d := PeerUpdateDto{Name: strPtr(" "), Secret: strPtr("")} d.Normalize() if d.Name != nil { t.Error("пустое имя при изменении обязано означать «не менять»") } if d.Secret != nil { t.Error("пустой секрет при изменении обязан означать «не менять»") } } func TestPeerUpdateNormalizeKeepsZeroValuedFlags(t *testing.T) { d := PeerUpdateDto{ Disabled: i64Ptr(0), QuotaBytes: i64Ptr(0), MaxDevices: i64Ptr(1), } d.Normalize() if d.Disabled == nil || *d.Disabled != 0 { t.Error("disabled=0 означает «включён», а не «не задано»") } if d.QuotaBytes == nil || *d.QuotaBytes != 0 { t.Error("quotaBytes=0 означает нулевую квоту, а не «не задано»") } } // Регрессия: очищенный крестиком фильтр отправлялся как `?name=` и отказывал // правилом длины, то есть список пиров ломался в один клик. func TestPeerPageNormalizeDropsClearedFilters(t *testing.T) { d := PeerPageDto{Name: strPtr(""), Remark: strPtr(" ")} d.Normalize() if d.Name != nil || d.Remark != nil { t.Fatalf("очищенный фильтр не снят: name=%v remark=%v", d.Name, d.Remark) } } func TestBaseNormalizeDropsZeroTimestamps(t *testing.T) { d := BaseDto{StartTime: i64Ptr(0), EndTime: i64Ptr(0)} d.Normalize() if d.StartTime != nil || d.EndTime != nil { t.Fatal("нулевая отметка времени означает отсутствие фильтра") } kept := BaseDto{StartTime: i64Ptr(1), EndTime: i64Ptr(2)} kept.Normalize() if kept.StartTime == nil || kept.EndTime == nil { t.Fatal("заданные отметки времени потеряны") } } func TestLogNormalizeDropsZeroLineCount(t *testing.T) { d := LogDto{NumLine: intPtr(0)} d.Normalize() if d.NumLine != nil { t.Fatal("«показать 0 строк» — это пропущенный параметр, а не запрос") } } // Все нормализуемые DTO обязаны реализовывать интерфейс: слой контроллеров // вызывает Normalize через него, и забытая реализация означала бы молча // пропущенный шаг. func TestNormalizableIsImplemented(t *testing.T) { var _ Normalizable = (*PeerSaveDto)(nil) var _ Normalizable = (*PeerUpdateDto)(nil) var _ Normalizable = (*PeerPageDto)(nil) var _ Normalizable = (*BaseDto)(nil) var _ Normalizable = (*LogDto)(nil) }