diff --git a/go.mod b/go.mod index 78dbe0c..1f5fe97 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.com/slon/shad-go go 1.13 require ( + github.com/go-resty/resty/v2 v2.1.0 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.4.0 go.uber.org/goleak v1.0.0 diff --git a/go.sum b/go.sum index 25e688a..a48ef17 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,15 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-resty/resty/v2 v2.1.0 h1:Z6IefCpUMfnvItVJaJXWv/pMiiD11So35QgwEELsldE= +github.com/go-resty/resty/v2 v2.1.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -47,6 +51,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -62,6 +68,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbO golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/urlshortener/main_test.go b/urlshortener/main_test.go new file mode 100644 index 0000000..b67497b --- /dev/null +++ b/urlshortener/main_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "reflect" + "sync" + "testing" + "time" + + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/require" + "gitlab.com/slon/shad-go/tools/testtool" +) + +const importPath = "gitlab.com/slon/shad-go/urlshortener" + +var binCache testtool.BinCache + +func TestMain(m *testing.M) { + os.Exit(func() int { + var teardown testtool.CloseFunc + binCache, teardown = testtool.NewBinCache() + defer teardown() + + return m.Run() + }()) +} + +func startServer(t *testing.T) (port string, stop func()) { + binary, err := binCache.GetBinary(importPath) + require.NoError(t, err) + + port, err = testtool.GetFreePort() + require.NoError(t, err, "unable to get free port") + + cmd := exec.Command(binary, "-port", port) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + + require.NoError(t, cmd.Start()) + + done := make(chan error) + go func() { + done <- cmd.Wait() + }() + + stop = func() { + _ = cmd.Process.Kill() + <-done + } + + if err = testtool.WaitForPort(t, time.Second*5, port); err != nil { + stop() + } + + require.NoError(t, err) + return +} + +func add(t *testing.T, c *resty.Client, shortenerURL, request string) string { + type Response struct { + URL string `json:"url"` + Key string `json:"key"` + } + + resp, err := c.R(). + SetBody(map[string]interface{}{"url": request}). + SetResult(&Response{}). + Post(shortenerURL) + + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode()) + response := resp.Result().(*Response) + require.Equal(t, request, response.URL) + require.Contains(t, resp.Header().Get("Content-Type"), "application/json") + + return response.Key +} + +func TestURLShortener_redirect(t *testing.T) { + port, stop := startServer(t) + defer stop() + + var mu sync.Mutex + redirects := make(map[string]struct{}) + + redirectTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + redirects[r.RequestURI] = struct{}{} + mu.Unlock() + _, _ = fmt.Fprintln(w, "hello") + })) + + client := resty.New() + addURL := fmt.Sprintf("http://localhost:%s/shorten", port) + + requests := make(map[string]struct{}) + for i := 0; i < 10; i++ { + path := "/" + testtool.RandomName() + req := redirectTarget.URL + path + requests[path] = struct{}{} + key := add(t, client, addURL, req) + + getURL := fmt.Sprintf("http://localhost:%s/go/%s", port, key) + resp, err := client.R().Get(getURL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode()) + } + + mu.Lock() + defer mu.Unlock() + + require.True(t, reflect.DeepEqual(requests, redirects), + fmt.Sprintf("expected: %+v, got: %+v", requests, redirects)) +} + +func TestURLShortener_badRequest(t *testing.T) { + port, stop := startServer(t) + defer stop() + + u := fmt.Sprintf("http://localhost:%s/shorten", port) + resp, err := resty.New().R(). + SetHeader("Content-Type", "application/json"). + SetBody([]byte(`{"url":"abc}`)). + Post(u) + + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode()) +} + +func TestURLShortener_badKey(t *testing.T) { + port, stop := startServer(t) + defer stop() + + u := fmt.Sprintf("http://localhost:%s/go/%s", port, testtool.RandomName()) + resp, err := resty.New(). + SetRedirectPolicy(resty.RedirectPolicyFunc(func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + })).R(). + Get(u) + + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode()) +} + +func TestURLShortener_consistency(t *testing.T) { + port, stop := startServer(t) + defer stop() + + client := resty.New() + + get := func(originalURL, key string) { + getURL := fmt.Sprintf("http://localhost:%s/go/%s", port, key) + + resp, err := client. + SetRedirectPolicy(resty.RedirectPolicyFunc(func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + })).R(). + Get(getURL) + + require.NoError(t, err) + require.Equal(t, http.StatusFound, resp.StatusCode()) + require.Contains(t, resp.Header().Get("Location"), originalURL) + } + + var urls []string + for i := 0; i < 10; i++ { + urls = append(urls, testtool.RandomName()) + } + + keyToURL := make(map[string]string) + urlToKey := make(map[string]string) + + addURL := fmt.Sprintf("http://localhost:%s/shorten", port) + for _, u := range urls { + key := add(t, client, addURL, u) + + url, ok := keyToURL[key] + require.False(t, ok, fmt.Sprintf("duplicate key %s for urls [%s, %s]", key, u, url)) + + urlToKey[u] = key + keyToURL[key] = u + } + + for _, u := range urls { + get(u, urlToKey[u]) + } + + for _, u := range urls { + key := add(t, client, addURL, u) + + url, ok := keyToURL[key] + require.True(t, ok, fmt.Sprintf("different keys for the same url %s", u)) + require.Equal(t, url, u) + } +}