Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client for table renderer #310

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Common client code - in go - for ONS APIs:
* releasecalendar
* renderer
* search
* table-renderer
* upload (Static Files)

## Usage
Expand Down
105 changes: 105 additions & 0 deletions tablerenderer/client.go
Original file line number Diff line number Diff line change
@@ -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)
}
125 changes: 125 additions & 0 deletions tablerenderer/client_test.go
Original file line number Diff line number Diff line change
@@ -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("<html/>")

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{}
},
}
}