diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4a408f7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Test +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go: ["1.21", "1.20"] + runs-on: ${{ matrix.os }} + + steps: + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + + - name: Check out code + uses: actions/checkout@v3 + + - name: Restore Go modules cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} + restore-keys: | + go-${{ runner.os }}- + + - name: Download dependencies + run: go mod download + + - name: Run tests + run: go test -race ./... + + - name: Build + run: go build -v ./... diff --git a/.gitignore b/.gitignore index 3b735ec..f273cac 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,15 @@ # Go workspace file go.work + +# macOS +.DS_Store + +# VS Code +.vscode + +# IntelliJ +.idea + +# vim +*.swp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..830a30f --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY: test +test: + go test ./... diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..9ddb483 --- /dev/null +++ b/client_test.go @@ -0,0 +1,266 @@ +package testclient + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestHandler struct { + Path string + Method string + Response string + StatusCode int + Middleware func(t *testing.T, w http.ResponseWriter, r *http.Request) +} + +func createTestServer(t *testing.T, handlers []TestHandler) http.Handler { + mux := http.NewServeMux() + + for _, h := range handlers { + handler := h // For closure + mux.HandleFunc(handler.Path, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, handler.Method, r.Method) + + if handler.Middleware != nil { + handler.Middleware(t, w, r) + } + + w.WriteHeader(handler.StatusCode) + _, _ = w.Write([]byte(handler.Response)) + }) + } + + return mux +} + +func TestClient_New(t *testing.T) { + server := http.NewServeMux() + client := New(server) + assert.NotNil(t, client) +} + +func TestClient_Request(t *testing.T) { + type want struct { + code int + body string + } + tests := []struct { + name string + path string + method string + handlers []TestHandler + want want + }{ + { + name: "When request is GET", + path: "/get", + method: http.MethodGet, + handlers: []TestHandler{ + { + Path: "/get", + Method: http.MethodGet, + StatusCode: http.StatusOK, + Response: "ok", + }, + }, + want: want{ + code: http.StatusOK, + body: "ok", + }, + }, + { + name: "When request is POST", + path: "/post", + method: http.MethodPost, + handlers: []TestHandler{ + { + Path: "/post", + Method: http.MethodPost, + StatusCode: http.StatusOK, + Response: "ok", + }, + }, + want: want{ + code: http.StatusOK, + body: "ok", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createTestServer(t, tt.handlers) + client := New(server) + req := httptest.NewRequest(tt.method, tt.path, nil) + client.Request(req) + res := client.Response() + + assert.Equal(t, tt.want.code, res.StatusCode) + + body, _ := io.ReadAll(res.Body) + assert.Equal(t, tt.want.body, string(body)) + }) + } +} + +func TestClient_PostForm(t *testing.T) { + type want struct { + code int + body string + } + tests := []struct { + name string + path string + params map[string]string + handlers []TestHandler + want want + }{ + { + name: "When request has some parameters", + path: "/post", + params: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + handlers: []TestHandler{ + { + Path: "/post", + Method: http.MethodPost, + StatusCode: http.StatusOK, + Response: "ok", + Middleware: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + assert.Equal(t, "application/x-www-form-urlencoded", contentType) + + err := r.ParseForm() + assert.NoError(t, err) + + assert.Equal(t, "value1", r.PostFormValue("key1")) + assert.Equal(t, "value2", r.PostFormValue("key2")) + }, + }, + }, + want: want{ + code: http.StatusOK, + body: "ok", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createTestServer(t, tt.handlers) + client := New(server) + client.PostForm(tt.path, tt.params) + res := client.Response() + + assert.Equal(t, tt.want.code, res.StatusCode) + + body, _ := io.ReadAll(res.Body) + assert.Equal(t, tt.want.body, string(body)) + }) + } +} + +func TestClient_FollowRedirect(t *testing.T) { + type want struct { + code int + body string + } + tests := []struct { + name string + path string + params map[string]string + handlers []TestHandler + want want + wantErr bool + }{ + { + name: "When redirect with cookie", + path: "/redirect", + handlers: []TestHandler{ + { + Path: "/redirect", + Method: http.MethodGet, + StatusCode: http.StatusFound, + Middleware: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{ + Name: "cookie", + Value: "candy", + }) + http.Redirect(w, r, "/target", http.StatusFound) + }, + }, + { + Path: "/target", + Method: http.MethodGet, + Response: "ok", + StatusCode: http.StatusOK, + Middleware: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("cookie") + assert.NoError(t, err) + assert.Equal(t, "candy", cookie.Value) + }, + }, + }, + want: want{ + code: http.StatusOK, + body: "ok", + }, + wantErr: false, + }, + { + name: "When no redirect", + path: "/no-redirect", + handlers: []TestHandler{ + { + Path: "/no-redirect", + Method: http.MethodGet, + Response: "ok", + StatusCode: http.StatusOK, + }, + }, + want: want{}, + wantErr: true, + }, + { + name: "When no location header", + path: "/no-location", + handlers: []TestHandler{ + { + Path: "/no-location", + Method: http.MethodGet, + Response: "ok", + StatusCode: http.StatusFound, + }, + }, + want: want{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createTestServer(t, tt.handlers) + client := New(server) + req := httptest.NewRequest(http.MethodGet, tt.path, nil) + client.Request(req) + + err := client.FollowRedirect() + if tt.wantErr { + assert.Error(t, err) + return + } + + res := client.Response() + assert.Equal(t, tt.want.code, res.StatusCode) + + body, _ := io.ReadAll(res.Body) + assert.Equal(t, tt.want.body, string(body)) + }) + } +} diff --git a/go.mod b/go.mod index 39ed6f4..7bb143d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,13 @@ module github.com/raksul/go-testclient -go 1.21.0 +// Specify to the minimum version that is supported in testing. +// .github/workflows/test.yml +go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=