Skip to content
quarry Artifact [e6d514bbab]
Login

Artifact [e6d514bbab]

Artifact e6d514bbab095fcbef6bcd5afb5df12a2f32e5f8fe315221ab4f7d0e5e9ecfc4:


package ops

import (
	"net/url"
	"reflect"
	"strings"
	"testing"
)

type formIn struct {
	Repo    string   `json:"repo"`
	Limit   int      `json:"limit"`
	After   uint64   `json:"after"`
	Ratio   float64  `json:"ratio"`
	Deleted bool     `json:"deleted"`
	Tags    []string `json:"tags"`
	Skip    string   `json:"-"`
	NoTag   string
}

func TestDecodeValues(t *testing.T) {
	v := url.Values{
		"repo":    {"sqlite"},
		"limit":   {"50"},
		"after":   {"12345"},
		"ratio":   {"0.5"},
		"deleted": {"true"},
		"tags":    {"a", "b"},
		"NoTag":   {"plain"},
	}
	var in formIn
	if err := DecodeValues(v, &in); err != nil {
		t.Fatal(err)
	}
	want := formIn{
		Repo: "sqlite", Limit: 50, After: 12345, Ratio: 0.5,
		Deleted: true, Tags: []string{"a", "b"}, NoTag: "plain",
	}
	if !reflect.DeepEqual(in, want) {
		t.Fatalf("got %+v, want %+v", in, want)
	}
}

func TestDecodeValuesMissingKeysLeaveZero(t *testing.T) {
	var in formIn
	if err := DecodeValues(url.Values{"repo": {"x"}}, &in); err != nil {
		t.Fatal(err)
	}
	if in.Repo != "x" || in.Limit != 0 || in.Tags != nil {
		t.Fatalf("got %+v", in)
	}
}

func TestDecodeValuesUnknownKeysIgnored(t *testing.T) {
	// HTML forms carry extras (CSRF tokens, submit buttons); never reject.
	var in formIn
	if err := DecodeValues(url.Values{"csrf": {"tok"}}, &in); err != nil {
		t.Fatal(err)
	}
}

func TestDecodeValuesBadNumber(t *testing.T) {
	var in formIn
	err := DecodeValues(url.Values{"limit": {"abc"}}, &in)
	if err == nil || !strings.Contains(err.Error(), "limit") {
		t.Fatalf("want error naming field limit, got %v", err)
	}
}

func TestDecodeValuesBadBool(t *testing.T) {
	var in formIn
	if err := DecodeValues(url.Values{"deleted": {"maybe"}}, &in); err == nil {
		t.Fatal("want bool parse error, got nil")
	}
}

func TestDecodeValuesSkipsDashTag(t *testing.T) {
	var in formIn
	if err := DecodeValues(url.Values{"Skip": {"x"}, "-": {"y"}}, &in); err != nil {
		t.Fatal(err)
	}
	if in.Skip != "" {
		t.Fatalf("json:\"-\" field decoded: %+v", in)
	}
}

func TestDecodeValuesRejectsNonPointerStruct(t *testing.T) {
	if err := DecodeValues(url.Values{}, formIn{}); err == nil {
		t.Fatal("want error for non-pointer dst, got nil")
	}
	x := 5
	if err := DecodeValues(url.Values{}, &x); err == nil {
		t.Fatal("want error for non-struct dst, got nil")
	}
}

func TestDecodeValuesRejectsUnsupportedFieldType(t *testing.T) {
	type bad struct {
		Nested struct{ X int } `json:"nested"`
	}
	var b bad
	if err := DecodeValues(url.Values{"nested": {"{}"}}, &b); err == nil {
		t.Fatal("want unsupported-type error, got nil")
	}
}

func TestWildcards(t *testing.T) {
	for _, tc := range []struct {
		pattern string
		want    []string
	}{
		{"/api/v1/r/{repo}/timeline", []string{"repo"}},
		{"/api/v1/r/{repo}/checkin/{hash}", []string{"repo", "hash"}},
		{"/api/v1/version", nil},
		{"/files/{path...}", []string{"path"}},
	} {
		got := Wildcards(tc.pattern)
		if !reflect.DeepEqual(got, tc.want) {
			t.Errorf("Wildcards(%q) = %v, want %v", tc.pattern, got, tc.want)
		}
	}
}

