Skip to content

Commit

Permalink
[INTG-2353] process deleted inspections (#268)
Browse files Browse the repository at this point in the history
* INTG-2353 drain tests

* INTG-2353 tests

* INTG-2353 export

* INTG-2353 change fixtures

* INTG-2353 tidy

* INTG-2353 don't fail when audit not working


Co-authored-by: Brandon Cook <[email protected]>
  • Loading branch information
MickStanciu and BCook98 authored Jul 10, 2022
1 parent 6de7f16 commit 8aa0d5e
Show file tree
Hide file tree
Showing 24 changed files with 875 additions and 146 deletions.
152 changes: 58 additions & 94 deletions internal/app/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
defaultRetryMax = 4
)

const activityHistoryLogURL = "/accounts/history/v1/activity_log/list"

// Client is used to with iAuditor API's.
type Client struct {
logger *zap.SugaredLogger
Expand Down Expand Up @@ -159,76 +161,6 @@ func OptAddTLSCert(certPath string) Opt {
}
}

// FeedMetadata is a representation of the metadata returned when fetching a feed
type FeedMetadata struct {
NextPage string `json:"next_page"`
RemainingRecords int64 `json:"remaining_records"`
}

// GetFeedParams is a list of all parameters we can set when fetching a feed
type GetFeedParams struct {
ModifiedAfter time.Time `url:"modified_after,omitempty"`
TemplateIDs []string `url:"template,omitempty"`
Archived string `url:"archived,omitempty"`
Completed string `url:"completed,omitempty"`
IncludeInactive bool `url:"include_inactive,omitempty"`
Limit int `url:"limit,omitempty"`
WebReportLink string `url:"web_report_link,omitempty"`

// Applicable only for sites
IncludeDeleted bool `url:"include_deleted,omitempty"`
ShowOnlyLeafNodes *bool `url:"show_only_leaf_nodes,omitempty"`
}

// GetFeedRequest has all the data needed to make a request to get a feed
type GetFeedRequest struct {
URL string
InitialURL string
Params GetFeedParams
}

// GetFeedResponse is a representation of the data returned when fetching a feed
type GetFeedResponse struct {
Metadata FeedMetadata `json:"metadata"`

Data json.RawMessage `json:"data"`
}

// GetMediaRequest has all the data needed to make a request to get a media
type GetMediaRequest struct {
URL string
AuditID string
}

// GetMediaResponse is a representation of the data returned when fetching media
type GetMediaResponse struct {
ContentType string
Body []byte
MediaID string
}

// ListInspectionsParams is a list of all parameters we can set when fetching inspections
type ListInspectionsParams struct {
ModifiedAfter time.Time `url:"modified_after,omitempty"`
TemplateIDs []string `url:"template,omitempty"`
Archived string `url:"archived,omitempty"`
Completed string `url:"completed,omitempty"`
Limit int `url:"limit,omitempty"`
}

// Inspection represents some of the properties present in an inspection
type Inspection struct {
ID string `json:"audit_id"`
ModifiedAt time.Time `json:"modified_at"`
}

// ListInspectionsResponse represents the response of listing inspections
type ListInspectionsResponse struct {
Count int `json:"count"`
Total int `json:"total"`
Inspections []Inspection `json:"audits"`
}

