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

pfs-116 FE only for downloading a report #3

Merged
merged 8 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
37 changes: 33 additions & 4 deletions internal/api/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"fmt"
"io"
"net/http"
)

Expand All @@ -21,18 +22,22 @@ func (e ClientError) Error() string {
return string(e)
}

func NewApiClient(httpClient HTTPClient) *ApiClient {
func NewApiClient(httpClient HTTPClient, siriusUrl string, backendUrl string) (*ApiClient, error) {
return &ApiClient{
http: httpClient,
}
http: httpClient,
siriusUrl: siriusUrl,
backendUrl: backendUrl,
}, nil
}

type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}

type ApiClient struct {
jackgoodby marked this conversation as resolved.
Show resolved Hide resolved
http HTTPClient
http HTTPClient
siriusUrl string
backendUrl string
}

type StatusError struct {
Expand All @@ -48,3 +53,27 @@ func (e StatusError) Error() string {
func (e StatusError) Data() interface{} {
return e
}

func (c *ApiClient) newBackendRequest(ctx Context, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx.Context, method, c.backendUrl+path, body)
if err != nil {
return nil, err
}

for _, c := range ctx.Cookies {
req.AddCookie(c)
}

req.Header.Add("OPG-Bypass-Membrane", "1")
req.Header.Add("X-XSRF-TOKEN", ctx.XSRFToken)

return req, err
}

func newStatusError(resp *http.Response) StatusError {
return StatusError{
Code: resp.StatusCode,
URL: resp.Request.URL.String(),
Method: resp.Request.Method,
}
}
53 changes: 53 additions & 0 deletions internal/api/api_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package api

import (
"context"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

type MockClient struct {
DoFunc func(req *http.Request) (*http.Response, error)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unused, I'd prefer this over the global GetDoFunc below

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also recommend looking at mockery for generating these. You can take a look at ministryofjustice/opg-data-lpa-store#253 for how that can be done

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove this field since you aren't using it

}

var (
// GetDoFunc fetches the mock client's `Do` func. Implement this within a test to modify the client's behaviour.
GetDoFunc func(req *http.Request) (*http.Response, error)
)

func (m *MockClient) Do(req *http.Request) (*http.Response, error) {
return GetDoFunc(req)
}

func getContext(cookies []*http.Cookie) Context {
return Context{
Context: context.Background(),
Cookies: cookies,
XSRFToken: "abcde",
}
}

func TestClientError(t *testing.T) {
assert.Equal(t, "message", ClientError("message").Error())
}

func TestStatusError(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, "/some/url", nil)

resp := &http.Response{
StatusCode: http.StatusTeapot,
Request: req,
}

err := newStatusError(resp)

assert.Equal(t, "POST /some/url returned 418", err.Error())
assert.Equal(t, err, err.Data())
}

func SetUpTest() *MockClient {
jackgoodby marked this conversation as resolved.
Show resolved Hide resolved
mockClient := &MockClient{}
return mockClient
}
91 changes: 91 additions & 0 deletions internal/api/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package api

import (
"bytes"
"encoding/json"
"github.com/opg-sirius-finance-admin/internal/model"
"net/http"
)

func (c *ApiClient) Download(ctx Context, reportType string, reportJournalType string, reportScheduleType string, reportAccountType string, reportDebtType string, dateField string, dateFromField string, dateToField string, emailField string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field is redundant naming here. Also, dateField needs to be more descriptive to distinguish it from the other date fields.

var body bytes.Buffer
var dateTransformed *model.Date
var toDateTransformed *model.Date
var fromDateTransformed *model.Date

if dateField != "" {
raisedDateFormatted := model.NewDate(dateField)
dateTransformed = &raisedDateFormatted
}

if dateToField != "" {
startDateFormatted := model.NewDate(dateToField)
toDateTransformed = &startDateFormatted
}

if dateFromField != "" {
endDateFormatted := model.NewDate(dateFromField)
fromDateTransformed = &endDateFormatted
}

err := json.NewEncoder(&body).Encode(model.Download{
ReportType: reportType,
ReportJournalType: reportJournalType,
ReportScheduleType: reportScheduleType,
ReportAccountType: reportAccountType,
ReportDebtType: reportDebtType,
DateField: dateTransformed,
ToDateField: toDateTransformed,
FromDateField: fromDateTransformed,
Email: emailField,
})
if err != nil {
return err
}

req, err := c.newBackendRequest(ctx, http.MethodGet, "/downloads", &body)

if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")

resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusCreated {
jackgoodby marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

if resp.StatusCode == http.StatusUnauthorized {
return ErrUnauthorized
}

if resp.StatusCode == http.StatusUnprocessableEntity {
var v model.ValidationError
if err := json.NewDecoder(resp.Body).Decode(&v); err == nil && len(v.Errors) > 0 {
return model.ValidationError{Errors: v.Errors}
}
}

if resp.StatusCode == http.StatusBadRequest {
var badRequests model.BadRequests
if err := json.NewDecoder(resp.Body).Decode(&badRequests); err != nil {
return err
}

validationErrors := make(model.ValidationErrors)
jackgoodby marked this conversation as resolved.
Show resolved Hide resolved
for _, reason := range badRequests.Reasons {
innerMap := make(map[string]string)
innerMap[reason] = reason
validationErrors[reason] = innerMap
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
innerMap := make(map[string]string)
innerMap[reason] = reason
validationErrors[reason] = innerMap
validationErrors[reason] = map[string]string{reason: reason}

}

return model.ValidationError{Errors: validationErrors}
}

return newStatusError(resp)
}
119 changes: 119 additions & 0 deletions internal/api/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package api

import (
"bytes"
"encoding/json"
"github.com/opg-sirius-finance-admin/internal/model"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you haven't already, get https://pkg.go.dev/golang.org/x/tools/cmd/goimports for formatting. Otherwise maybe it needs updating?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you look into this?

"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSubmitDownload(t *testing.T) {
mockClient := SetUpTest()
client, _ := NewApiClient(mockClient, "http://localhost:3000", "")

data := `{
"reportType": "AccountsReceivable",
"reportJournalType": "",
"reportScheduleType": "",
"reportAccountType": "BadDebtWriteOffReport",
"reportDebtType": "",
"dateField": "11/05/2024",
"dateFromField": "01/04/2024",
"dateToField": "31/03/2025",
"emailField": "[email protected]",
}
`

r := io.NopCloser(bytes.NewReader([]byte(data)))

GetDoFunc = func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 201,
Body: r,
}, nil
}

err := client.Download(getContext(nil), "AccountsReceivable", "", "", "BadDebtWriteOffReport", "", "11/05/2024", "01/04/2024", "31/03/2025", "[email protected]")
assert.Equal(t, nil, err)
}