func TestDecodeValuesAppliesDefaults(t *testing.T) {
	type withDef struct {
		Limit int `json:"limit" validate:"min=0,max=200,default=50"`
	}
	var in withDef
	if err := DecodeValues(url.Values{}, &in); err != nil {
		t.Fatal(err)
	}
	if in.Limit != 50 {
		t.Fatalf("absent key: limit = %d, want default 50", in.Limit)
	}
	// An explicit 0 is NOT the default (ticket 080).
	in = withDef{}
	if err := DecodeValues(url.Values{"limit": {"0"}}, &in); err != nil {
		t.Fatal(err)
	}
	if in.Limit != 0 {
		t.Fatalf("explicit 0: limit = %d", in.Limit)
	}
}

func TestDecodeValuesCleanParseErrors(t *testing.T) {
	var in formIn
	err := DecodeValues(url.Values{"limit": {"abc"}}, &in)
	if err == nil || strings.Contains(err.Error(), "strconv") || !strings.Contains(err.Error(), "must be an integer") {
		t.Fatalf("err = %v", err)
	}
}

// The router decodes twice — query/body first, then the routed path so
// the path wins. DecodeValues applies a field's default when its key is
// absent, and EVERY field is absent from a path that names one wildcard,
// so the second pass used to reset defaulted fields that the first pass
// had set. ?limit=200 became 50 on every op with a wildcard route.
func TestOverlayValuesKeepsWhatItDoesNotName(t *testing.T) {
	type in struct {
		Repo   string `json:"repo"`
		Limit  int    `json:"limit" validate:"min=0,max=200,default=50"`
		Branch string `json:"branch"`
	}

	var got in
	if err := DecodeValues(url.Values{"limit": {"200"}, "branch": {"trunk"}}, &got); err != nil {
		t.Fatal(err)
	}
	if got.Limit != 200 {
		t.Fatalf("first pass: Limit = %d, want 200", got.Limit)
	}

	// The path names only repo; everything else must survive untouched.
	if err := OverlayValues(url.Values{"repo": {"alpha"}}, &got); err != nil {
		t.Fatal(err)
	}
	if got.Repo != "alpha" {
		t.Errorf("Repo = %q, want the path value", got.Repo)
	}
	if got.Limit != 200 {
		t.Errorf("Limit = %d — the path pass reset it to its default", got.Limit)
	}
	if got.Branch != "trunk" {
		t.Errorf("Branch = %q, want it preserved", got.Branch)
	}
}

// DecodeValues must keep applying defaults; the fix is scoped to the
// overlay pass, not a removal of default handling.
func TestDecodeValuesStillAppliesDefaults(t *testing.T) {
	type in struct {
		Limit int `json:"limit" validate:"min=0,max=200,default=50"`
	}
	var got in
	if err := DecodeValues(url.Values{}, &got); err != nil {
		t.Fatal(err)
	}
	if got.Limit != 50 {
		t.Errorf("Limit = %d, want the declared default 50", got.Limit)
	}
}

// `default=` on a bool parsed, published itself in the op documentation,
// and then did nothing, because applyDefault had cases for int, uint
// and float and none for bool. An op that documented a field as
// defaulting to true handed the caller false.
func TestABoolTakesItsDeclaredDefault(t *testing.T) {
	var in struct {
		On  bool `json:"on" validate:"default=1"`
		Off bool `json:"off"`
	}
	if err := DecodeValues(url.Values{}, &in); err != nil {
		t.Fatal(err)
	}
	if !in.On {
		t.Error("FAIL: a bool declared default=1 arrived false")
	}
	if in.Off {
		t.Error("FAIL: a bool with no default did not arrive false")
	}
	// An explicit value still wins over the default, or the default
	// would make the field unsettable.
	if err := DecodeValues(url.Values{"on": {"false"}}, &in); err != nil {
		t.Fatal(err)
	}
	if in.On {
		t.Error("FAIL: an explicit false was overwritten by the default")
	}
}

// A pointer field is how an In struct says "absent" when absent has to
// mean something other than the zero value — keeping what is already
// stored rather than setting it to false.
func TestAPointerFieldDistinguishesAbsentFromZero(t *testing.T) {
	type in struct {
		Listed *bool `json:"listed"`
	}
	var absent in
	if err := DecodeValues(url.Values{}, &absent); err != nil {
		t.Fatal(err)
	}
	if absent.Listed != nil {
		t.Error("FAIL: an absent key did not leave the pointer nil")
	}
	for _, tc := range []struct {
		sent string
		want bool
	}{{"true", true}, {"false", false}} {
		var got in
		if err := DecodeValues(url.Values{"listed": {tc.sent}}, &got); err != nil {
			t.Fatal(err)
		}
		if got.Listed == nil {
			t.Errorf("FAIL: listed=%s left the pointer nil", tc.sent)
			continue
		}
		if *got.Listed != tc.want {
			t.Errorf("FAIL: listed=%s decoded as %v", tc.sent, *got.Listed)
		}
	}
}