fix: невалидное имя варианта проекта — предупреждение вместо ошибки

Имя варианта "main" или с недопустимыми символами больше не отклоняет
запрос ошибкой 400 — теперь автоматически конвертируется в валидное
(sanitizeProjectVariantName), а API/UI показывают variant_warning вместо
блокировки создания/переименования варианта.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-09-17 11:56:55 +03:00
co-authored by Claude Sonnet 5
parent 006d7e87c5
commit 2f3bdc609a
6 changed files with 140 additions and 80 deletions
+50 -10
View File
@@ -1,37 +1,47 @@
package services
import (
"errors"
"path/filepath"
"testing"
"git.mchus.pro/mchus/quoteforge/internal/localdb"
)
func TestProjectServiceCreateRejectsReservedMainVariant(t *testing.T) {
// TestProjectServiceCreateSanitizesReservedMainVariant verifies that naming a new
// variant "main" no longer errors: the name is auto-converted to a valid one and a
// warning is returned instead of the request being rejected.
func TestProjectServiceCreateSanitizesReservedMainVariant(t *testing.T) {
local, err := newProjectTestLocalDB(t)
if err != nil {
t.Fatalf("open localdb: %v", err)
}
service := NewProjectService(local)
_, err = service.Create("tester", &CreateProjectRequest{
project, warning, err := service.Create("tester", &CreateProjectRequest{
Code: "OPS-1",
Variant: "main",
})
if !errors.Is(err, ErrReservedMainVariant) {
t.Fatalf("expected ErrReservedMainVariant, got %v", err)
if err != nil {
t.Fatalf("create project: %v", err)
}
if warning == "" {
t.Fatalf("expected a warning about the reserved variant name")
}
if project.Variant == "" || normalizeProjectVariant(project.Variant) == "main" {
t.Fatalf("expected variant to be converted away from 'main', got %q", project.Variant)
}
}
func TestProjectServiceUpdateRejectsReservedMainVariant(t *testing.T) {
// TestProjectServiceUpdateSanitizesReservedMainVariant mirrors the create case for
// renaming an existing (non-main) variant to "main".
func TestProjectServiceUpdateSanitizesReservedMainVariant(t *testing.T) {
local, err := newProjectTestLocalDB(t)
if err != nil {
t.Fatalf("open localdb: %v", err)
}
service := NewProjectService(local)
created, err := service.Create("tester", &CreateProjectRequest{
created, _, err := service.Create("tester", &CreateProjectRequest{
Code: "OPS-1",
Variant: "Lenovo",
})
@@ -40,11 +50,41 @@ func TestProjectServiceUpdateRejectsReservedMainVariant(t *testing.T) {
}
mainName := "main"
_, err = service.Update(created.UUID, "tester", &UpdateProjectRequest{
updated, warning, err := service.Update(created.UUID, "tester", &UpdateProjectRequest{
Variant: &mainName,
})
if !errors.Is(err, ErrReservedMainVariant) {
t.Fatalf("expected ErrReservedMainVariant, got %v", err)
if err != nil {
t.Fatalf("update project: %v", err)
}
if warning == "" {
t.Fatalf("expected a warning about the reserved variant name")
}
if updated.Variant == "" || normalizeProjectVariant(updated.Variant) == "main" {
t.Fatalf("expected variant to be converted away from 'main', got %q", updated.Variant)
}
}
// TestSanitizeProjectVariantName_InvalidChars verifies disallowed characters are
// stripped/replaced rather than rejected outright.
func TestSanitizeProjectVariantName_InvalidChars(t *testing.T) {
sanitized, warning := sanitizeProjectVariantName("Лендинг сервер!")
if warning == "" {
t.Fatalf("expected a warning for invalid characters")
}
if !projectCodeRe.MatchString(sanitized) {
t.Fatalf("sanitized variant %q still fails projectCodeRe", sanitized)
}
}
// TestSanitizeProjectVariantName_ValidPassesThrough ensures already-valid names are
// left untouched with no warning.
func TestSanitizeProjectVariantName_ValidPassesThrough(t *testing.T) {
sanitized, warning := sanitizeProjectVariantName("lenovo-2u")
if warning != "" {
t.Fatalf("expected no warning, got %q", warning)
}
if sanitized != "lenovo-2u" {
t.Fatalf("expected unchanged variant, got %q", sanitized)
}
}