func (a *Client) do(doer HTTPDoer) (*http.Response, error) {
url := doer.URL()

Expand Down Expand Up @@ -265,6 +197,10 @@ func (a *Client) do(doer HTTPDoer) (*http.Response, error) {
)
return resp, nil

case status == http.StatusForbidden:
a.logger.Debugw("no access to this resource", "url", url, "status", status)
return resp, nil

default:
a.logger.Errorw("http request error status",
"url", url,
Expand Down Expand Up @@ -355,7 +291,8 @@ func (a *Client) GetFeed(ctx context.Context, request *GetFeedRequest) (*GetFeed
initialURL = request.URL
}

sl := a.sling.New().Get(initialURL).
sl := a.sling.New().
Get(initialURL).
Set(string(Authorization), "Bearer "+a.accessToken).
Set(string(IntegrationID), "iauditor-exporter").
Set(string(IntegrationVersion), version.GetVersion()).
Expand Down Expand Up @@ -404,6 +341,56 @@ func (a *Client) DrainFeed(ctx context.Context, request *GetFeedRequest, feedFn
return nil
}

// ListOrganisationActivityLog returns response from AccountsActivityLog or error
func (a *Client) ListOrganisationActivityLog(ctx context.Context, request *GetAccountsActivityLogRequestParams) (*GetAccountsActivityLogResponse, error) {
sl := a.sling.New().
Post(activityHistoryLogURL).
Set(string(Authorization), "Bearer "+a.accessToken).
Set(string(IntegrationID), "iauditor-exporter").
Set(string(IntegrationVersion), version.GetVersion()).
Set(string(XRequestID), util.RequestIDFromContext(ctx)).
BodyJSON(request)

req, _ := sl.Request()
req = req.WithContext(ctx)

var res GetAccountsActivityLogResponse
var errMsg json.RawMessage
_, err := a.do(&slingHTTPDoer{
sl: sl,
req: req,
successV: &res,
failureV: &errMsg,
})
if err != nil {
return nil, errors.Wrap(err, "Failed request to API")
}

return &res, nil
}

// DrainAccountActivityHistoryLog cycle throgh GetAccountsActivityLogResponse and adapts the filter whule there is a next page
func (a *Client) DrainAccountActivityHistoryLog(ctx context.Context, req *GetAccountsActivityLogRequestParams, feedFn func(*GetAccountsActivityLogResponse) error) error {
for {
res, err := a.ListOrganisationActivityLog(ctx, req)
if err != nil {
return err
}

err = feedFn(res)
if err != nil {
return err
}

if res.NextPageToken != "" {
req.PageToken = res.NextPageToken
} else {
break
}
}
return nil
}

// ListInspections retrieves the list of inspections from iAuditor
func (a *Client) ListInspections(ctx context.Context, params *ListInspectionsParams) (*ListInspectionsResponse, error) {
var (
Expand Down Expand Up @@ -499,15 +486,6 @@ func (a *Client) DrainInspections(
return nil
}

type initiateInspectionReportExportRequest struct {
Format string `json:"format"`
PreferenceID string `json:"preference_id,omitempty"`
}

type initiateInspectionReportExportResponse struct {
MessageID string `json:"messageId"`
}

// InitiateInspectionReportExport export the report of the given auditID.
func (a *Client) InitiateInspectionReportExport(ctx context.Context, auditID string, format string, preferenceID string) (string, error) {
var (
Expand Down Expand Up @@ -544,12 +522,6 @@ func (a *Client) InitiateInspectionReportExport(ctx context.Context, auditID str
return result.MessageID, nil
}

// InspectionReportExportCompletionResponse represents the response of report export completion status
type InspectionReportExportCompletionResponse struct {
Status string `json:"status"`
URL string `json:"url,omitempty"`
}

// CheckInspectionReportExportCompletion checks if the report export is complete.
func (a *Client) CheckInspectionReportExportCompletion(ctx context.Context, auditID string, messageID string) (*InspectionReportExportCompletionResponse, error) {
var (
Expand Down Expand Up @@ -605,14 +577,6 @@ func (a *Client) DownloadInspectionReportFile(ctx context.Context, url string) (
return res.Body, nil
}

// WhoAmIResponse represents the the response of WhoAmI
type WhoAmIResponse struct {
UserID string `json:"user_id"`
OrganisationID string `json:"organisation_id"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
}

// WhoAmI returns the details for the user who is making the request
func (a *Client) WhoAmI(ctx context.Context) (*WhoAmIResponse, error) {
var (
Expand Down
113 changes: 113 additions & 0 deletions internal/app/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"errors"
"fmt"
"github.com/stretchr/testify/require"
"net/http"
"net/url"
"path"
"strings"
"testing"
"time"
Expand All @@ -17,6 +19,117 @@ import (
"gopkg.in/h2non/gock.v1"
)

func TestClient_DrainDeletedInspections(t *testing.T) {
defer gock.Off()

gock.New("http://localhost:9999").
Post("/accounts/history/v1/activity_log/list").
BodyString(`{"org_id":"","page_size":4,"page_token":"","filters":{"timeframe":{"from":"2022-06-30T10:43:17Z"},"event_types":["inspection.deleted"],"limit":4}}`).
Reply(http.StatusOK).
File(path.Join("fixtures", "inspections_deleted_page_1.json"))

gock.New("http://localhost:9999").
Post("/accounts/history/v1/activity_log/list").
BodyString(`{"org_id":"","page_size":4,"page_token":"eyJldmVudF90eXBlcyI6WyJpbnNwZWN0aW9uLmFyY2hpdmVkIl0sImxpbWl0Ijo0LCJvZmZzZXQiOjR9","filters":{"timeframe":{"from":"2022-06-30T10:43:17Z"},"event_types":["inspection.deleted"],"limit":4}}`).
Reply(http.StatusOK).
File(path.Join("fixtures", "inspections_deleted_page_2.json"))

gock.New("http://localhost:9999").
Post("/accounts/history/v1/activity_log/list").
BodyString(`{"org_id":"","page_size":4,"page_token":"eyJldmVudF90eXBlcyI6WyJpbnNwZWN0aW9uLmFyY2hpdmVkIl0sImxpbWl0Ijo0LCJvZmZzZXQiOjh9","filters":{"timeframe":{"from":"2022-06-30T10:43:17Z"},"event_types":["inspection.deleted"],"limit":4}}`).
Reply(http.StatusOK).
File(path.Join("fixtures", "inspections_deleted_page_3.json"))

gock.New("http://localhost:9999").
Post("/accounts/history/v1/activity_log/list").
BodyString(`{"org_id":"","page_size":4,"page_token":"eyJldmVudF90eXBlcyI6WyJpbnNwZWN0aW9uLmFyY2hpdmVkIl0sImxpbWl0Ijo0LCJvZmZzZXQiOjEyfQ==","filters":{"timeframe":{"from":"2022-06-30T10:43:17Z"},"event_types":["inspection.deleted"],"limit":4}}`).
Reply(http.StatusOK).
File(path.Join("fixtures", "inspections_deleted_page_4.json"))

apiClient := api.GetTestClient()
gock.InterceptClient(apiClient.HTTPClient())

fakeTime, err := time.Parse(time.RFC3339, "2022-06-30T10:43:17Z")
require.Nil(t, err)
req := api.NewGetAccountsActivityLogRequest(4, fakeTime)

calls := 0
var deletedIds = make([]string, 0, 15)
fn := func(res *api.GetAccountsActivityLogResponse) error {
calls++
for _, a := range res.Activities {
deletedIds = append(deletedIds, a.Metadata["inspection_id"])
}
return nil
}
err = apiClient.DrainAccountActivityHistoryLog(context.TODO(), req, fn)
require.Nil(t, err)
assert.EqualValues(t, 4, calls)
require.EqualValues(t, 15, len(deletedIds))
assert.EqualValues(t, "3b8ac4f4-e904-453e-b5a0-b5cceedb0ee1", deletedIds[0])
assert.EqualValues(t, "4b3bc1d5-3011-4f81-94d4-125d2bce7ca8", deletedIds[1])
assert.EqualValues(t, "6bd628a6-5188-425f-89ef-81f9dfcdf5cd", deletedIds[2])
assert.EqualValues(t, "d722fc86-defa-4de2-b8d7-c0a3e0ec6ce4", deletedIds[3])
assert.EqualValues(t, "ed8b3911-4141-41c4-946c-167bb6f61109", deletedIds[4])
assert.EqualValues(t, "fd95cb4b-e1e7-488b-ba58-93fecd2379dc", deletedIds[5])
assert.EqualValues(t, "1878c1e2-8a42-4f63-9e07-2e605f76762b", deletedIds[6])
assert.EqualValues(t, "9e28ab2c-ce8c-44a7-81d3-76d0ac47dc91", deletedIds[7])
assert.EqualValues(t, "48d61915-98c8-4d05-b786-4948dad199be", deletedIds[8])
assert.EqualValues(t, "331727d2-4976-45da-857a-6d080dc645a9", deletedIds[9])
assert.EqualValues(t, "1f2c9c1b-6f35-4bae-9b38-4094b40e13c1", deletedIds[10])
assert.EqualValues(t, "35583d49-6421-40a8-a6f5-591c718c6025", deletedIds[11])
assert.EqualValues(t, "eb49e9f8-4a3c-4b8f-a180-7ba0d171e93d", deletedIds[12])
assert.EqualValues(t, "47ac0dce-16f9-4d73-b517-8372368af162", deletedIds[13])
assert.EqualValues(t, "6d2f8bd5-a965-4046-b2b4-ccdf8341c9f0", deletedIds[14])
}

func TestClient_DrainDeletedInspections_WhenApiReturnsError(t *testing.T) {
defer gock.Off()

gock.New("http://localhost:9999").
Persist().
Post("/accounts/history/v1/activity_log/list").
Reply(http.StatusInternalServerError).
JSON(`{"error": "something bad happened"}`)

apiClient := api.GetTestClient()
gock.InterceptClient(apiClient.HTTPClient())

fakeTime, err := time.Parse(time.RFC3339, "2022-06-30T10:43:17Z")
require.Nil(t, err)
req := api.NewGetAccountsActivityLogRequest(14, fakeTime)
fn := func(res *api.GetAccountsActivityLogResponse) error {
return nil
}
err = apiClient.DrainAccountActivityHistoryLog(context.TODO(), req, fn)
require.NotNil(t, err)
assert.EqualValues(t, "Failed request to API: http://localhost:9999/accounts/history/v1/activity_log/list giving up after 2 attempt(s)", err.Error())
}

func TestClient_DrainDeletedInspections_WhenFeedFnReturnsError(t *testing.T) {
defer gock.Off()

gock.New("http://localhost:9999").
Post("/accounts/history/v1/activity_log/list").
BodyString(`{"org_id":"","page_size":4,"page_token":"","filters":{"timeframe":{"from":"2022-06-30T10:43:17Z"},"event_types":["inspection.deleted"],"limit":4}}`).
Reply(http.StatusOK).
File(path.Join("fixtures", "inspections_deleted_page_1.json"))

apiClient := api.GetTestClient()
gock.InterceptClient(apiClient.HTTPClient())

fakeTime, err := time.Parse(time.RFC3339, "2022-06-30T10:43:17Z")
require.Nil(t, err)
req := api.NewGetAccountsActivityLogRequest(4, fakeTime)

fn := func(res *api.GetAccountsActivityLogResponse) error {
return fmt.Errorf("ERROR_GetAccountsActivityLogResponse")
}
err = apiClient.DrainAccountActivityHistoryLog(context.TODO(), req, fn)
require.NotNil(t, err)
assert.EqualValues(t, "ERROR_GetAccountsActivityLogResponse", err.Error())
}

func TestAPIClientDrainFeed_should_return_for_as_long_next_page_set(t *testing.T) {
defer gock.Off()

Expand Down
Loading

0 comments on commit 8aa0d5e

Please sign in to comment.