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)
}
}
}