Ctrl K
ComunidadGuías…

cli/exp_api_internal_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli

import (
	"encoding/json"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestAPIURLIsInsecure(t *testing.T) {
	t.Parallel()
	for raw, want := range map[string]bool{
		"https://coder.example.com": false,
		"http://coder.example.com":  true,
		"http://10.0.0.5:3000":      true,
		"http://localhost:3000":     false,
		"http://127.0.0.1:3000":     false,
		"http://[::1]:3000":         false,
	} {
		u, err := url.Parse(raw)
		require.NoError(t, err)
		require.Equal(t, want, apiURLIsInsecure(u), raw)
	}
}

func TestAPIRequestPath(t *testing.T) {
	t.Parallel()
	for _, tc := range []struct {
		raw     string
		want    string
		wantErr string
	}{
		{raw: "organizations", want: "/api/v2/organizations"},
		{raw: "/organizations", want: "/api/v2/organizations"},
		{raw: "/api/v2/users", want: "/api/v2/users"},
		{raw: "api/v2/users", want: "/api/v2/users"},
		{raw: "/api/experimental/chats", want: "/api/experimental/chats"},
		{raw: "users:me", want: "/api/v2/users:me"},
		{raw: "workspaces?q=owner:me", want: "/api/v2/workspaces?q=owner:me"},
		{raw: "users?redirect=https://example.com", want: "/api/v2/users?redirect=https://example.com"},
		{raw: "https://example.com/api/v2/users/me", wantErr: "must not include a scheme or host"},
		{raw: "//example.com/api/v2/users/me", wantErr: "must not include a scheme or host"},
		{raw: "../../@alice/ws/apps/code/", wantErr: "must not contain . or .. segments"},
		{raw: "workspaces/../../../@alice/ws/apps/code/", wantErr: "must not contain . or .. segments"},
		{raw: "/api/../healthz", wantErr: "must not contain . or .. segments"},
		{raw: "%2e%2e/%2E%2E/healthz", wantErr: "must not contain . or .. segments"},
		{raw: "users/./me", wantErr: "must not contain . or .. segments"},
	} {
		t.Run(tc.raw, func(t *testing.T) {
			t.Parallel()
			got, err := apiRequestPath(tc.raw)
			if tc.wantErr != "" {
				require.ErrorContains(t, err, tc.wantErr)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tc.want, got)
		})
	}
}

func TestSplitAPIArgs(t *testing.T) {
	t.Parallel()
	for _, tc := range []struct {
		name       string
		args       []string
		wantMethod string
		wantPath   string
		wantItems  []string
	}{
		{"PathOnly", []string{"users"}, "", "users", []string{}},
		{"LowercaseMethod", []string{"patch", "users/me", "a=b"}, http.MethodPatch, "users/me", []string{"a=b"}},
		{"EveryMethod", []string{"OPTIONS", "x"}, http.MethodOptions, "x", []string{}},
		{"PathThatIsNotAMethod", []string{"deleted", "a==b"}, "", "deleted", []string{"a==b"}},
		{"MethodWithoutPath", []string{"get"}, http.MethodGet, "", nil},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			method, path, items := splitAPIArgs(tc.args)
			require.Equal(t, tc.wantMethod, method)
			require.Equal(t, tc.wantPath, path)
			require.Equal(t, tc.wantItems, items)
		})
	}
}

func TestSplitAPIRequestItem(t *testing.T) {
	t.Parallel()
	for _, tc := range []struct {
		arg       string
		wantKey   string
		wantSep   string
		wantValue string
	}{
		{"X-Custom:value", "X-Custom", ":", "value"},
		{"Referer:http://example.com/a=b", "Referer", ":", "http://example.com/a=b"},
		{"q==owner:me", "q", "==", "owner:me"},
		{"name=value", "name", "=", "value"},
		{"email=a=b@example.com", "email", "=", "a=b@example.com"},
		{"count:=5", "count", ":=", "5"},
		{"script=@file.sh", "script", "=@", "file.sh"},
		{"meta:=@file.json", "meta", ":=@", "file.json"},
		{"empty=", "empty", "=", ""},
		// The first matching position wins over a longer separator later.
		{"a=b:=c", "a", "=", "b:=c"},
		{"a:b=c", "a", ":", "b=c"},
		// Escaped separator characters belong to the key.
		{`foo\==bar`, "foo=", "=", "bar"},
		{`foo\:bar:=1`, "foo:bar", ":=", "1"},
		{`a\=\=b==c`, "a==b", "==", "c"},
		{`at\@=x`, "at@", "=", "x"},
		// Escapes also apply in values, and a backslash before any other
		// character is kept.
		{`k=a\=b`, "k", "=", "a=b"},
		{`path\x=y`, `path\x`, "=", "y"},
		// Windows file paths keep their backslashes. Forward slashes avoid
		// escaping an @ that starts a directory name.
		{`script=@C:\Users\me\script.sh`, "script", "=@", `C:\Users\me\script.sh`},
		{`meta:=@C:/proj/node_modules/@types/x.json`, "meta", ":=@", "C:/proj/node_modules/@types/x.json"},
		// File separators for query parameters and headers are recognized so
		// they can be rejected, and an escaped @ is literal.
		{"q==@file", "q", "==@", "file"},
		{"X-Token:@file", "X-Token", ":@", "file"},
		{`q==\@me`, "q", "==", "@me"},
	} {
		t.Run(tc.arg, func(t *testing.T) {
			t.Parallel()
			key, sep, value, ok := splitAPIRequestItem(tc.arg)
			require.True(t, ok)
			require.Equal(t, tc.wantKey, key)
			require.Equal(t, tc.wantSep, sep)
			require.Equal(t, tc.wantValue, value)
		})
	}
	for _, arg := range []string{"novalue", `escaped\=only`, ""} {
		_, _, _, ok := splitAPIRequestItem(arg)
		require.False(t, ok, arg)
	}
}