func TestSubmitDownloadUnauthorised(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd stick to one style of testing, either use the mock to switch behaviour or do a test server

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

w.WriteHeader(http.StatusUnauthorized)
}))
defer svr.Close()

client, _ := NewApiClient(http.DefaultClient, svr.URL, svr.URL)

err := client.Download(getContext(nil), "AccountsReceivable", "", "", "BadDebtWriteOffReport", "", "11/05/2024", "01/04/2024", "31/03/2025", "[email protected]")

assert.Equal(t, ErrUnauthorized.Error(), err.Error())
}

func TestSubmitDownloadReturns500Error(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer svr.Close()

client, _ := NewApiClient(http.DefaultClient, svr.URL, svr.URL)

err := client.Download(getContext(nil), "AccountsReceivable", "", "", "BadDebtWriteOffReport", "", "11/05/2024", "01/04/2024", "31/03/2025", "[email protected]")

assert.Equal(t, StatusError{
Code: http.StatusInternalServerError,
URL: svr.URL + "/downloads",
Method: http.MethodGet,
}, err)
}

func TestSubmitDownloadReturnsBadRequestError(t *testing.T) {
mockClient := SetUpTest()
client, _ := NewApiClient(mockClient, "http://localhost:3000", "")

json := `
{"reasons":["StartDate","EndDate"]}
`

r := io.NopCloser(bytes.NewReader([]byte(json)))

GetDoFunc = func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 400,
Body: r,
}, nil
}

err := client.Download(getContext(nil), "AccountsReceivable", "", "", "BadDebtWriteOffReport", "", "11/05/2024", "01/04/2024", "31/03/2025", "[email protected]")

expectedError := model.ValidationError{Message: "", Errors: model.ValidationErrors{"EndDate": map[string]string{"EndDate": "EndDate"}, "StartDate": map[string]string{"StartDate": "StartDate"}}}
assert.Equal(t, expectedError, err)
}

func TestSubmitDownloadReturnsValidationError(t *testing.T) {
validationErrors := model.ValidationError{
Message: "Validation failed",
Errors: map[string]map[string]string{
"ReportType": {
"required": "Please select a report type",
},
},
}
responseBody, _ := json.Marshal(validationErrors)
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write(responseBody)
}))
defer svr.Close()

client, _ := NewApiClient(http.DefaultClient, svr.URL, svr.URL)

err := client.Download(getContext(nil), "", "", "", "", "", "", "", "", "")
expectedError := model.ValidationError{Message: "", Errors: model.ValidationErrors{"ReportType": map[string]string{"required": "Please select a report type"}}}
assert.Equal(t, expectedError, err.(model.ValidationError))
}
Loading
Loading