diff --git a/internal/services/game_export_service/failure_test.go b/internal/services/game_export_service/failure_test.go index d6bdd46..8f2d612 100644 --- a/internal/services/game_export_service/failure_test.go +++ b/internal/services/game_export_service/failure_test.go @@ -9,14 +9,14 @@ import ( ) func TestSafeFailure(t *testing.T) { - validation := &game_package_service.ValidationError{Reason: "некорректная команда"} + validation := &game_package_service.ValidationError{Reason: "шаг экранов №2 — текст экрана не должен быть пустым"} for _, tc := range []struct { name string err error want string }{ - {"validation", validation, "Не удалось подготовить автономный пакет: некорректная команда"}, - {"wrapped validation", fmt.Errorf("private path: %w", validation), "Не удалось подготовить автономный пакет: некорректная команда"}, + {"validation", validation, "Не удалось подготовить автономный пакет: шаг экранов №2 — текст экрана не должен быть пустым"}, + {"wrapped validation", fmt.Errorf("private path: %w", validation), "Не удалось подготовить автономный пакет: шаг экранов №2 — текст экрана не должен быть пустым"}, {"invalid package", errors.Join(game_package_service.ErrInvalidPackage, errors.New("private")), "Данные игры не подходят для автономного пакета."}, {"technical", errors.New("private password"), "Не удалось подготовить пакет. Запустите экспорт повторно."}, {"media", game_package_service.ErrMediaUnavailable, "Не удалось получить медиафайлы игры. Проверьте их доступность и повторите экспорт."}, diff --git a/internal/services/game_package_service/service_test.go b/internal/services/game_package_service/service_test.go index 1ee38f4..a2f3cdc 100644 --- a/internal/services/game_package_service/service_test.go +++ b/internal/services/game_package_service/service_test.go @@ -882,3 +882,33 @@ func TestPackageValidatesScreenSteps(t *testing.T) { }) } } + +func TestScreenStepFailureExplainsReason(t *testing.T) { + for _, tc := range []struct { + name string + step game_screen_plans_repo.Step + reason string + }{ + {"mode", game_screen_plans_repo.Step{Mode: 11}, "неподдерживаемый режим экрана"}, + {"timer type", game_screen_plans_repo.Step{Mode: 7, TimerType: 3}, "неизвестный тип таймера"}, + {"duration", game_screen_plans_repo.Step{Mode: 7}, "длительность таймера должна быть от 1 до 86400 секунд"}, + {"timer label", game_screen_plans_repo.Step{Mode: 7, TimerDurationSeconds: 1, TimerLabel: strings.Repeat("a", 201)}, "подпись таймера превышает 200 байт"}, + {"empty text", game_screen_plans_repo.Step{Mode: 8, Text: " "}, "текст экрана не должен быть пустым"}, + {"long text", game_screen_plans_repo.Step{Mode: 8, Text: strings.Repeat("a", 10001)}, "текст экрана превышает 10000 байт"}, + {"QR URL", game_screen_plans_repo.Step{Mode: 9, QRURL: "javascript:private"}, "ссылка QR-кода должна использовать HTTP/HTTPS, содержать адрес сайта, не содержать логин, пароль и крайние пробелы и занимать не более 2048 байт"}, + {"QR caption", game_screen_plans_repo.Step{Mode: 9, QRURL: "https://example.org", Text: strings.Repeat("a", 10001)}, "подпись QR-кода превышает 10000 байт"}, + } { + t.Run(tc.name, func(t *testing.T) { + snapshot, _ := fixture() + snapshot.Data.ScreenPlan.Steps = []game_screen_plans_repo.Step{{Mode: 1}, tc.step} + err := validateData(&snapshot.Data) + var validation *ValidationError + if !errors.Is(err, ErrInvalidPackage) || !errors.As(err, &validation) { + t.Fatalf("expected validation error, got %v", err) + } + if want := "шаг экранов №2 — " + tc.reason; validation.Reason != want { + t.Fatalf("got %q, want %q", validation.Reason, want) + } + }) + } +} diff --git a/internal/services/game_package_service/validation.go b/internal/services/game_package_service/validation.go index b7b73a9..4918867 100644 --- a/internal/services/game_package_service/validation.go +++ b/internal/services/game_package_service/validation.go @@ -156,13 +156,28 @@ func validateData(data *Data) error { (len(plan.Steps) > 0 && plan.CurrentStep >= len(plan.Steps)) { return invalid("некорректный план экранов") } - for _, step := range plan.Steps { - if !screen_service.ValidMode(step.Mode) || - !screen_service.ValidTimerType(step.TimerType) || - (step.Mode == 9 && !screen_service.ValidQR(step.QRURL, step.Text)) || - (step.Mode == 7 && (step.TimerDurationSeconds <= 0 || step.TimerDurationSeconds > 86400 || len(step.TimerLabel) > 200)) || - (step.Mode == 8 && (strings.TrimSpace(step.Text) == "" || len(step.Text) > 10000)) { - return invalid("некорректный шаг плана экранов") + for i, step := range plan.Steps { + var reason string + switch { + case !screen_service.ValidMode(step.Mode): + reason = "неподдерживаемый режим экрана" + case !screen_service.ValidTimerType(step.TimerType): + reason = "неизвестный тип таймера" + case step.Mode == 7 && (step.TimerDurationSeconds <= 0 || step.TimerDurationSeconds > 86400): + reason = "длительность таймера должна быть от 1 до 86400 секунд" + case step.Mode == 7 && len(step.TimerLabel) > 200: + reason = "подпись таймера превышает 200 байт" + case step.Mode == 8 && strings.TrimSpace(step.Text) == "": + reason = "текст экрана не должен быть пустым" + case step.Mode == 8 && len(step.Text) > 10000: + reason = "текст экрана превышает 10000 байт" + case step.Mode == 9 && !screen_service.ValidQR(step.QRURL, ""): + reason = "ссылка QR-кода должна использовать HTTP/HTTPS, содержать адрес сайта, не содержать логин, пароль и крайние пробелы и занимать не более 2048 байт" + case step.Mode == 9 && len(step.Text) > 10000: + reason = "подпись QR-кода превышает 10000 байт" + } + if reason != "" { + return invalid(fmt.Sprintf("шаг экранов №%d — %s", i+1, reason)) } } }