func TestParseAPIRequestItems(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	textFile := filepath.Join(dir, "script.sh")
	require.NoError(t, os.WriteFile(textFile, []byte("echo hi\r\n"), 0o600))
	jsonFile := filepath.Join(dir, "meta.json")
	require.NoError(t, os.WriteFile(jsonFile, []byte(`{"a": [1, 2]}`+"\n"), 0o600))
	badJSONFile := filepath.Join(dir, "bad.json")
	require.NoError(t, os.WriteFile(badJSONFile, []byte(`{"a":`), 0o600))

	for _, tc := range []struct {
		name        string
		items       []string
		stdin       string
		wantHeaders http.Header
		wantQuery   url.Values
		wantBody    string
		wantErr     string
	}{
		{
			name:        "Headers",
			items:       []string{"X-One: a ", "X-One:b", "content-type:text/plain"},
			wantHeaders: http.Header{"X-One": {"a", "b"}, "Content-Type": {"text/plain"}},
		},
		{
			name:        "EmptyHeaderRemovesHeader",
			items:       []string{"Content-Type:", "X-One:a", "x-one:", "X-One:b"},
			wantHeaders: http.Header{"Content-Type": {}, "X-One": {"b"}},
		},
		{
			name:      "QueryAllowsRepeats",
			items:     []string{"q==owner:me", "q==status:running", "limit==5"},
			wantQuery: url.Values{"q": {"owner:me", "status:running"}, "limit": {"5"}},
		},
		{
			name: "DataFields",
			items: []string{
				"name=alice", "count:=5", "enabled:=true", "nothing:=null",
				`tags:=["a",2]`, `meta:={"owner":{"name":"bob"}}`, "num=5",
			},
			wantBody: `{"name":"alice","count":5,"enabled":true,"nothing":null,
				"tags":["a",2],"meta":{"owner":{"name":"bob"}},"num":"5"}`,
		},
		{
			name:     "EscapedKey",
			items:    []string{`foo\==bar`},
			wantBody: `{"foo=":"bar"}`,
		},
		{
			name:     "Files",
			items:    []string{"script=@" + textFile, "meta:=@" + jsonFile},
			wantBody: `{"script":"echo hi\r\n","meta":{"a":[1,2]}}`,
		},
		{
			name:     "StringFromStdin",
			items:    []string{"content=@-"},
			stdin:    "line1\nline2\n",
			wantBody: `{"content":"line1\nline2\n"}`,
		},
		{
			name:     "JSONFromStdin",
			items:    []string{"meta:=@-"},
			stdin:    `{"x":true}`,
			wantBody: `{"meta":{"x":true}}`,
		},
		{name: "InvalidJSON", items: []string{"count:=five"}, wantErr: "not valid JSON"},
		{name: "InvalidJSONFile", items: []string{"meta:=@" + badJSONFile}, wantErr: "not valid JSON"},
		{name: "MissingFile", items: []string{"a=@" + filepath.Join(dir, "missing")}, wantErr: "read file"},
		{name: "DuplicateDataKey", items: []string{"a=1", "a:=2"}, wantErr: `duplicate data field "a"`},
		{name: "NoSeparator", items: []string{"novalue"}, wantErr: "invalid request item"},
		{name: "EmptyKey", items: []string{"=value"}, wantErr: "missing name"},
		{name: "TwoStdinItems", items: []string{"a=@-", "b:=@-"}, stdin: "1", wantErr: "only one request item can read stdin"},
		{name: "HostHeader", items: []string{"host:example.com"}, wantErr: "Host header cannot be set"},
		{name: "QueryFromFile", items: []string{"q==@" + textFile}, wantErr: "from a file is not supported"},
		{name: "HeaderFromFile", items: []string{"X-Token:@" + textFile}, wantErr: "from a file is not supported"},
		{name: "NestedJSONKey", items: []string{"search[type]=id"}, wantErr: "nested JSON field names are not supported"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got, err := parseAPIRequestItems(tc.items, strings.NewReader(tc.stdin))
			if tc.wantErr != "" {
				require.ErrorContains(t, err, tc.wantErr)
				return
			}
			require.NoError(t, err)
			if tc.wantHeaders == nil {
				tc.wantHeaders = http.Header{}
			}
			if tc.wantQuery == nil {
				tc.wantQuery = url.Values{}
			}
			require.Equal(t, tc.wantHeaders, got.headers)
			require.Equal(t, tc.wantQuery, got.query)
			if tc.wantBody == "" {
				require.Empty(t, got.data)
				return
			}
			body, err := json.Marshal(got.data)
			require.NoError(t, err)
			require.JSONEq(t, tc.wantBody, string(body))
		})
	}
}