mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
This more closely aligns with GitHub's label search style. Actual search params need to be converted to allow this format, by default they will throw an error if they do not support listing.
465 lines
12 KiB
Go
465 lines
12 KiB
Go
package httpapi_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
)
|
|
|
|
type queryParamTestCase[T any] struct {
|
|
QueryParam string
|
|
// No set does not set the query param, rather than setting the empty value
|
|
NoSet bool
|
|
// Value vs values is the difference between a single query param and multiple
|
|
// to the same key.
|
|
// -> key=value
|
|
Value string
|
|
// -> key=value1 key=value2
|
|
Values []string
|
|
Default T
|
|
Expected T
|
|
ExpectedErrorContains string
|
|
Parse func(r *http.Request, def T, queryParam string) T
|
|
}
|
|
|
|
func TestParseQueryParams(t *testing.T) {
|
|
t.Parallel()
|
|
const multipleValuesError = "provided more than once"
|
|
|
|
t.Run("Enum", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expParams := []queryParamTestCase[database.ResourceType]{
|
|
{
|
|
QueryParam: "resource_type",
|
|
Value: string(database.ResourceTypeWorkspace),
|
|
Expected: database.ResourceTypeWorkspace,
|
|
},
|
|
{
|
|
QueryParam: "bad_type",
|
|
Value: "foo",
|
|
ExpectedErrorContains: "not a valid value",
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, func(vals url.Values, def database.ResourceType, queryParam string) database.ResourceType {
|
|
return httpapi.ParseCustom(parser, vals, def, queryParam, httpapi.ParseEnum[database.ResourceType])
|
|
})
|
|
})
|
|
|
|
t.Run("EnumList", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expParams := []queryParamTestCase[[]database.ResourceType]{
|
|
{
|
|
QueryParam: "resource_type",
|
|
Value: fmt.Sprintf("%s,%s", database.ResourceTypeWorkspace, database.ResourceTypeApiKey),
|
|
Expected: []database.ResourceType{database.ResourceTypeWorkspace, database.ResourceTypeApiKey},
|
|
},
|
|
{
|
|
QueryParam: "resource_type_as_list",
|
|
Values: []string{string(database.ResourceTypeWorkspace), string(database.ResourceTypeApiKey)},
|
|
Expected: []database.ResourceType{database.ResourceTypeWorkspace, database.ResourceTypeApiKey},
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, func(vals url.Values, def []database.ResourceType, queryParam string) []database.ResourceType {
|
|
return httpapi.ParseCustomList(parser, vals, def, queryParam, httpapi.ParseEnum[database.ResourceType])
|
|
})
|
|
})
|
|
|
|
t.Run("Time", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expParams := []queryParamTestCase[time.Time]{
|
|
{
|
|
QueryParam: "date",
|
|
Value: "2023-01-16T00:00:00+12:00",
|
|
Expected: time.Date(2023, 1, 15, 12, 0, 0, 0, time.UTC),
|
|
},
|
|
{
|
|
QueryParam: "bad_date",
|
|
Value: "2010",
|
|
ExpectedErrorContains: "must be a valid date format",
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, func(vals url.Values, def time.Time, queryParam string) time.Time {
|
|
return parser.Time3339Nano(vals, time.Time{}, queryParam)
|
|
})
|
|
})
|
|
|
|
t.Run("UUID", func(t *testing.T) {
|
|
t.Parallel()
|
|
me := uuid.New()
|
|
expParams := []queryParamTestCase[uuid.UUID]{
|
|
{
|
|
QueryParam: "valid_id",
|
|
Value: "afe39fbf-0f52-4a62-b0cc-58670145d773",
|
|
Expected: uuid.MustParse("afe39fbf-0f52-4a62-b0cc-58670145d773"),
|
|
},
|
|
{
|
|
QueryParam: "me",
|
|
Value: "me",
|
|
Expected: me,
|
|
},
|
|
{
|
|
QueryParam: "invalid_id",
|
|
Value: "bogus",
|
|
ExpectedErrorContains: "invalid UUID length",
|
|
},
|
|
{
|
|
QueryParam: "long_id",
|
|
Value: "afe39fbf-0f52-4a62-b0cc-58670145d773-123",
|
|
ExpectedErrorContains: "invalid UUID length",
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Default: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Expected: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
},
|
|
{
|
|
QueryParam: "empty",
|
|
Value: "",
|
|
Expected: uuid.Nil,
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, func(vals url.Values, def uuid.UUID, queryParam string) uuid.UUID {
|
|
return parser.UUIDorMe(vals, def, me, queryParam)
|
|
})
|
|
})
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
t.Parallel()
|
|
expParams := []queryParamTestCase[string]{
|
|
{
|
|
QueryParam: "valid_string",
|
|
Value: "random",
|
|
Expected: "random",
|
|
},
|
|
{
|
|
QueryParam: "empty",
|
|
Value: "",
|
|
Expected: "",
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Default: "default",
|
|
Expected: "default",
|
|
},
|
|
{
|
|
QueryParam: "unexpected_list",
|
|
Values: []string{"one", "two"},
|
|
ExpectedErrorContains: multipleValuesError,
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, parser.String)
|
|
})
|
|
|
|
t.Run("Boolean", func(t *testing.T) {
|
|
t.Parallel()
|
|
expParams := []queryParamTestCase[bool]{
|
|
{
|
|
QueryParam: "valid_true",
|
|
Value: "true",
|
|
Expected: true,
|
|
},
|
|
{
|
|
QueryParam: "casing",
|
|
Value: "True",
|
|
Expected: true,
|
|
},
|
|
{
|
|
QueryParam: "all_caps",
|
|
Value: "TRUE",
|
|
Expected: true,
|
|
},
|
|
{
|
|
QueryParam: "no_value_true_def",
|
|
NoSet: true,
|
|
Default: true,
|
|
Expected: true,
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Expected: false,
|
|
},
|
|
|
|
{
|
|
QueryParam: "invalid_boolean",
|
|
Value: "yes",
|
|
Expected: false,
|
|
ExpectedErrorContains: "must be a valid boolean",
|
|
},
|
|
{
|
|
QueryParam: "unexpected_list",
|
|
Values: []string{"true", "false"},
|
|
ExpectedErrorContains: multipleValuesError,
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, parser.Boolean)
|
|
})
|
|
|
|
t.Run("Int", func(t *testing.T) {
|
|
t.Parallel()
|
|
expParams := []queryParamTestCase[int]{
|
|
{
|
|
QueryParam: "valid_integer",
|
|
Value: "100",
|
|
Expected: 100,
|
|
},
|
|
{
|
|
QueryParam: "empty",
|
|
Value: "",
|
|
Expected: 0,
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Default: 5,
|
|
Expected: 5,
|
|
},
|
|
{
|
|
QueryParam: "negative",
|
|
Value: "-10",
|
|
Expected: -10,
|
|
Default: 5,
|
|
},
|
|
{
|
|
QueryParam: "invalid_integer",
|
|
Value: "bogus",
|
|
Expected: 0,
|
|
ExpectedErrorContains: "must be a valid integer",
|
|
},
|
|
{
|
|
QueryParam: "unexpected_list",
|
|
Values: []string{"5", "10"},
|
|
ExpectedErrorContains: multipleValuesError,
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, parser.Int)
|
|
})
|
|
|
|
t.Run("PositiveInt32", func(t *testing.T) {
|
|
t.Parallel()
|
|
expParams := []queryParamTestCase[int32]{
|
|
{
|
|
QueryParam: "valid_integer",
|
|
Value: "100",
|
|
Expected: 100,
|
|
},
|
|
{
|
|
QueryParam: "empty",
|
|
Value: "",
|
|
Expected: 0,
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Default: 5,
|
|
Expected: 5,
|
|
},
|
|
{
|
|
QueryParam: "negative",
|
|
Value: "-1",
|
|
Expected: 0,
|
|
Default: 5,
|
|
ExpectedErrorContains: "must be a valid 32-bit positive integer",
|
|
},
|
|
{
|
|
QueryParam: "invalid_integer",
|
|
Value: "bogus",
|
|
Expected: 0,
|
|
ExpectedErrorContains: "must be a valid 32-bit positive integer",
|
|
},
|
|
{
|
|
QueryParam: "max_int_plus_one",
|
|
Value: "2147483648",
|
|
Expected: 0,
|
|
ExpectedErrorContains: "must be a valid 32-bit positive integer",
|
|
},
|
|
{
|
|
QueryParam: "unexpected_list",
|
|
Values: []string{"5", "10"},
|
|
ExpectedErrorContains: multipleValuesError,
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, parser.PositiveInt32)
|
|
})
|
|
|
|
t.Run("UInt", func(t *testing.T) {
|
|
t.Parallel()
|
|
expParams := []queryParamTestCase[uint64]{
|
|
{
|
|
QueryParam: "valid_integer",
|
|
Value: "100",
|
|
Expected: 100,
|
|
},
|
|
{
|
|
QueryParam: "empty",
|
|
Value: "",
|
|
Expected: 0,
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Default: 5,
|
|
Expected: 5,
|
|
},
|
|
{
|
|
QueryParam: "negative",
|
|
Value: "-10",
|
|
Default: 5,
|
|
ExpectedErrorContains: "must be a valid positive integer",
|
|
},
|
|
{
|
|
QueryParam: "invalid_integer",
|
|
Value: "bogus",
|
|
Expected: 0,
|
|
ExpectedErrorContains: "must be a valid positive integer",
|
|
},
|
|
{
|
|
QueryParam: "unexpected_list",
|
|
Values: []string{"5", "10"},
|
|
ExpectedErrorContains: multipleValuesError,
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, parser.UInt)
|
|
})
|
|
|
|
t.Run("UUIDs", func(t *testing.T) {
|
|
t.Parallel()
|
|
expParams := []queryParamTestCase[[]uuid.UUID]{
|
|
{
|
|
QueryParam: "valid_ids_with_spaces",
|
|
Value: "6c8ef17d-5dd8-4b92-bac9-41944f90f237, 65fb05f3-12c8-4a0a-801f-40439cf9e681 , 01b94888-1eab-4bbf-aed0-dc7a8010da97",
|
|
Expected: []uuid.UUID{
|
|
uuid.MustParse("6c8ef17d-5dd8-4b92-bac9-41944f90f237"),
|
|
uuid.MustParse("65fb05f3-12c8-4a0a-801f-40439cf9e681"),
|
|
uuid.MustParse("01b94888-1eab-4bbf-aed0-dc7a8010da97"),
|
|
},
|
|
},
|
|
{
|
|
QueryParam: "empty",
|
|
Value: "",
|
|
Default: []uuid.UUID{},
|
|
Expected: []uuid.UUID{},
|
|
},
|
|
{
|
|
QueryParam: "no_value",
|
|
NoSet: true,
|
|
Default: []uuid.UUID{},
|
|
Expected: []uuid.UUID{},
|
|
},
|
|
{
|
|
QueryParam: "default",
|
|
NoSet: true,
|
|
Default: []uuid.UUID{uuid.Nil},
|
|
Expected: []uuid.UUID{uuid.Nil},
|
|
},
|
|
{
|
|
QueryParam: "invalid_id_in_set",
|
|
Value: "6c8ef17d-5dd8-4b92-bac9-41944f90f237,bogus",
|
|
Expected: []uuid.UUID{},
|
|
Default: []uuid.UUID{},
|
|
ExpectedErrorContains: "bogus",
|
|
},
|
|
{
|
|
QueryParam: "multiple_keys",
|
|
Values: []string{"6c8ef17d-5dd8-4b92-bac9-41944f90f237", "65fb05f3-12c8-4a0a-801f-40439cf9e681"},
|
|
Expected: []uuid.UUID{
|
|
uuid.MustParse("6c8ef17d-5dd8-4b92-bac9-41944f90f237"),
|
|
uuid.MustParse("65fb05f3-12c8-4a0a-801f-40439cf9e681"),
|
|
},
|
|
},
|
|
{
|
|
QueryParam: "multiple_and_csv",
|
|
Values: []string{"6c8ef17d-5dd8-4b92-bac9-41944f90f237", "65fb05f3-12c8-4a0a-801f-40439cf9e681, 01b94888-1eab-4bbf-aed0-dc7a8010da97"},
|
|
Expected: []uuid.UUID{
|
|
uuid.MustParse("6c8ef17d-5dd8-4b92-bac9-41944f90f237"),
|
|
uuid.MustParse("65fb05f3-12c8-4a0a-801f-40439cf9e681"),
|
|
uuid.MustParse("01b94888-1eab-4bbf-aed0-dc7a8010da97"),
|
|
},
|
|
},
|
|
}
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
testQueryParams(t, expParams, parser, parser.UUIDs)
|
|
})
|
|
|
|
t.Run("Required", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
parser.RequiredNotEmpty("test_value")
|
|
parser.UUID(url.Values{}, uuid.New(), "test_value")
|
|
require.Len(t, parser.Errors, 1)
|
|
|
|
parser = httpapi.NewQueryParamParser()
|
|
parser.RequiredNotEmpty("test_value")
|
|
parser.String(url.Values{"test_value": {""}}, "", "test_value")
|
|
require.Len(t, parser.Errors, 1)
|
|
})
|
|
}
|
|
|
|
func testQueryParams[T any](t *testing.T, testCases []queryParamTestCase[T], parser *httpapi.QueryParamParser, parse func(vals url.Values, def T, queryParam string) T) {
|
|
v := url.Values{}
|
|
for _, c := range testCases {
|
|
if c.NoSet {
|
|
continue
|
|
}
|
|
if len(c.Values) > 0 && c.Value != "" {
|
|
t.Errorf("test case %q has both value and values, choose one, not both!", c.QueryParam)
|
|
t.FailNow()
|
|
}
|
|
if c.Value != "" {
|
|
c.Values = append(c.Values, c.Value)
|
|
}
|
|
|
|
for _, value := range c.Values {
|
|
v.Add(c.QueryParam, value)
|
|
}
|
|
}
|
|
|
|
for _, c := range testCases {
|
|
// !! Do not run these in parallel !!
|
|
t.Run(c.QueryParam, func(t *testing.T) {
|
|
value := parse(v, c.Default, c.QueryParam)
|
|
require.Equal(t, c.Expected, value, fmt.Sprintf("param=%q value=%q", c.QueryParam, c.Value))
|
|
if c.ExpectedErrorContains != "" {
|
|
errors := parser.Errors
|
|
require.True(t, len(errors) > 0, "error exist")
|
|
last := errors[len(errors)-1]
|
|
require.True(t, last.Field == c.QueryParam, fmt.Sprintf("query param %q did not fail", c.QueryParam))
|
|
require.Contains(t, last.Detail, c.ExpectedErrorContains, "correct error")
|
|
}
|
|
})
|
|
}
|
|
}
|