Skip to content

Commit 6c8641d

Browse files
Add ability to dynamically add filter JSON fields (and fix test coverage)
Signed-off-by: Peter Broadhurst <[email protected]>
1 parent 599b7b1 commit 6c8641d

File tree

10 files changed

+238
-25
lines changed

10 files changed

+238
-25
lines changed

pkg/ffapi/apiserver.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ func (as *apiServer[T]) Serve(ctx context.Context) (err error) {
202202
if err != nil {
203203
return err
204204
}
205-
as.monitoringPublicURL = buildPublicURL(as.MonitoringConfig, apiHTTPServer.Addr())
205+
as.monitoringPublicURL = buildPublicURL(as.MonitoringConfig, monitoringHTTPServer.Addr())
206206
go monitoringHTTPServer.ServeHTTP(ctx)
207207
}
208208

@@ -219,6 +219,10 @@ func (as *apiServer[T]) APIPublicURL() string {
219219
return as.apiPublicURL
220220
}
221221

222+
func (as *apiServer[T]) MonitoringPublicURL() string {
223+
return as.monitoringPublicURL
224+
}
225+
222226
func (as *apiServer[T]) waitForServerStop(httpErrChan, monitoringErrChan chan error) error {
223227
select {
224228
case err := <-httpErrChan:

pkg/ffapi/apiserver_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,26 @@ func TestAPIServerInvokeAPIRouteStream(t *testing.T) {
176176
assert.Equal(t, "a stream!", string(res.Body()))
177177
}
178178

179+
func TestAPIServerInvokeAPIRouteLiveness(t *testing.T) {
180+
_, as, done := newTestAPIServer(t, true)
181+
defer done()
182+
183+
<-as.Started()
184+
185+
res, err := resty.New().R().Get(fmt.Sprintf("%s/livez", as.MonitoringPublicURL()))
186+
assert.NoError(t, err)
187+
assert.Equal(t, 200, res.StatusCode()) // note this should really be 204, but not changing as would change behavior
188+
}
189+
190+
func TestAPIServerPanicsMisConfig(t *testing.T) {
191+
assert.Panics(t, func() {
192+
_ = NewAPIServer(context.Background(), APIServerOptions[any]{})
193+
})
194+
assert.Panics(t, func() {
195+
_ = NewAPIServer(context.Background(), APIServerOptions[any]{APIConfig: config.RootSection("any")})
196+
})
197+
}
198+
179199
func TestAPIServerInvokeAPIRouteJSON(t *testing.T) {
180200
um, as, done := newTestAPIServer(t, true)
181201
defer done()
@@ -342,6 +362,19 @@ func TestAPIServerInvokeEnrichFailForm(t *testing.T) {
342362
assert.Equal(t, 500, res.StatusCode())
343363
}
344364

365+
func TestAPIServerInvokeEnrichFailStream(t *testing.T) {
366+
um, as, done := newTestAPIServer(t, true)
367+
defer done()
368+
369+
um.mockEnrichErr = fmt.Errorf("pop")
370+
<-as.Started()
371+
372+
res, err := resty.New().R().
373+
Get(fmt.Sprintf("%s/api/v1/ut/utresource/id12345/getit", as.APIPublicURL()))
374+
assert.NoError(t, err)
375+
assert.Equal(t, 500, res.StatusCode())
376+
}
377+
345378
func TestAPIServer404(t *testing.T) {
346379
_, as, done := newTestAPIServer(t, true)
347380
defer done()
@@ -397,6 +430,45 @@ func TestAPIServerFailServeMonitoring(t *testing.T) {
397430

398431
}
399432

433+
func TestAPIServerFailServeMonitoringBadRouteSlash(t *testing.T) {
434+
_, as, done := newTestAPIServer(t, false)
435+
defer done()
436+
437+
as.MonitoringConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1:0")
438+
as.MonitoringRoutes = []*Route{
439+
{Path: "right", Extensions: &APIServerRouteExt[*utManager]{}},
440+
{Path: "/wrong"},
441+
}
442+
err := as.Serve(context.Background())
443+
assert.Regexp(t, "FF00255", err)
444+
445+
// Check we still closed the started channel
446+
<-as.Started()
447+
448+
}
449+
450+
func TestAPIServerFailServeBadRouteSlash(t *testing.T) {
451+
_, as, done := newTestAPIServer(t, false)
452+
defer done()
453+
454+
as.Routes = []*Route{
455+
{
456+
Path: "/wrong",
457+
Extensions: &APIServerRouteExt[*utManager]{
458+
JSONHandler: func(a *APIRequest, um *utManager) (output interface{}, err error) {
459+
return nil, nil
460+
},
461+
},
462+
},
463+
}
464+
err := as.Serve(context.Background())
465+
assert.Regexp(t, "FF00255", err)
466+
467+
// Check we still closed the started channel
468+
<-as.Started()
469+
470+
}
471+
400472
func TestWaitForServerStop(t *testing.T) {
401473
_, as, done := newTestAPIServer(t, false)
402474
defer done()
@@ -611,6 +683,7 @@ func TestVersionedAPIInitErrors(t *testing.T) {
611683
err = as.Serve(ctx)
612684
assert.Error(t, err)
613685
assert.Regexp(t, "FF00253", err)
686+
614687
as = NewAPIServer(ctx, APIServerOptions[*utManager]{
615688
MetricsRegistry: metric.NewPrometheusMetricsRegistry("ut"),
616689
VersionedAPIs: &VersionedAPIs{
@@ -633,4 +706,32 @@ func TestVersionedAPIInitErrors(t *testing.T) {
633706
assert.Error(t, err)
634707
assert.Regexp(t, "FF00254", err)
635708

709+
as = NewAPIServer(ctx, APIServerOptions[*utManager]{
710+
MetricsRegistry: metric.NewPrometheusMetricsRegistry("ut"),
711+
VersionedAPIs: &VersionedAPIs{
712+
DefaultVersion: "unknown",
713+
APIVersions: map[string]*APIVersion{
714+
"v1": {
715+
Routes: []*Route{
716+
{
717+
Path: "/wrong",
718+
Extensions: &APIServerRouteExt[*utManager]{
719+
JSONHandler: func(r *APIRequest, um *utManager) (output interface{}, err error) {
720+
return nil, nil
721+
},
722+
},
723+
},
724+
},
725+
},
726+
},
727+
},
728+
Description: "unit testing",
729+
APIConfig: apiConfig,
730+
MonitoringConfig: monitoringConfig,
731+
})
732+
733+
err = as.Serve(ctx)
734+
assert.Error(t, err)
735+
assert.Regexp(t, "FF00255", err)
736+
636737
}

pkg/ffapi/handler_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"net/url"
2929
"strings"
3030
"testing"
31+
"testing/iotest"
3132
"time"
3233

3334
"github.com/getkin/kin-openapi/openapi3"
@@ -727,3 +728,23 @@ func TestPOSTFormParamsMultiValueUnsupported(t *testing.T) {
727728
assert.NoError(t, err)
728729
assert.Equal(t, 400, res.StatusCode)
729730
}
731+
732+
func TestGetFormParamsFail(t *testing.T) {
733+
734+
hs := newTestHandlerFactory("", nil)
735+
req := httptest.NewRequest(http.MethodPost, "http://localhost:12345/anything", iotest.ErrReader(fmt.Errorf("pop")))
736+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
737+
_, err := hs.getFormParams(req)
738+
require.Regexp(t, "FF00250.*pop", err)
739+
}
740+
741+
func TestGetFormEmptyValue(t *testing.T) {
742+
743+
hs := newTestHandlerFactory("", nil)
744+
req := httptest.NewRequest(http.MethodPost, "http://localhost:12345/anything", nil)
745+
req.Form = url.Values{
746+
"nothing": []string{},
747+
}
748+
_, err := hs.getFormParams(req)
749+
require.NoError(t, err)
750+
}

pkg/ffapi/openapi3_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,21 @@ var testRoutes = []*Route{
178178
JSONOutputValue: func() interface{} { return &TestStruct1{} },
179179
JSONOutputCodes: []int{http.StatusOK},
180180
},
181+
{
182+
Name: "op7",
183+
Path: "example7",
184+
Method: http.MethodPut,
185+
PathParams: nil,
186+
QueryParams: nil,
187+
Description: ExampleDesc,
188+
JSONInputValue: func() interface{} { return &TestStruct1{} },
189+
JSONOutputValue: func() interface{} { return nil },
190+
JSONOutputCodes: []int{http.StatusNoContent},
191+
FormParams: []*FormParam{
192+
{Name: "metadata", Description: ExampleDesc},
193+
},
194+
Tag: example2TagName,
195+
},
181196
}
182197

183198
type TestInOutType struct {

pkg/ffapi/openapihandler.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,6 @@ func (ohf *OpenAPIHandlerFactory) getPublicURL(req *http.Request, relativePath s
9595
if publicURL == "" {
9696
publicURL = ohf.StaticPublicURL
9797
}
98-
if publicURL == "" {
99-
// Do not recommend this fallback - StaticPublicURL and/or DynamicPublicURLHeader should be set
100-
publicURL = fmt.Sprintf("https://%s", req.Host)
101-
}
10298
publicURL = strings.TrimSuffix(publicURL, "/")
10399
if len(relativePath) > 0 {
104100
publicURL = publicURL + "/" + strings.TrimPrefix(relativePath, "/")

pkg/ffapi/openapihandler_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
"testing"
2525

2626
"github.com/go-resty/resty/v2"
27+
"github.com/gorilla/mux"
2728
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
2830
)
2931

3032
func TestOpenAPI3SwaggerUI(t *testing.T) {
@@ -98,3 +100,28 @@ func TestOpenAPI3SwaggerUIDynamicPublicURL(t *testing.T) {
98100
assert.NoError(t, err)
99101
assert.Contains(t, string(b), "https://example.host.com:12345/api/myswagger.json")
100102
}
103+
104+
func TestOpenAPIHandlerNonVersioned(t *testing.T) {
105+
mux := mux.NewRouter()
106+
hf := HandlerFactory{}
107+
oah := &OpenAPIHandlerFactory{
108+
BaseSwaggerGenOptions: SwaggerGenOptions{
109+
Title: "FireFly Transaction Manager API",
110+
Version: "1.0",
111+
SupportFieldRedaction: true,
112+
},
113+
}
114+
mux.Path("/api/spec.json").Methods(http.MethodGet).Handler(hf.APIWrapper(oah.OpenAPIHandler("", OpenAPIFormatJSON, []*Route{})))
115+
mux.Path("/api/spec.yaml").Methods(http.MethodGet).Handler(hf.APIWrapper(oah.OpenAPIHandler("", OpenAPIFormatYAML, []*Route{})))
116+
117+
ts := httptest.NewServer(mux)
118+
defer ts.Close()
119+
120+
res, err := resty.New().R().Get(fmt.Sprintf("%s/api/spec.json", ts.URL))
121+
require.NoError(t, err)
122+
require.True(t, res.IsSuccess())
123+
124+
res, err = resty.New().R().Get(fmt.Sprintf("%s/api/spec.yaml", ts.URL))
125+
require.NoError(t, err)
126+
require.True(t, res.IsSuccess())
127+
}

pkg/ffapi/query_fields.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ type QueryFactory interface {
3838
NewFilter(ctx context.Context) FilterBuilder
3939
NewFilterLimit(ctx context.Context, defLimit uint64) FilterBuilder
4040
NewUpdate(ctx context.Context) UpdateBuilder
41+
Clone() ClonedQueryFactory
42+
}
43+
44+
type ClonedQueryFactory interface {
45+
QueryFactory
46+
AddField(n string, f Field)
4147
}
4248

4349
type FieldMod int
@@ -53,23 +59,35 @@ type HasFieldMods interface {
5359

5460
type QueryFields map[string]Field
5561

56-
func (qf *QueryFields) NewFilterLimit(ctx context.Context, defLimit uint64) FilterBuilder {
62+
func (qf QueryFields) NewFilterLimit(ctx context.Context, defLimit uint64) FilterBuilder {
5763
return &filterBuilder{
5864
ctx: ctx,
59-
queryFields: *qf,
65+
queryFields: qf,
6066
limit: defLimit,
6167
}
6268
}
6369

64-
func (qf *QueryFields) NewFilter(ctx context.Context) FilterBuilder {
70+
func (qf QueryFields) NewFilter(ctx context.Context) FilterBuilder {
6571
return qf.NewFilterLimit(ctx, 0)
6672
}
6773

68-
func (qf *QueryFields) NewUpdate(ctx context.Context) UpdateBuilder {
74+
func (qf QueryFields) NewUpdate(ctx context.Context) UpdateBuilder {
6975
return &updateBuilder{
7076
ctx: ctx,
71-
queryFields: *qf,
77+
queryFields: qf,
78+
}
79+
}
80+
81+
func (qf QueryFields) AddField(n string, f Field) {
82+
qf[n] = f
83+
}
84+
85+
func (qf QueryFields) Clone() ClonedQueryFactory {
86+
qf2 := make(QueryFields, len(qf))
87+
for n, f := range qf {
88+
qf2[n] = f
7289
}
90+
return qf2
7391
}
7492

7593
// FieldSerialization - we stand on the shoulders of the well adopted SQL serialization interface here to help us define what
@@ -284,17 +302,11 @@ func (f *bigIntField) Scan(src interface{}) (err error) {
284302
case int64:
285303
f.i = fftypes.NewFFBigInt(tv)
286304
case uint:
287-
if tv > math.MaxInt64 {
288-
return i18n.NewError(context.Background(), i18n.MsgTypeRestoreFailed, src, f.i)
289-
}
290-
f.i = fftypes.NewFFBigInt(int64(tv))
305+
f.i = (*fftypes.FFBigInt)(new(big.Int).SetUint64(uint64(tv)))
291306
case uint32:
292307
f.i = fftypes.NewFFBigInt(int64(tv))
293308
case uint64:
294-
if tv > math.MaxInt64 {
295-
return i18n.NewError(context.Background(), i18n.MsgTypeRestoreFailed, src, f.i)
296-
}
297-
f.i = fftypes.NewFFBigInt(int64(tv))
309+
f.i = (*fftypes.FFBigInt)(new(big.Int).SetUint64(tv))
298310
case fftypes.FFBigInt:
299311
i := tv
300312
f.i = &i

pkg/ffapi/query_fields_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package ffapi
1818

1919
import (
20+
"fmt"
21+
"math"
2022
"testing"
2123
"time"
2224

@@ -138,6 +140,15 @@ func TestInt64Field(t *testing.T) {
138140
assert.NoError(t, err)
139141
assert.Equal(t, int64(0), v)
140142

143+
err = f.Scan(uint64(math.MaxInt64 + 1))
144+
assert.Regexp(t, "FF00105", err)
145+
146+
err = f.Scan(uint(math.MaxInt64 + 1))
147+
assert.Regexp(t, "FF00105", err)
148+
149+
err = f.Scan(fmt.Sprintf("%d", uint64(math.MaxInt64+1)))
150+
assert.Regexp(t, "FF00105", err)
151+
141152
}
142153

143154
func TestBigIntField(t *testing.T) {

0 commit comments

Comments
 (0)