package ops
import (
"context"
"strings"
"testing"
)
type valIn struct {
Repo string `json:"repo" validate:"required,max=64"`
Limit int `json:"limit" validate:"min=1,max=500"`
Sort string `json:"sort" validate:"oneof=asc|desc"`
Tags []string `json:"tags" validate:"max=10"`
Note string `json:"note"`
}
func TestValidateOK(t *testing.T) {
in := valIn{Repo: "sqlite", Limit: 50, Sort: "asc", Note: "anything"}
if err := Validate(in); err != nil {
t.Fatal(err)
}
}
func TestValidateRequired(t *testing.T) {
err := Validate(valIn{Limit: 1, Sort: "asc"})
if err == nil || !strings.Contains(err.Error(), "repo") || !strings.Contains(err.Error(), "required") {
t.Fatalf("want required error naming repo, got %v", err)
}
}
func TestValidateNumericBounds(t *testing.T) {
if err := Validate(valIn{Repo: "r", Limit: 0, Sort: "asc"}); err == nil {
t.Fatal("want min violation for limit=0, got nil")
}
if err := Validate(valIn{Repo: "r", Limit: 501, Sort: "asc"}); err == nil {
t.Fatal("want max violation for limit=501, got nil")
}
}
func TestValidateStringAndSliceLength(t *testing.T) {
if err := Validate(valIn{Repo: strings.Repeat("x", 65), Limit: 1, Sort: "asc"}); err == nil {
t.Fatal("want max-length violation for repo, got nil")
}
long := make([]string, 11)
if err := Validate(valIn{Repo: "r", Limit: 1, Sort: "asc", Tags: long}); err == nil {
t.Fatal("want max-length violation for tags, got nil")
}
}
func TestValidateOneof(t *testing.T) {
if err := Validate(valIn{Repo: "r", Limit: 1, Sort: "sideways"}); err == nil {
t.Fatal("want oneof violation, got nil")
}
// Empty non-required oneof field passes: oneof only constrains set values.
if err := Validate(valIn{Repo: "r", Limit: 1}); err != nil {
t.Fatal(err)
}
}
func TestValidateOptionalFieldAdmitsZero(t *testing.T) {
// Pin the semantics: min/max apply unconditionally. An optional field
// is one without `required` whose bounds admit the zero value.
type opt struct {
N int `json:"n" validate:"max=5"`
}
if err := Validate(opt{}); err != nil {
t.Fatal(err)
}
}
func TestRegisterRejectsBadValidateTag(t *testing.T) {
type badTag struct {
X int `json:"x" validate:"min=notanumber"`
}
r := NewRegistry()
err := Register(r, Op[badTag, echoOut]{
Name: "bad.tag",
Doc: "d",
Kind: Query,
Route: "GET /bad",
Run: func(_ context.Context, _ badTag) (echoOut, error) { return echoOut{}, nil },
})
if err == nil || !strings.Contains(err.Error(), "validate") {
t.Fatalf("want validate-tag error at register time, got %v", err)
}
}
func TestRegisterRejectsUnknownRule(t *testing.T) {
type badRule struct {
X string `json:"x" validate:"shiny"`
}
r := NewRegistry()
err := Register(r, Op[badRule, echoOut]{
Name: "bad.rule",
Doc: "d",
Kind: Query,
Route: "GET /badrule",
Run: func(_ context.Context, _ badRule) (echoOut, error) { return echoOut{}, nil },
})
if err == nil || !strings.Contains(err.Error(), "shiny") {
t.Fatalf("want unknown-rule error, got %v", err)
}
}
// A space in a oneof list is a typo for the separator, and it is
// caught when the op registers rather than when a caller is refused:
// "oneof=user shun" accepted only the string "user shun".
func TestOneofRejectsSpacesAtRegistration(t *testing.T) {
if _, err := parseRules("required,oneof=user shun"); err == nil || !strings.Contains(err.Error(), "|") {
t.Fatalf("FAIL: a spaced oneof was accepted: %v", err)
}
if _, err := parseRules("required,oneof=user|shun"); err != nil {
t.Fatalf("FAIL: a |-separated oneof was refused: %v", err)
}
}