From 6c36983ce0ba576990b8ca89c65d4bf539698b2c Mon Sep 17 00:00:00 2001 From: Rafa Date: Fri, 26 Aug 2022 16:41:35 +0100 Subject: [PATCH] Add client for table renderer --- README.md | 1 + tablerenderer/client.go | 105 +++++++++++++++++++++++++++++ tablerenderer/client_test.go | 125 +++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 tablerenderer/client.go create mode 100644 tablerenderer/client_test.go diff --git a/README.md b/README.md index 719442c6..84038c01 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Common client code - in go - for ONS APIs: * releasecalendar * renderer * search +* table-renderer * upload (Static Files) ## Usage diff --git a/tablerenderer/client.go b/tablerenderer/client.go new file mode 100644 index 00000000..84f07a48 --- /dev/null +++ b/tablerenderer/client.go @@ -0,0 +1,105 @@ +package tablerenderer + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + + healthcheck "github.com/ONSdigital/dp-api-clients-go/v2/health" + health "github.com/ONSdigital/dp-healthcheck/healthcheck" + "github.com/ONSdigital/log.go/v2/log" +) + +const service = "table-renderer" + +// Client represents a dp-table-renderer client +type Client struct { + hcCli *healthcheck.Client +} + +// ErrInvalidTableRendererResponse is returned when the table-renderer service does not respond with a status 200 +type ErrInvalidTableRendererResponse struct { + responseCode int +} + +// Error should be called by the user to print out the stringified version of the error +func (e ErrInvalidTableRendererResponse) Error() string { + return fmt.Sprintf("invalid response from table-renderer service - status %d", e.responseCode) +} + +// Code returns the status code received from table-renderer if an error is returned +func (e ErrInvalidTableRendererResponse) Code() int { + return e.responseCode +} + +// New creates a new instance of Client with a given table-renderer url +func New(tableRendererURL string) *Client { + return &Client{ + healthcheck.NewClient(service, tableRendererURL), + } +} + +// NewWithHealthClient creates a new instance of Client, +// reusing the URL and Clienter from the provided health check client. +func NewWithHealthClient(hcCli *healthcheck.Client) *Client { + return &Client{ + healthcheck.NewClientWithClienter(service, hcCli.URL, hcCli.Client), + } +} + +// closeResponseBody closes the response body and logs an error if unsuccessful +func closeResponseBody(ctx context.Context, resp *http.Response) { + if resp.Body != nil { + if err := resp.Body.Close(); err != nil { + log.Error(ctx, "error closing http response body", err) + } + } +} + +// Checker calls table-renderer health endpoint and returns a check object to the caller. +func (r *Client) Checker(ctx context.Context, check *health.CheckState) error { + return r.hcCli.Checker(ctx, check) +} + +// URL returns the URL used by this client +func (c *Client) URL() string { + return c.hcCli.URL +} + +// HealthClient returns the underlying Healthcheck Client for this cient +func (c *Client) HealthClient() *healthcheck.Client { + return c.hcCli +} + +// Render returns the given table json rendered with the given format +func (c *Client) Render(ctx context.Context, format string, json []byte) ([]byte, error) { + if json == nil { + json = []byte(`{}`) + } + return c.post(ctx, fmt.Sprintf("/render/%s", format), json) +} + +func (r *Client) post(ctx context.Context, path string, b []byte) ([]byte, error) { + uri := r.hcCli.URL + path + + req, err := http.NewRequest(http.MethodPost, uri, bytes.NewBuffer(b)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := r.hcCli.Client.Do(ctx, req) + if err != nil { + return nil, err + } + defer closeResponseBody(ctx, resp) + + if resp.StatusCode != http.StatusOK { + return nil, ErrInvalidTableRendererResponse{resp.StatusCode} + } + + return ioutil.ReadAll(resp.Body) +} diff --git a/tablerenderer/client_test.go b/tablerenderer/client_test.go new file mode 100644 index 00000000..9a7f34d9 --- /dev/null +++ b/tablerenderer/client_test.go @@ -0,0 +1,125 @@ +package tablerenderer + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/ONSdigital/dp-api-clients-go/v2/health" + dphttp "github.com/ONSdigital/dp-net/http" +) + +const ( + testHost = "http://localhost:8080" +) + +func TestClientNew(t *testing.T) { + Convey("New creates a new client with the expected URL and name", t, func() { + client := New(testHost) + So(client.URL(), ShouldEqual, testHost) + So(client.HealthClient().Name, ShouldEqual, "table-renderer") + }) + + Convey("Given an existing healthcheck client", t, func() { + hcClient := health.NewClient("generic", testHost) + Convey("When creating a new table rednerer client providing it", func() { + client := NewWithHealthClient(hcClient) + Convey("Then it returns a new client with the expected URL and name", func() { + So(client.URL(), ShouldEqual, testHost) + So(client.HealthClient().Name, ShouldEqual, "table-renderer") + }) + }) + }) +} + +func TestRender(t *testing.T) { + json := []byte("{ }") + html := []byte("") + + Convey("Given that 200 OK is returned by the service", t, func() { + httpClient := newMockHTTPClient(&http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewBuffer(html)), + }, nil) + client := newTableRendererClient(httpClient) + + Convey("When Render is called", func() { + response, err := client.Render(context.Background(), "html", json) + + Convey("Then the expected call to the table-renderer is made", func() { + expectedUrl := fmt.Sprintf("%s/render/html", testHost) + So(httpClient.DoCalls(), ShouldHaveLength, 1) + So(httpClient.DoCalls()[0].Req.URL.String(), ShouldEqual, expectedUrl) + So(httpClient.DoCalls()[0].Req.Method, ShouldEqual, http.MethodPost) + }) + Convey("And the expected result is returned without error", func() { + So(err, ShouldBeNil) + So(response, ShouldResemble, html) + }) + }) + }) + + Convey("Given that 404 is returned by the service ", t, func() { + httpClient := newMockHTTPClient(&http.Response{ + StatusCode: http.StatusNotFound, + Body: ioutil.NopCloser(bytes.NewReader([]byte("URL not found"))), + }, nil) + client := newTableRendererClient(httpClient) + Convey("When Render is called", func() { + response, err := client.Render(context.Background(), "csv", json) + Convey("Then the expected call to the table-renderer is made", func() { + expectedUrl := fmt.Sprintf("%s/render/csv", testHost) + So(httpClient.DoCalls(), ShouldHaveLength, 1) + So(httpClient.DoCalls()[0].Req.URL.String(), ShouldEqual, expectedUrl) + So(httpClient.DoCalls()[0].Req.Method, ShouldEqual, http.MethodPost) + }) + Convey("And an error is returned", func() { + So(err, ShouldResemble, ErrInvalidTableRendererResponse{responseCode: 404}) + So(response, ShouldBeNil) + }) + }) + }) + + Convey("Given an http client that fails to perform a request", t, func() { + errorString := "table renderer error" + httpClient := newMockHTTPClient(nil, errors.New(errorString)) + client := newTableRendererClient(httpClient) + + Convey("When Render is called", func() { + response, err := client.Render(context.Background(), "xlsx", json) + Convey("Then the expected call to the table-renderer is made", func() { + expectedUrl := fmt.Sprintf("%s/render/xlsx", testHost) + So(httpClient.DoCalls(), ShouldHaveLength, 1) + So(httpClient.DoCalls()[0].Req.URL.String(), ShouldEqual, expectedUrl) + So(httpClient.DoCalls()[0].Req.Method, ShouldEqual, http.MethodPost) + }) + Convey("And an error is returned", func() { + So(err.Error(), ShouldResemble, errorString) + So(response, ShouldBeNil) + }) + }) + }) +} + +func newTableRendererClient(clienter *dphttp.ClienterMock) *Client { + healthClient := health.NewClientWithClienter("", testHost, clienter) + return NewWithHealthClient(healthClient) +} + +func newMockHTTPClient(r *http.Response, err error) *dphttp.ClienterMock { + return &dphttp.ClienterMock{ + SetPathsWithNoRetriesFunc: func(paths []string) {}, + DoFunc: func(ctx context.Context, req *http.Request) (*http.Response, error) { + return r, err + }, + GetPathsWithNoRetriesFunc: func() []string { + return []string{} + }, + } +}