feat: api - list versions/get version - add tests (#64)

Reviewed-on: twhelp/corev3#64
This commit is contained in:
Dawid Wysokiński 2024-02-24 06:57:09 +00:00
parent a8b8ffea58
commit 48125dd50a
55 changed files with 8017 additions and 10 deletions

View File

@ -927,10 +927,14 @@ components:
self:
description: Cursor pointing to the current page.
type: string
minLength: 1
maxLength: 1000
x-go-type-skip-optional-pointer: true
next:
description: Cursor pointing to the next page.
type: string
minLength: 1
maxLength: 1000
x-go-type-skip-optional-pointer: true
PaginationResponse:
type: object
@ -943,13 +947,15 @@ components:
name: cursor
schema:
type: string
minLength: 1
maxLength: 1000
required: false
LimitQueryParam:
in: query
name: limit
schema:
type: integer
minimum: 0
minimum: 1
required: false
ServerOpenQueryParam:
name: open

View File

@ -1,4 +1,4 @@
package service_test
package port_test
import (
"context"

View File

@ -1,4 +1,4 @@
package service_test
package port_test
import (
"context"

View File

@ -1,4 +1,4 @@
package service_test
package port_test
import (
"context"

View File

@ -63,7 +63,7 @@ func TestOpenAPI(t *testing.T) {
}
})
t.Run("OK: Swagger", func(t *testing.T) {
t.Run("OK: swagger", func(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t, func(cfg *apiHTTPHandlerConfig) {

View File

@ -1,15 +1,19 @@
package port_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter"
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
"gitea.dwysokinski.me/twhelp/corev3/internal/bun/buntest"
"gitea.dwysokinski.me/twhelp/corev3/internal/port"
"github.com/stretchr/testify/require"
)
type apiHTTPHandlerConfig struct {
@ -25,11 +29,12 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
opt(cfg)
}
db := buntest.NewSQLiteDB(tb)
bunDB := buntest.NewSQLiteDB(tb)
buntest.NewFixture(bunDB).Load(tb, context.Background(), os.DirFS("testdata"), "api/fixture.yml")
versionRepo := adapter.NewVersionBunRepository(db)
serverRepo := adapter.NewServerBunRepository(db)
tribeRepo := adapter.NewTribeBunRepository(db)
versionRepo := adapter.NewVersionBunRepository(bunDB)
serverRepo := adapter.NewServerBunRepository(bunDB)
tribeRepo := adapter.NewTribeBunRepository(bunDB)
return port.NewAPIHTTPHandler(
app.NewVersionService(versionRepo),
@ -48,3 +53,10 @@ func doCustomRequest(h http.Handler, r *http.Request) *http.Response {
h.ServeHTTP(rr, r)
return rr.Result()
}
func decodeJSON[T any](tb testing.TB, r io.Reader) T {
tb.Helper()
var res T
require.NoError(tb, json.NewDecoder(r).Decode(&res))
return res
}

View File

@ -0,0 +1,399 @@
package port_test
import (
"cmp"
"fmt"
"net/http"
"net/http/httptest"
"slices"
"strconv"
"strings"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListVersions(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t)
tests := []struct {
name string
reqModifier func(t *testing.T, req *http.Request)
assertResp func(t *testing.T, req *http.Request, resp *http.Response)
}{
{
name: "OK: without params",
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListVersionsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Version) int {
return cmp.Compare(a.Code, b.Code)
}))
},
},
{
name: "OK: limit=3",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "3")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListVersionsResponse](t, resp.Body)
assert.NotZero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: limit=3 cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "3")
req.URL.RawQuery = q.Encode()
resp := doCustomRequest(handler, req.Clone(req.Context()))
defer resp.Body.Close()
body := decodeJSON[apimodel.ListVersionsResponse](t, resp.Body)
require.NotEmpty(t, body.Cursor.Next)
q.Set("cursor", body.Cursor.Next)
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListVersionsResponse](t, resp.Body)
assert.NotZero(t, body.Cursor.Next)
assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "ERR: limit is not a string",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "asd")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
Message: fmt.Sprintf(
"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
req.URL.Query().Get("limit"),
),
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: limit < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "0")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
Min: 1,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: limit > 200",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "201")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
Max: 200,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", "")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) > 1000",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(1001))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: invalid cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(100))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
var domainErr domain.Error
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/v2/versions", nil)
if tt.reqModifier != nil {
tt.reqModifier(t, req)
}
resp := doCustomRequest(handler, req)
defer resp.Body.Close()
tt.assertResp(t, req, resp)
})
}
}
func TestGetVersion(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t)
version := randVersion(t, handler)
tests := []struct {
name string
reqModifier func(t *testing.T, req *http.Request)
assertResp func(t *testing.T, req *http.Request, resp *http.Response)
}{
{
name: "OK",
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.GetVersionResponse](t, resp.Body)
assert.Equal(t, version, body.Data)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = "/v2/versions/asdf"
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 4)
domainErr := domain.VersionNotFoundError{
VersionCode: pathSegments[3],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"code": domainErr.VersionCode,
},
},
},
}, body)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/v2/versions/"+version.Code, nil)
if tt.reqModifier != nil {
tt.reqModifier(t, req)
}
resp := doCustomRequest(handler, req)
defer resp.Body.Close()
tt.assertResp(t, req, resp)
})
}
}
func randVersion(tb testing.TB, h http.Handler) apimodel.Version {
tb.Helper()
resp := doRequest(h, http.MethodGet, "/v2/versions", nil)
defer resp.Body.Close()
require.Equal(tb, http.StatusOK, resp.StatusCode)
body := decodeJSON[apimodel.ListVersionsResponse](tb, resp.Body)
require.NotEmpty(tb, body.Data)
return body.Data[gofakeit.IntRange(0, len(body.Data)-1)]
}

View File

@ -1,4 +1,4 @@
package service_test
package port_test
import (
"flag"

7590
internal/port/testdata/api/fixture.yml vendored Normal file

File diff suppressed because it is too large Load Diff