Skip to content

Commit fa03f8f

Browse files
authored
Merge pull request #185 from hyperledger/filter-ext
Add ability to dynamically add filter JSON fields (and test coverage)
2 parents 599b7b1 + 91dda9f commit fa03f8f

15 files changed

+412
-42
lines changed

pkg/ffapi/apiserver.go

Lines changed: 8 additions & 5 deletions
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:
@@ -382,9 +386,8 @@ func (as *apiServer[T]) notFoundHandler(res http.ResponseWriter, req *http.Reque
382386
return 404, i18n.NewError(req.Context(), i18n.Msg404NotFound)
383387
}
384388

385-
func (as *apiServer[T]) emptyJSONHandler(res http.ResponseWriter, _ *http.Request) (status int, err error) {
386-
res.Header().Add("Content-Type", "application/json")
387-
return 200, nil
389+
func (as *apiServer[T]) noContentResponder(res http.ResponseWriter, _ *http.Request) {
390+
res.WriteHeader(http.StatusNoContent)
388391
}
389392

390393
func (as *apiServer[T]) createMonitoringMuxRouter(ctx context.Context) (*mux.Router, error) {
@@ -396,7 +399,7 @@ func (as *apiServer[T]) createMonitoringMuxRouter(ctx context.Context) (*mux.Rou
396399
panic(err)
397400
}
398401
r.Path(as.metricsPath).Handler(h)
399-
r.HandleFunc(as.livenessPath, hf.APIWrapper(as.emptyJSONHandler))
402+
r.HandleFunc(as.livenessPath, as.noContentResponder)
400403

401404
for _, route := range as.MonitoringRoutes {
402405
path := route.Path

pkg/ffapi/apiserver_test.go

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package ffapi
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"io"
2324
"net/http"
@@ -112,6 +113,29 @@ var utAPIRoute2 = &Route{
112113
},
113114
}
114115

116+
type testInputStruct struct {
117+
Input1 string `json:"input1,omitempty"`
118+
}
119+
120+
var utAPIRoute3 = &Route{
121+
Name: "utAPIRoute3",
122+
Path: "ut/utresource/{resourceid}/postbatch",
123+
Method: http.MethodPost,
124+
Description: "post an array to check arrays go through ok",
125+
JSONInputDecoder: func(req *http.Request, body io.Reader) (interface{}, error) {
126+
var arrayInput []*testInputStruct
127+
err := json.NewDecoder(body).Decode(&arrayInput)
128+
return arrayInput, err
129+
},
130+
JSONInputValue: func() interface{} { return []*testInputStruct{} },
131+
JSONOutputValue: func() interface{} { return []*testInputStruct{} },
132+
Extensions: &APIServerRouteExt[*utManager]{
133+
JSONHandler: func(a *APIRequest, um *utManager) (output interface{}, err error) {
134+
return a.Input.([]*testInputStruct), nil
135+
},
136+
},
137+
}
138+
115139
func initUTConfig() (config.Section, config.Section, config.Section) {
116140
config.RootConfigReset()
117141
apiConfig := config.RootSection("ut.api")
@@ -129,7 +153,7 @@ func newTestAPIServer(t *testing.T, start bool) (*utManager, *apiServer[*utManag
129153
um := &utManager{t: t}
130154
as := NewAPIServer(ctx, APIServerOptions[*utManager]{
131155
MetricsRegistry: metric.NewPrometheusMetricsRegistry("ut"),
132-
Routes: []*Route{utAPIRoute1, utAPIRoute2},
156+
Routes: []*Route{utAPIRoute1, utAPIRoute2, utAPIRoute3},
133157
EnrichRequest: func(r *APIRequest) (*utManager, error) {
134158
// This could be some dynamic object based on extra processing in the request,
135159
// but the most common case is you just have a "manager" that you inject into each
@@ -176,6 +200,50 @@ func TestAPIServerInvokeAPIRouteStream(t *testing.T) {
176200
assert.Equal(t, "a stream!", string(res.Body()))
177201
}
178202

203+
func TestAPIServerInvokeAPIPostEmptyArray(t *testing.T) {
204+
_, as, done := newTestAPIServer(t, true)
205+
defer done()
206+
207+
<-as.Started()
208+
209+
var o []*testInputStruct
210+
res, err := resty.New().R().
211+
SetBody([]*testInputStruct{}).
212+
SetResult(&o).
213+
Post(fmt.Sprintf("%s/api/v1/ut/utresource/id12345/postbatch", as.APIPublicURL()))
214+
assert.NoError(t, err)
215+
assert.Equal(t, 200, res.StatusCode())
216+
assert.Equal(t, []*testInputStruct{}, o)
217+
218+
res, err = resty.New().R().
219+
SetBody([]*testInputStruct{{Input1: "in1"}}).
220+
SetResult(&o).
221+
Post(fmt.Sprintf("%s/api/v1/ut/utresource/id12345/postbatch", as.APIPublicURL()))
222+
assert.NoError(t, err)
223+
assert.Equal(t, 200, res.StatusCode())
224+
assert.Equal(t, []*testInputStruct{{Input1: "in1"}}, o)
225+
}
226+
227+
func TestAPIServerInvokeAPIRouteLiveness(t *testing.T) {
228+
_, as, done := newTestAPIServer(t, true)
229+
defer done()
230+
231+
<-as.Started()
232+
233+
res, err := resty.New().R().Get(fmt.Sprintf("%s/livez", as.MonitoringPublicURL()))
234+
assert.NoError(t, err)
235+
assert.Equal(t, 204, res.StatusCode())
236+
}
237+
238+
func TestAPIServerPanicsMisConfig(t *testing.T) {
239+
assert.Panics(t, func() {
240+
_ = NewAPIServer(context.Background(), APIServerOptions[any]{})
241+
})
242+
assert.Panics(t, func() {
243+
_ = NewAPIServer(context.Background(), APIServerOptions[any]{APIConfig: config.RootSection("any")})
244+
})
245+
}
246+
179247
func TestAPIServerInvokeAPIRouteJSON(t *testing.T) {
180248
um, as, done := newTestAPIServer(t, true)
181249
defer done()
@@ -342,6 +410,19 @@ func TestAPIServerInvokeEnrichFailForm(t *testing.T) {
342410
assert.Equal(t, 500, res.StatusCode())
343411
}
344412

413+
func TestAPIServerInvokeEnrichFailStream(t *testing.T) {
414+
um, as, done := newTestAPIServer(t, true)
415+
defer done()
416+
417+
um.mockEnrichErr = fmt.Errorf("pop")
418+
<-as.Started()
419+
420+
res, err := resty.New().R().
421+
Get(fmt.Sprintf("%s/api/v1/ut/utresource/id12345/getit", as.APIPublicURL()))
422+
assert.NoError(t, err)
423+
assert.Equal(t, 500, res.StatusCode())
424+
}
425+
345426
func TestAPIServer404(t *testing.T) {
346427
_, as, done := newTestAPIServer(t, true)
347428
defer done()
@@ -397,6 +478,45 @@ func TestAPIServerFailServeMonitoring(t *testing.T) {
397478

398479
}
399480

481+
func TestAPIServerFailServeMonitoringBadRouteSlash(t *testing.T) {
482+
_, as, done := newTestAPIServer(t, false)
483+
defer done()
484+
485+
as.MonitoringConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1:0")
486+
as.MonitoringRoutes = []*Route{
487+
{Path: "right", Extensions: &APIServerRouteExt[*utManager]{}},
488+
{Path: "/wrong"},
489+
}
490+
err := as.Serve(context.Background())
491+
assert.Regexp(t, "FF00255", err)
492+
493+
// Check we still closed the started channel
494+
<-as.Started()
495+
496+
}
497+
498+
func TestAPIServerFailServeBadRouteSlash(t *testing.T) {
499+
_, as, done := newTestAPIServer(t, false)
500+
defer done()
501+
502+
as.Routes = []*Route{
503+
{
504+
Path: "/wrong",
505+
Extensions: &APIServerRouteExt[*utManager]{
506+
JSONHandler: func(a *APIRequest, um *utManager) (output interface{}, err error) {
507+
return nil, nil
508+
},
509+
},
510+
},
511+
}
512+
err := as.Serve(context.Background())
513+
assert.Regexp(t, "FF00255", err)
514+
515+
// Check we still closed the started channel
516+
<-as.Started()
517+
518+
}
519+
400520
func TestWaitForServerStop(t *testing.T) {
401521
_, as, done := newTestAPIServer(t, false)
402522
defer done()
@@ -611,6 +731,7 @@ func TestVersionedAPIInitErrors(t *testing.T) {
611731
err = as.Serve(ctx)
612732
assert.Error(t, err)
613733
assert.Regexp(t, "FF00253", err)
734+
614735
as = NewAPIServer(ctx, APIServerOptions[*utManager]{
615736
MetricsRegistry: metric.NewPrometheusMetricsRegistry("ut"),
616737
VersionedAPIs: &VersionedAPIs{
@@ -633,4 +754,32 @@ func TestVersionedAPIInitErrors(t *testing.T) {
633754
assert.Error(t, err)
634755
assert.Regexp(t, "FF00254", err)
635756

757+
as = NewAPIServer(ctx, APIServerOptions[*utManager]{
758+
MetricsRegistry: metric.NewPrometheusMetricsRegistry("ut"),
759+
VersionedAPIs: &VersionedAPIs{
760+
DefaultVersion: "unknown",
761+
APIVersions: map[string]*APIVersion{
762+
"v1": {
763+
Routes: []*Route{
764+
{
765+
Path: "/wrong",
766+
Extensions: &APIServerRouteExt[*utManager]{
767+
JSONHandler: func(r *APIRequest, um *utManager) (output interface{}, err error) {
768+
return nil, nil
769+
},
770+
},
771+
},
772+
},
773+
},
774+
},
775+
},
776+
Description: "unit testing",
777+
APIConfig: apiConfig,
778+
MonitoringConfig: monitoringConfig,
779+
})
780+
781+
err = as.Serve(ctx)
782+
assert.Error(t, err)
783+
assert.Regexp(t, "FF00255", err)
784+
636785
}

pkg/ffapi/handler.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ func (hs *HandlerFactory) RouteHandler(route *Route) http.HandlerFunc {
216216
req.Header.Set("Content-Type", "application/json; charset=utf8")
217217
fallthrough
218218
case strings.HasPrefix(strings.ToLower(contentType), "application/json"):
219-
if jsonInput != nil {
219+
if route.JSONInputDecoder != nil {
220+
jsonInput, err = route.JSONInputDecoder(req, req.Body)
221+
} else if jsonInput != nil {
220222
d := json.NewDecoder(req.Body)
221223
d.UseNumber()
222224
err = d.Decode(&jsonInput)

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+
}

0 commit comments

Comments
 (0)