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) var _ Normalizable = (*LoginDto)(nil) var _ Normalizable = (*AdminChangePasswordDto)(nil) } // Пароль НЕ триммится ни на одной из двух форм. // // Набор его символов не ограничен, пробел по краям — часть значения, и // молчаливое его удаление означало бы, что на сервер уезжает не то, что ввёл // оператор. Логин при этом триммится: скопированное из bootstrap-admin.secret // значение регулярно приезжает с пробелом или переводом строки на конце. func TestPasswordIsNeverTrimmedByNormalization(t *testing.T) { login := LoginDto{Username: strPtr(" hy2xsadmin\n"), Pass: strPtr(" пароль ")} login.Normalize() if login.Username == nil || *login.Username != "hy2xsadmin" { t.Fatalf("логин не приведён к каноничному виду: %v", login.Username) } if login.Pass == nil || *login.Pass != " пароль " { t.Fatalf("пароль изменён нормализацией: %q", *login.Pass) } change := AdminChangePasswordDto{ OldPassword: strPtr("abcde "), NewPassword: strPtr(" abcdef"), } change.Normalize() if change.OldPassword == nil || *change.OldPassword != "abcde " { t.Fatalf("старый пароль изменён нормализацией: %v", change.OldPassword) } if change.NewPassword == nil || *change.NewPassword != " abcdef" { t.Fatalf("новый пароль изменён нормализацией: %v", change.NewPassword) } } // Пустой пароль обязан выглядеть НЕЗАДАННЫМ. // // У поля-указателя `required` считает значение заданным по самому факту // ненулевого указателя, поэтому без этого шага оператор, отправивший пустую // форму, получал отказ по длине вместо «поле обязательно». func TestEmptyPasswordBecomesAbsent(t *testing.T) { login := LoginDto{Username: strPtr("hy2xsadmin"), Pass: strPtr("")} login.Normalize() if login.Pass != nil { t.Fatalf("пустой пароль остался заданным: %q", *login.Pass) } change := AdminChangePasswordDto{OldPassword: strPtr(""), NewPassword: strPtr("")} change.Normalize() if change.OldPassword != nil || change.NewPassword != nil { t.Fatal("пустые пароли формы смены остались заданными") } }