Skip to content

Commit 06c3c58

Browse files
authored
Merge pull request #33 from calvinmclean/feature/testing-new
Add new testing utilities
2 parents 87ec1f1 + 02e85b9 commit 06c3c58

14 files changed

+715
-280
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
go-version: "1.21"
3737

3838
- name: Test
39-
run: go test -short -race -covermode=atomic -coverprofile=coverage.out -coverpkg=.,./storage ./...
39+
run: go test -short -race -covermode=atomic -coverprofile=coverage.out -coverpkg=.,./storage,./test ./...
4040

4141
- name: Upload coverage reports to Codecov
4242
uses: codecov/codecov-action@v3

babyapi.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type API[T Resource] struct {
2020
name string
2121
base string
2222

23-
subAPIs map[string]RelatedAPI
23+
subAPIs map[string]relatedAPI
2424
middlewares []func(http.Handler) http.Handler
2525
idMiddlewares []func(http.Handler) http.Handler
2626

@@ -50,10 +50,10 @@ type API[T Resource] struct {
5050

5151
onCreateOrUpdate func(*http.Request, T) *ErrResponse
5252

53-
parent RelatedAPI
53+
parent relatedAPI
5454

55-
customResponseCodes map[string]int
56-
serverCtx context.Context
55+
responseCodes map[string]int
56+
serverCtx context.Context
5757

5858
// GetAll is the handler for /base and returns an array of resources
5959
GetAll http.HandlerFunc
@@ -82,7 +82,7 @@ func NewAPI[T Resource](name, base string, instance func() T) *API[T] {
8282
api := &API[T]{
8383
name,
8484
base,
85-
map[string]RelatedAPI{},
85+
map[string]relatedAPI{},
8686
nil,
8787
nil,
8888
MapStorage[T]{},
@@ -99,7 +99,7 @@ func NewAPI[T Resource](name, base string, instance func() T) *API[T] {
9999
defaultBeforeAfter,
100100
func(*http.Request, T) *ErrResponse { return nil },
101101
nil,
102-
map[string]int{},
102+
defaultResponseCodes(),
103103
nil,
104104
nil,
105105
nil,
@@ -149,7 +149,7 @@ func (a *API[T]) Name() string {
149149

150150
// SetCustomResponseCode will override the default response codes for the specified HTTP verb
151151
func (a *API[T]) SetCustomResponseCode(verb string, code int) *API[T] {
152-
a.customResponseCodes[verb] = code
152+
a.responseCodes[verb] = code
153153
return a
154154
}
155155

@@ -206,12 +206,14 @@ func (a *API[T]) SetResponseWrapper(responseWrapper func(T) render.Renderer) *AP
206206

207207
// Client returns a new Client based on the API's configuration. It is a shortcut for NewClient
208208
func (a *API[T]) Client(addr string) *Client[T] {
209-
return NewClient[T](addr, makePathWithRoot(a.base, a.parent))
209+
return NewClient[T](addr, makePathWithRoot(a.base, a.parent)).
210+
SetCustomResponseCodeMap(a.responseCodes)
210211
}
211212

212213
// AnyClient returns a new Client based on the API's configuration. It is a shortcut for NewClient
213214
func (a *API[T]) AnyClient(addr string) *Client[*AnyResource] {
214-
return NewClient[*AnyResource](addr, makePathWithRoot(a.base, a.parent))
215+
return NewClient[*AnyResource](addr, makePathWithRoot(a.base, a.parent)).
216+
SetCustomResponseCodeMap(a.responseCodes)
215217
}
216218

217219
// AddCustomRootRoute appends a custom API route to the absolute root path ("/"). It does not work for APIs with

babyapi_test.go

+13-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
"github.com/calvinmclean/babyapi"
18+
babytest "github.com/calvinmclean/babyapi/test"
1819
"github.com/go-chi/chi/v5"
1920
"github.com/go-chi/render"
2021
"github.com/rs/xid"
@@ -194,12 +195,13 @@ func TestBabyAPI(t *testing.T) {
194195

195196
t.Run("DeleteAlbum", func(t *testing.T) {
196197
t.Run("Successful", func(t *testing.T) {
197-
err := client.Delete(context.Background(), album1.GetID())
198+
resp, err := client.Delete(context.Background(), album1.GetID())
198199
require.NoError(t, err)
200+
require.Equal(t, http.NoBody, resp.Response.Body)
199201
})
200202

201203
t.Run("NotFound", func(t *testing.T) {
202-
err := client.Delete(context.Background(), album1.GetID())
204+
_, err := client.Delete(context.Background(), album1.GetID())
203205
require.Error(t, err)
204206
require.Equal(t, "error deleting resource: unexpected response with text: Resource not found.", err.Error())
205207
})
@@ -285,7 +287,7 @@ func TestNestedAPI(t *testing.T) {
285287
artistAPI.AddNestedAPI(albumAPI).AddNestedAPI(musicVideoAPI)
286288
albumAPI.AddNestedAPI(songAPI)
287289

288-
serverURL, stop := babyapi.TestServe[*Artist](t, artistAPI)
290+
serverURL, stop := babytest.TestServe[*Artist](t, artistAPI)
289291
defer stop()
290292

291293
artist1 := &Artist{Name: "Artist1"}
@@ -508,7 +510,7 @@ func TestCLI(t *testing.T) {
508510
{
509511
"Delete",
510512
[]string{"delete", "Albums", "cljcqg5o402e9s28rbp0"},
511-
`null`,
513+
``,
512514
false,
513515
},
514516
{
@@ -664,7 +666,7 @@ func TestHTML(t *testing.T) {
664666
Content: "Item1",
665667
}
666668

667-
address, closer := babyapi.TestServe[*ListItem](t, api)
669+
address, closer := babytest.TestServe[*ListItem](t, api)
668670
defer closer()
669671

670672
client := api.Client(address)
@@ -718,7 +720,7 @@ func TestServerSentEvents(t *testing.T) {
718720

719721
events := api.AddServerSentEventHandler("/events")
720722

721-
address, closer := babyapi.TestServe[*ListItem](t, api)
723+
address, closer := babytest.TestServe[*ListItem](t, api)
722724
defer closer()
723725

724726
item1 := &ListItem{
@@ -813,23 +815,23 @@ func TestAPIModifiers(t *testing.T) {
813815
require.NoError(t, err)
814816
r.Header.Add("Content-Type", "application/json")
815817

816-
w := babyapi.Test[*Album](t, api, r)
818+
w := babytest.TestRequest[*Album](t, api, r)
817819
require.Equal(t, http.StatusTeapot, w.Result().StatusCode)
818820
})
819821

820822
t.Run("DeleteResource", func(t *testing.T) {
821823
r, err := http.NewRequest(http.MethodDelete, "/albums/"+albumID, http.NoBody)
822824
require.NoError(t, err)
823825

824-
w := babyapi.Test[*Album](t, api, r)
826+
w := babytest.TestRequest[*Album](t, api, r)
825827
require.Equal(t, http.StatusNoContent, w.Result().StatusCode)
826828
})
827829

828830
t.Run("GetResourceNotFound", func(t *testing.T) {
829831
r, err := http.NewRequest(http.MethodGet, "/albums/DoesNotExist", http.NoBody)
830832
require.NoError(t, err)
831833

832-
w := babyapi.Test[*Album](t, api, r)
834+
w := babytest.TestRequest[*Album](t, api, r)
833835
require.Equal(t, http.StatusNotFound, w.Result().StatusCode)
834836
})
835837

@@ -919,7 +921,7 @@ func TestRootAPIWithMiddlewareAndCustomHandlers(t *testing.T) {
919921
for _, tt := range tests {
920922
t.Run(tt.method+tt.path, func(t *testing.T) {
921923
r := httptest.NewRequest(tt.method, tt.path, http.NoBody)
922-
w := babyapi.Test[*babyapi.NilResource](t, api, r)
924+
w := babytest.TestRequest[*babyapi.NilResource](t, api, r)
923925

924926
require.Equal(t, tt.expectedStatus, w.Result().StatusCode)
925927
})
@@ -1080,7 +1082,7 @@ func TestRootAPICLI(t *testing.T) {
10801082
{
10811083
"Delete",
10821084
[]string{"delete", "MusicVideos", "cljcqg5o402e9s28rbp0"},
1083-
`null`,
1085+
``,
10841086
false,
10851087
},
10861088
{

cli.go

+23-18
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,32 @@ func (a *API[T]) RunWithArgs(out io.Writer, args []string, bindAddress string, a
6262
return a.runClientCLI(out, args, address, pretty, headers, query)
6363
}
6464

65-
func (a *API[T]) buildClientMap(selfClient *Client[*AnyResource], clientMap map[string]*Client[*AnyResource], reqEditor func(*http.Request) error) {
66-
for name, child := range a.subAPIs {
67-
base := makePathWithRoot(child.Base(), a)
65+
// CreateClientMap returns a map of API names to the corresponding Client for that child API. This makes it easy to use
66+
// child APIs dynamically. The initial parent/base client must be provided so child APIs can use NewSubClient
67+
func (a *API[T]) CreateClientMap(parent *Client[*AnyResource]) map[string]*Client[*AnyResource] {
68+
clientMap := map[string]*Client[*AnyResource]{}
69+
if !a.rootAPI {
70+
clientMap[a.name] = parent
71+
}
6872

73+
for _, child := range a.subAPIs {
74+
base := makePathWithRoot(child.Base(), a)
6975
var childClient *Client[*AnyResource]
76+
7077
if a.rootAPI && a.parent == nil {
7178
// If the current API is a root API and has no parent, then this client has no need for parent IDs
72-
childClient = NewClient[*AnyResource](selfClient.addr, base)
79+
childClient = NewClient[*AnyResource](parent.addr, base)
7380
} else {
74-
childClient = NewSubClient[*AnyResource, *AnyResource](selfClient, base)
81+
childClient = NewSubClient[*AnyResource, *AnyResource](parent, base)
7582
}
7683

77-
childClient.SetRequestEditor(reqEditor)
78-
clientMap[name] = childClient
79-
child.buildClientMap(childClient, clientMap, reqEditor)
84+
childMap := child.CreateClientMap(childClient)
85+
for n, c := range childMap {
86+
clientMap[n] = c
87+
}
8088
}
89+
90+
return clientMap
8191
}
8292

8393
func (a *API[T]) runClientCLI(out io.Writer, args []string, address string, pretty bool, headers []string, query string) error {
@@ -107,21 +117,16 @@ func (a *API[T]) runClientCLI(out io.Writer, args []string, address string, pret
107117
return nil
108118
}
109119

110-
selfClient := a.AnyClient(address)
111-
selfClient.SetRequestEditor(reqEditor)
112-
113-
clientMap := map[string]*Client[*AnyResource]{}
114-
if !a.rootAPI {
115-
clientMap[a.name] = selfClient
116-
}
117-
a.buildClientMap(selfClient, clientMap, reqEditor)
120+
clientMap := a.CreateClientMap(a.AnyClient(address))
118121

119122
targetAPI := args[1]
120123
client, ok := clientMap[targetAPI]
121124
if !ok {
122125
return fmt.Errorf("invalid API %q. valid options are: %v", targetAPI, maps.Keys[map[string]*Client[*AnyResource]](clientMap))
123126
}
124127

128+
client.SetRequestEditor(reqEditor)
129+
125130
var cmd func([]string, *Client[*AnyResource]) (*Response[*AnyResource], error)
126131
switch args[0] {
127132
case "get":
@@ -171,12 +176,12 @@ func (a *API[T]) runDeleteCommand(args []string, client *Client[*AnyResource]) (
171176
if len(args) < 1 {
172177
return nil, fmt.Errorf("at least one argument required")
173178
}
174-
err := client.Delete(context.Background(), args[0], args[1:]...)
179+
result, err := client.Delete(context.Background(), args[0], args[1:]...)
175180
if err != nil {
176181
return nil, fmt.Errorf("error running Delete: %w", err)
177182
}
178183

179-
return nil, nil
184+
return result, nil
180185
}
181186

182187
func (a *API[T]) runListCommand(args []string, client *Client[*AnyResource]) (*Response[*ResourceList[*AnyResource]], error) {

0 commit comments

Comments
 (0)