From 9e91bb5ad4c210fbeaca6268487bebeb58d67e90 Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Mon, 15 May 2023 15:38:18 +0000 Subject: [PATCH 01/17] DXE-2628 DXE-2633 [Edgegrid Golang] Create CloudWrapper package --- pkg/cloudwrapper/cloudwrapper.go | 38 ++++++++ pkg/cloudwrapper/errors.go | 79 +++++++++++++++++ pkg/cloudwrapper/errors_test.go | 144 +++++++++++++++++++++++++++++++ pkg/cloudwrapper/mocks.go | 13 +++ 4 files changed, 274 insertions(+) create mode 100644 pkg/cloudwrapper/cloudwrapper.go create mode 100644 pkg/cloudwrapper/errors.go create mode 100644 pkg/cloudwrapper/errors_test.go create mode 100644 pkg/cloudwrapper/mocks.go diff --git a/pkg/cloudwrapper/cloudwrapper.go b/pkg/cloudwrapper/cloudwrapper.go new file mode 100644 index 00000000..d229d3da --- /dev/null +++ b/pkg/cloudwrapper/cloudwrapper.go @@ -0,0 +1,38 @@ +// Package cloudwrapper provides access to the Akamai Cloud Wrapper API +package cloudwrapper + +import ( + "errors" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" +) + +var ( + // ErrStructValidation is returned when given struct validation failed + ErrStructValidation = errors.New("struct validation") +) + +type ( + // CloudWrapper is the api interface for Cloud Wrapper + CloudWrapper interface { + } + + cloudwrapper struct { + session.Session + } + + // Option defines an CloudWrapper option + Option func(*cloudwrapper) +) + +// Client returns a new cloudwrapper Client instance with the specified controller +func Client(sess session.Session, opts ...Option) CloudWrapper { + c := &cloudwrapper{ + Session: sess, + } + + for _, opt := range opts { + opt(c) + } + return c +} diff --git a/pkg/cloudwrapper/errors.go b/pkg/cloudwrapper/errors.go new file mode 100644 index 00000000..bccce52a --- /dev/null +++ b/pkg/cloudwrapper/errors.go @@ -0,0 +1,79 @@ +package cloudwrapper + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +type ( + // Error is a cloudwrapper error implementation + // For details on possible error types, refer to: https://techdocs.akamai.com/cloud-wrapper/reference/errors + Error struct { + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Instance string `json:"instance"` + Status int `json:"status"` + Detail string `json:"detail"` + Errors []ErrorItem `json:"errors"` + } + + // ErrorItem is a cloud wrapper error's item + ErrorItem struct { + Type string `json:"type"` + Title string `json:"title"` + Detail string `json:"detail"` + IllegalValue any `json:"illegalValue"` + IllegalParameter string `json:"illegalParameter"` + } +) + +// Error parses an error from the response +func (e *cloudwrapper) Error(r *http.Response) error { + var result Error + var body []byte + body, err := ioutil.ReadAll(r.Body) + if err != nil { + e.Log(r.Request.Context()).Errorf("reading error response body: %s", err) + result.Status = r.StatusCode + result.Title = "Failed to read error body" + result.Detail = err.Error() + return &result + } + + if err := json.Unmarshal(body, &result); err != nil { + e.Log(r.Request.Context()).Errorf("could not unmarshal API error: %s", err) + result.Title = string(body) + result.Status = r.StatusCode + } + return &result +} + +func (e *Error) Error() string { + msg, err := json.MarshalIndent(e, "", "\t") + if err != nil { + return fmt.Sprintf("error marshaling API error: %s", err) + } + return fmt.Sprintf("API error: \n%s", msg) +} + +// Is handles error comparisons +func (e *Error) Is(target error) bool { + + var t *Error + if !errors.As(target, &t) { + return false + } + + if e == t { + return true + } + + if e.Status != t.Status { + return false + } + + return e.Error() == t.Error() +} diff --git a/pkg/cloudwrapper/errors_test.go b/pkg/cloudwrapper/errors_test.go new file mode 100644 index 00000000..3172ef13 --- /dev/null +++ b/pkg/cloudwrapper/errors_test.go @@ -0,0 +1,144 @@ +package cloudwrapper + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/require" + "github.com/tj/assert" +) + +func TestNewError(t *testing.T) { + sess, err := session.New() + require.NoError(t, err) + + req, err := http.NewRequest( + http.MethodHead, + "/", + nil) + require.NoError(t, err) + + tests := map[string]struct { + response *http.Response + expected *Error + }{ + "Bad request 400": { + response: &http.Response{ + Status: "Internal Server Error", + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser(strings.NewReader( + `{ + "type": "bad-request", + "title": "Bad Request", + "instance": "30109837-7ea6-4b14-a41d-50cfb12a4b03", + "status": 400, + "detail": "Erroneous data input", + "errors": [ + { + "type": "bad-request", + "title": "Bad Request", + "detail": "Configuration with name UpdateConfiguration already exists in account 1234-3KNWKV.", + "illegalValue": "UpdateConfiguration", + "illegalParameter": "configurationName" + }, + { + "type": "bad-request", + "title": "Bad Request", + "detail": "One or more ARL Property is already used in another configuration.", + "illegalValue": [ + { + "propertyId": "123010" + } + ], + "illegalParameter": "properties" + } + ] +}`), + ), + Request: req, + }, + expected: &Error{ + Type: "bad-request", + Title: "Bad Request", + Instance: "30109837-7ea6-4b14-a41d-50cfb12a4b03", + Status: http.StatusBadRequest, + Detail: "Erroneous data input", + Errors: []ErrorItem{ + { + Type: "bad-request", + Title: "Bad Request", + Detail: "Configuration with name UpdateConfiguration already exists in account 1234-3KNWKV.", + IllegalValue: "UpdateConfiguration", + IllegalParameter: "configurationName", + }, + { + Type: "bad-request", + Title: "Bad Request", + Detail: "One or more ARL Property is already used in another configuration.", + IllegalValue: []any{map[string]any{"propertyId": "123010"}}, + IllegalParameter: "properties", + }, + }, + }, + }, + "invalid response body, assign status code": { + response: &http.Response{ + Status: "Internal Server Error", + StatusCode: http.StatusInternalServerError, + Body: ioutil.NopCloser(strings.NewReader( + `test`), + ), + Request: req, + }, + expected: &Error{ + Title: "test", + Detail: "", + Status: http.StatusInternalServerError, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := Client(sess).(*cloudwrapper).Error(test.response) + assert.Equal(t, test.expected, res) + }) + } +} + +func TestIs(t *testing.T) { + tests := map[string]struct { + err Error + target Error + expected bool + }{ + "different error code": { + err: Error{Status: 404}, + target: Error{Status: 401}, + expected: false, + }, + "same error code": { + err: Error{Status: 404}, + target: Error{Status: 404}, + expected: true, + }, + "same error code and title": { + err: Error{Status: 404, Title: "some error"}, + target: Error{Status: 404, Title: "some error"}, + expected: true, + }, + "same error code and different error message": { + err: Error{Status: 404, Title: "some error"}, + target: Error{Status: 404, Title: "other error"}, + expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.err.Is(&test.target), test.expected) + }) + } +} diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go new file mode 100644 index 00000000..e41b0552 --- /dev/null +++ b/pkg/cloudwrapper/mocks.go @@ -0,0 +1,13 @@ +//revive:disable:exported + +package cloudwrapper + +import ( + "github.com/stretchr/testify/mock" +) + +type Mock struct { + mock.Mock +} + +var _ CloudWrapper = &Mock{} From 8455a51ee39aaf47b4ae8016d945a4ab101a7345 Mon Sep 17 00:00:00 2001 From: Mateusz Jakubiec Date: Thu, 25 May 2023 08:45:18 +0000 Subject: [PATCH 02/17] DXE-2616 Implement CloudWrapper list capacity endpoint --- CHANGELOG.md | 7 + pkg/cloudwrapper/capacity.go | 107 +++++++++++++ pkg/cloudwrapper/capacity_test.go | 212 ++++++++++++++++++++++++++ pkg/cloudwrapper/cloudwrapper.go | 1 + pkg/cloudwrapper/cloudwrapper_test.go | 62 ++++++++ pkg/cloudwrapper/errors.go | 17 ++- pkg/cloudwrapper/mocks.go | 11 ++ 7 files changed, 411 insertions(+), 6 deletions(-) create mode 100644 pkg/cloudwrapper/capacity.go create mode 100644 pkg/cloudwrapper/capacity_test.go create mode 100644 pkg/cloudwrapper/cloudwrapper_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index dd61ebb0..c9ad0162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # EDGEGRID GOLANG RELEASE NOTES +## x.x.x (x x, 2023) + +### FEATURES/ENHANCEMENTS: + +* Added Cloud Wrapper API support + * `ListCapacities` + ## 7.1.0 (July 25, 2023) ### FEATURES/ENHANCEMENTS: diff --git a/pkg/cloudwrapper/capacity.go b/pkg/cloudwrapper/capacity.go new file mode 100644 index 00000000..4b1c5f20 --- /dev/null +++ b/pkg/cloudwrapper/capacity.go @@ -0,0 +1,107 @@ +package cloudwrapper + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" +) + +type ( + // Capacities is a Cloud Wrapper API interface. + Capacities interface { + // ListCapacities fetches capacities available for a given contractId. + // If no contract id is provided, lists all available capacity locations + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/getcapacityinventory + ListCapacities(context.Context, ListCapacitiesRequest) (*ListCapacitiesResponse, error) + } + + // ListCapacitiesRequest is a request struct + ListCapacitiesRequest struct { + ContractIDs []string + } + + // ListCapacitiesResponse contains response list of location capacities + ListCapacitiesResponse struct { + Capacities []LocationCapacity `json:"capacities"` + } + + // LocationCapacity contains location capacity information + LocationCapacity struct { + LocationID int `json:"locationId"` + LocationName string `json:"locationName"` + ContractID string `json:"contractId"` + Type CapacityType `json:"type"` + ApprovedCapacity Capacity `json:"approvedCapacity"` + AssignedCapacity Capacity `json:"assignedCapacity"` + UnassignedCapacity Capacity `json:"unassignedCapacity"` + } + + // CapacityType is a type of the properties, this capacity is related to + CapacityType string + + // Capacity struct holds capacity information + Capacity struct { + Value int64 `json:"value"` + Unit Unit `json:"unit"` + } + + // Unit type of capacity value. Can be either GB or TB + Unit string +) + +const ( + // UnitGB is a const value for capacity unit + UnitGB Unit = "GB" + // UnitTB is a const value for capacity unit + UnitTB Unit = "TB" +) + +const ( + // Media type + Media CapacityType = "MEDIA" + // WebStandardTLS type + WebStandardTLS CapacityType = "WEB_STANDARD_TLS" + // WebEnhancedTLS type + WebEnhancedTLS CapacityType = "WEB_ENHANCED_TLS" +) + +var ( + // ErrListCapacities is returned in case an error occurs on ListCapacities operation + ErrListCapacities = errors.New("list capacities") +) + +func (c *cloudwrapper) ListCapacities(ctx context.Context, params ListCapacitiesRequest) (*ListCapacitiesResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListCapacities") + + uri, err := url.Parse("/cloud-wrapper/v1/capacity") + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrListCapacities, err) + } + + q := uri.Query() + for _, contractID := range params.ContractIDs { + q.Add("contractIds", contractID) + } + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListCapacities, err) + } + + var result ListCapacitiesResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListCapacities, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListCapacities, c.Error(resp)) + } + + return &result, nil +} diff --git a/pkg/cloudwrapper/capacity_test.go b/pkg/cloudwrapper/capacity_test.go new file mode 100644 index 00000000..da8b4771 --- /dev/null +++ b/pkg/cloudwrapper/capacity_test.go @@ -0,0 +1,212 @@ +package cloudwrapper + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tj/assert" +) + +func TestListCapacity(t *testing.T) { + tests := map[string]struct { + request ListCapacitiesRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *ListCapacitiesResponse + withError func(*testing.T, error) + }{ + "200 OK": { + request: ListCapacitiesRequest{ + ContractIDs: nil, + }, + responseStatus: 200, + expectedPath: "/cloud-wrapper/v1/capacity", + responseBody: ` + { + "capacities": [ + { + "locationId": 1, + "locationName": "US East", + "contractId": "A-BCDEFG", + "type": "MEDIA", + "approvedCapacity": { + "value": 2000, + "unit": "GB" + }, + "assignedCapacity": { + "value": 2, + "unit": "GB" + }, + "unassignedCapacity": { + "value": 1998, + "unit": "GB" + } + } + ] + }`, + expectedResponse: &ListCapacitiesResponse{ + Capacities: []LocationCapacity{ + { + LocationID: 1, + LocationName: "US East", + ContractID: "A-BCDEFG", + Type: Media, + ApprovedCapacity: Capacity{ + Value: 2000, + Unit: "GB", + }, + AssignedCapacity: Capacity{ + Value: 2, + Unit: "GB", + }, + UnassignedCapacity: Capacity{ + Value: 1998, + Unit: "GB", + }, + }, + }, + }, + }, + "200 OK with contracts": { + request: ListCapacitiesRequest{ + ContractIDs: []string{"A-BCDEF", "B-CDEFG"}, + }, + responseStatus: 200, + expectedPath: "/cloud-wrapper/v1/capacity?contractIds=A-BCDEF&contractIds=B-CDEFG", + responseBody: ` + { + "capacities": [ + { + "locationId": 1, + "locationName": "US East", + "contractId": "A-BCDEFG", + "type": "WEB_ENHANCED_TLS", + "approvedCapacity": { + "value": 10, + "unit": "TB" + }, + "assignedCapacity": { + "value": 1, + "unit": "TB" + }, + "unassignedCapacity": { + "value": 9, + "unit": "TB" + } + } + ] + }`, + expectedResponse: &ListCapacitiesResponse{ + Capacities: []LocationCapacity{ + { + LocationID: 1, + LocationName: "US East", + ContractID: "A-BCDEFG", + Type: WebEnhancedTLS, + ApprovedCapacity: Capacity{ + Value: 10, + Unit: UnitTB, + }, + AssignedCapacity: Capacity{ + Value: 1, + Unit: UnitTB, + }, + UnassignedCapacity: Capacity{ + Value: 9, + Unit: UnitTB, + }, + }, + }, + }, + }, + "401 not authorized": { + request: ListCapacitiesRequest{}, + responseStatus: 401, + responseBody: `{ + "type": "https://problems.luna-dev.akamaiapis.net/-/pep-authn/deny", + "title": "Not authorized", + "status": 401, + "detail": "The signature does not match", + "instance": "https://instance.luna-dev.akamaiapis.net/cloud-wrapper/v1/capacity", + "method": "GET", + "serverIp": "2.2.2.2", + "clientIp": "3.3.3.3", + "requestId": "a7a7a7a7a7a", + "requestTime": "2023-05-22T10:05:22Z" +}`, + expectedPath: "/cloud-wrapper/v1/capacity", + expectedResponse: nil, + withError: func(t *testing.T, e error) { + err := Error{ + Type: "https://problems.luna-dev.akamaiapis.net/-/pep-authn/deny", + Title: "Not authorized", + Instance: "https://instance.luna-dev.akamaiapis.net/cloud-wrapper/v1/capacity", + Status: 401, + Detail: "The signature does not match", + Method: "GET", + ServerIP: "2.2.2.2", + ClientIP: "3.3.3.3", + RequestID: "a7a7a7a7a7a", + RequestTime: "2023-05-22T10:05:22Z", + } + assert.Equal(t, true, err.Is(e)) + }, + }, + "500": { + request: ListCapacitiesRequest{}, + responseStatus: 500, + expectedPath: "/cloud-wrapper/v1/capacity", + expectedResponse: nil, + responseBody: `{ + "type": "https://problems.luna-dev.akamaiapis.net/-/resource-impl/forward-origin-error", + "title": "Server Error", + "status": 500, + "instance": "https://instance.luna-dev.akamaiapis.net/cloud-wrapper/v1/capacity", + "method": "GET", + "serverIp": "2.2.2.2", + "clientIp": "3.3.3.3", + "requestId": "a7a7a7a7a7a", + "requestTime": "2021-12-06T10:27:11Z" + }`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "https://problems.luna-dev.akamaiapis.net/-/resource-impl/forward-origin-error", + Title: "Server Error", + Status: 500, + Instance: "https://instance.luna-dev.akamaiapis.net/cloud-wrapper/v1/capacity", + Method: "GET", + ServerIP: "2.2.2.2", + ClientIP: "3.3.3.3", + RequestID: "a7a7a7a7a7a", + RequestTime: "2021-12-06T10:27:11Z", + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.ListCapacities(context.Background(), test.request) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/cloudwrapper/cloudwrapper.go b/pkg/cloudwrapper/cloudwrapper.go index d229d3da..662a4f97 100644 --- a/pkg/cloudwrapper/cloudwrapper.go +++ b/pkg/cloudwrapper/cloudwrapper.go @@ -15,6 +15,7 @@ var ( type ( // CloudWrapper is the api interface for Cloud Wrapper CloudWrapper interface { + Capacities } cloudwrapper struct { diff --git a/pkg/cloudwrapper/cloudwrapper_test.go b/pkg/cloudwrapper/cloudwrapper_test.go new file mode 100644 index 00000000..5a1992fe --- /dev/null +++ b/pkg/cloudwrapper/cloudwrapper_test.go @@ -0,0 +1,62 @@ +package cloudwrapper + +import ( + "crypto/tls" + "crypto/x509" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegrid" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockAPIClient(t *testing.T, mockServer *httptest.Server) CloudWrapper { + serverURL, err := url.Parse(mockServer.URL) + require.NoError(t, err) + certPool := x509.NewCertPool() + certPool.AddCert(mockServer.Certificate()) + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, + }, + } + s, err := session.New(session.WithClient(httpClient), session.WithSigner(&edgegrid.Config{Host: serverURL.Host})) + assert.NoError(t, err) + return Client(s) +} + +func TestClient(t *testing.T) { + sess, err := session.New() + require.NoError(t, err) + tests := map[string]struct { + options []Option + expected *cloudwrapper + }{ + "no options provided, return default": { + options: nil, + expected: &cloudwrapper{ + Session: sess, + }, + }, + "option provided, overwrite session": { + options: []Option{func(c *cloudwrapper) { + c.Session = nil + }}, + expected: &cloudwrapper{ + Session: nil, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := Client(sess, test.options...) + assert.Equal(t, res, test.expected) + }) + } +} diff --git a/pkg/cloudwrapper/errors.go b/pkg/cloudwrapper/errors.go index bccce52a..0a9c80ea 100644 --- a/pkg/cloudwrapper/errors.go +++ b/pkg/cloudwrapper/errors.go @@ -12,12 +12,17 @@ type ( // Error is a cloudwrapper error implementation // For details on possible error types, refer to: https://techdocs.akamai.com/cloud-wrapper/reference/errors Error struct { - Type string `json:"type,omitempty"` - Title string `json:"title,omitempty"` - Instance string `json:"instance"` - Status int `json:"status"` - Detail string `json:"detail"` - Errors []ErrorItem `json:"errors"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Instance string `json:"instance"` + Status int `json:"status"` + Detail string `json:"detail"` + Errors []ErrorItem `json:"errors"` + Method string `json:"method"` + ServerIP string `json:"serverIp"` + ClientIP string `json:"clientIp"` + RequestID string `json:"requestId"` + RequestTime string `json:"requestTime"` } // ErrorItem is a cloud wrapper error's item diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go index e41b0552..b3f95aaf 100644 --- a/pkg/cloudwrapper/mocks.go +++ b/pkg/cloudwrapper/mocks.go @@ -3,6 +3,8 @@ package cloudwrapper import ( + "context" + "github.com/stretchr/testify/mock" ) @@ -11,3 +13,12 @@ type Mock struct { } var _ CloudWrapper = &Mock{} + +// ListCapacities implements CloudWrapper +func (m *Mock) ListCapacities(ctx context.Context, req ListCapacitiesRequest) (*ListCapacitiesResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*ListCapacitiesResponse), args.Error(1) +} From ddf17b83e9df848aebc05dcc9d40cacb1f54e6c7 Mon Sep 17 00:00:00 2001 From: Piyush Kaushik Date: Thu, 25 May 2023 09:15:12 +0000 Subject: [PATCH 03/17] DXE-2623 DXE-2647 List locations endpoint --- CHANGELOG.md | 11 ++- pkg/cloudwrapper/cloudwrapper.go | 1 + pkg/cloudwrapper/errors.go | 8 +- pkg/cloudwrapper/locations.go | 63 +++++++++++++ pkg/cloudwrapper/locations_test.go | 140 +++++++++++++++++++++++++++++ pkg/cloudwrapper/mocks.go | 11 +++ 6 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 pkg/cloudwrapper/locations.go create mode 100644 pkg/cloudwrapper/locations_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ad0162..c37276e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # EDGEGRID GOLANG RELEASE NOTES -## x.x.x (x x, 2023) +## X.X.X (XX xx, 202x) [CloudWrapper changelog] -### FEATURES/ENHANCEMENTS: +#### FEATURES/ENHANCEMENTS: -* Added Cloud Wrapper API support - * `ListCapacities` +* [IMPORTANT] Added CloudWrapper API support + * Locations + * [ListLocations](https://techdocs.akamai.com/cloud-wrapper/reference/get-locations) + * Capacities + * [ListCapacities](https://techdocs.akamai.com/cloud-wrapper/reference/get-capacity-inventory) ## 7.1.0 (July 25, 2023) diff --git a/pkg/cloudwrapper/cloudwrapper.go b/pkg/cloudwrapper/cloudwrapper.go index 662a4f97..532ce69b 100644 --- a/pkg/cloudwrapper/cloudwrapper.go +++ b/pkg/cloudwrapper/cloudwrapper.go @@ -16,6 +16,7 @@ type ( // CloudWrapper is the api interface for Cloud Wrapper CloudWrapper interface { Capacities + Locations } cloudwrapper struct { diff --git a/pkg/cloudwrapper/errors.go b/pkg/cloudwrapper/errors.go index 0a9c80ea..07e642b5 100644 --- a/pkg/cloudwrapper/errors.go +++ b/pkg/cloudwrapper/errors.go @@ -36,20 +36,20 @@ type ( ) // Error parses an error from the response -func (e *cloudwrapper) Error(r *http.Response) error { +func (c *cloudwrapper) Error(r *http.Response) error { var result Error var body []byte body, err := ioutil.ReadAll(r.Body) if err != nil { - e.Log(r.Request.Context()).Errorf("reading error response body: %s", err) + c.Log(r.Request.Context()).Errorf("reading error response body: %s", err) result.Status = r.StatusCode result.Title = "Failed to read error body" result.Detail = err.Error() return &result } - if err := json.Unmarshal(body, &result); err != nil { - e.Log(r.Request.Context()).Errorf("could not unmarshal API error: %s", err) + if err = json.Unmarshal(body, &result); err != nil { + c.Log(r.Request.Context()).Errorf("could not unmarshal API error: %s", err) result.Title = string(body) result.Status = r.StatusCode } diff --git a/pkg/cloudwrapper/locations.go b/pkg/cloudwrapper/locations.go new file mode 100644 index 00000000..03fb0c76 --- /dev/null +++ b/pkg/cloudwrapper/locations.go @@ -0,0 +1,63 @@ +package cloudwrapper + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +type ( + // Locations is the cloudwrapper location API interface + Locations interface { + // ListLocations returns a list of locations available to distribute Cloud Wrapper capacity + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/getlocations + ListLocations(context.Context) (*ListLocationResponse, error) + } + + // ListLocationResponse represents a response object returned by ListLocations + ListLocationResponse struct { + Locations []Location `json:"locations"` + } + + // Location represents a Location object + Location struct { + LocationID int `json:"locationId"` + LocationName string `json:"locationName"` + MultiCDNLocationID string `json:"multiCdnLocationId"` + TrafficTypes []TrafficTypeItem `json:"trafficTypes"` + } + + // TrafficTypeItem represents a TrafficType object for the location + TrafficTypeItem struct { + FailoverMapName string `json:"failoverMapName"` + TrafficTypeID int `json:"trafficTypeId"` + TrafficType string `json:"TrafficType"` + } +) + +var ( + // ErrListLocations is returned when ListLocations fails + ErrListLocations = errors.New("list locations") +) + +func (c *cloudwrapper) ListLocations(ctx context.Context) (*ListLocationResponse, error) { + url := "/cloud-wrapper/v1/locations" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request:\n%s", ErrListLocations, err) + } + + var locations ListLocationResponse + resp, err := c.Exec(req, &locations) + if err != nil { + return nil, fmt.Errorf("%w: request failed:\n%s", ErrListLocations, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListLocations, c.Error(resp)) + } + + return &locations, nil +} diff --git a/pkg/cloudwrapper/locations_test.go b/pkg/cloudwrapper/locations_test.go new file mode 100644 index 00000000..b120f6c1 --- /dev/null +++ b/pkg/cloudwrapper/locations_test.go @@ -0,0 +1,140 @@ +package cloudwrapper + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/tj/assert" +) + +func TestCloudwrapper_ListLocations(t *testing.T) { + tests := map[string]struct { + responseStatus int + expectedPath string + responseBody string + expectedResponse *ListLocationResponse + withError func(*testing.T, error) + }{ + "200 OK": { + responseStatus: http.StatusOK, + expectedPath: "/cloud-wrapper/v1/locations", + responseBody: `{ + "locations": [ + { + "locationId": 1, + "locationName": "US East", + "trafficTypes": [ + { + "trafficTypeId": 1, + "trafficType": "TEST_TT1", + "failoverMapName": "test_FMN" + }, + { + "trafficTypeId": 2, + "trafficType": "TEST_TT2", + "failoverMapName": "test_FMN1" + } + ], + "multiCdnLocationId": "0123" + }, + { + "locationId": 2, + "locationName": "US West", + "trafficTypes": [ + { + "trafficTypeId": 3, + "trafficType": "TEST_TT1", + "failoverMapName": "test_FMN" + }, + { + "trafficTypeId": 4, + "trafficType": "TEST_TT2", + "failoverMapName": "test_FMN1" + } + ], + "multiCdnLocationId": "4567" + } + ]}`, + expectedResponse: &ListLocationResponse{ + Locations: []Location{ + { + LocationID: 1, + LocationName: "US East", + TrafficTypes: []TrafficTypeItem{ + { + TrafficTypeID: 1, + TrafficType: "TEST_TT1", + FailoverMapName: "test_FMN", + }, + { + TrafficTypeID: 2, + TrafficType: "TEST_TT2", + FailoverMapName: "test_FMN1", + }, + }, + MultiCDNLocationID: "0123", + }, + { + LocationID: 2, + LocationName: "US West", + TrafficTypes: []TrafficTypeItem{ + { + TrafficTypeID: 3, + TrafficType: "TEST_TT1", + FailoverMapName: "test_FMN", + }, + { + TrafficTypeID: 4, + TrafficType: "TEST_TT2", + FailoverMapName: "test_FMN1", + }, + }, + MultiCDNLocationID: "4567", + }, + }, + }, + }, + "500 internal server error": { + responseStatus: http.StatusInternalServerError, + expectedPath: "/cloud-wrapper/v1/locations", + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error processing request", + "status": 500 + }`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error processing request", + Status: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + users, err := client.ListLocations(context.Background()) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedResponse, users) + }) + } +} diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go index b3f95aaf..70ae2d54 100644 --- a/pkg/cloudwrapper/mocks.go +++ b/pkg/cloudwrapper/mocks.go @@ -22,3 +22,14 @@ func (m *Mock) ListCapacities(ctx context.Context, req ListCapacitiesRequest) (* } return args.Get(0).(*ListCapacitiesResponse), args.Error(1) } + +// ListLocations implements CloudWrapper +func (m *Mock) ListLocations(ctx context.Context) (*ListLocationResponse, error) { + args := m.Called(ctx) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListLocationResponse), args.Error(1) +} From 2cfb0be8d3dfee90650c7a6df117ea8fcaf0b1db Mon Sep 17 00:00:00 2001 From: Dawid Dzhafarov Date: Mon, 29 May 2023 14:14:01 +0000 Subject: [PATCH 04/17] DXE-2626 Implement Properties interface --- CHANGELOG.md | 3 + pkg/cloudwrapper/cloudwrapper.go | 1 + pkg/cloudwrapper/mocks.go | 20 ++ pkg/cloudwrapper/properties.go | 175 +++++++++++++ pkg/cloudwrapper/properties_test.go | 384 ++++++++++++++++++++++++++++ 5 files changed, 583 insertions(+) create mode 100644 pkg/cloudwrapper/properties.go create mode 100644 pkg/cloudwrapper/properties_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c37276e8..a068f0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ * [ListLocations](https://techdocs.akamai.com/cloud-wrapper/reference/get-locations) * Capacities * [ListCapacities](https://techdocs.akamai.com/cloud-wrapper/reference/get-capacity-inventory) + * Properties + * [ListProperties](https://techdocs.akamai.com/cloud-wrapper/reference/get-properties) + * [ListOrigins](https://techdocs.akamai.com/cloud-wrapper/reference/get-origins) ## 7.1.0 (July 25, 2023) diff --git a/pkg/cloudwrapper/cloudwrapper.go b/pkg/cloudwrapper/cloudwrapper.go index 532ce69b..8ce0491f 100644 --- a/pkg/cloudwrapper/cloudwrapper.go +++ b/pkg/cloudwrapper/cloudwrapper.go @@ -17,6 +17,7 @@ type ( CloudWrapper interface { Capacities Locations + Properties } cloudwrapper struct { diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go index 70ae2d54..c37b0725 100644 --- a/pkg/cloudwrapper/mocks.go +++ b/pkg/cloudwrapper/mocks.go @@ -33,3 +33,23 @@ func (m *Mock) ListLocations(ctx context.Context) (*ListLocationResponse, error) return args.Get(0).(*ListLocationResponse), args.Error(1) } + +func (m *Mock) ListProperties(ctx context.Context, r ListPropertiesRequest) (*ListPropertiesResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListPropertiesResponse), args.Error(1) +} + +func (m *Mock) ListOrigins(ctx context.Context, r ListOriginsRequest) (*ListOriginsResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListOriginsResponse), args.Error(1) +} diff --git a/pkg/cloudwrapper/properties.go b/pkg/cloudwrapper/properties.go new file mode 100644 index 00000000..fd5d3720 --- /dev/null +++ b/pkg/cloudwrapper/properties.go @@ -0,0 +1,175 @@ +package cloudwrapper + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // Properties is a CloudWrapper properties API interface + Properties interface { + // ListProperties lists unused properties + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-properties + ListProperties(context.Context, ListPropertiesRequest) (*ListPropertiesResponse, error) + // ListOrigins lists property origins + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-origins + ListOrigins(context.Context, ListOriginsRequest) (*ListOriginsResponse, error) + } + + // ListPropertiesRequest holds parameters for ListProperties + ListPropertiesRequest struct { + Unused bool + ContractIDs []string + } + + // ListOriginsRequest holds parameters for ListOrigins + ListOriginsRequest struct { + PropertyID int64 + ContractID string + GroupID int64 + } + + // ListPropertiesResponse contains response from ListProperties + ListPropertiesResponse struct { + Properties []Property `json:"properties"` + } + + // ListOriginsResponse contains response from ListOrigins + ListOriginsResponse struct { + Children []Child `json:"children"` + Default []Behavior `json:"default"` + } + + // Child represents children rules in a property + Child struct { + Name string `json:"name"` + Behaviors []Behavior `json:"behaviors"` + } + + // Behavior contains behavior information + Behavior struct { + Hostname string `json:"hostname"` + OriginType OriginType `json:"originType"` + } + + // Property represents property object + Property struct { + GroupID int64 `json:"groupId"` + ContractID string `json:"contractId"` + PropertyID int64 `json:"propertyId"` + PropertyName string `json:"propertyName"` + Type PropertyType `json:"type"` + } + + // OriginType represents the type of the origin + OriginType string + + // PropertyType represents the type of the property + PropertyType string +) + +const ( + // PropertyTypeWeb is the web type of the property + PropertyTypeWeb PropertyType = "WEB" + // PropertyTypeMedia is the media type of the property + PropertyTypeMedia PropertyType = "MEDIA" + // OriginTypeCustomer is the customer type of the origin + OriginTypeCustomer OriginType = "CUSTOMER" + // OriginTypeNetStorage is the net storage type of the origin + OriginTypeNetStorage OriginType = "NET_STORAGE" +) + +// Validate validates ListOriginsRequest +func (r ListOriginsRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "PropertyID": validation.Validate(r.PropertyID, validation.Required), + "ContractID": validation.Validate(r.ContractID, validation.Required), + "GroupID": validation.Validate(r.GroupID, validation.Required), + }) +} + +var ( + // ErrListProperties is returned when ListProperties fails + ErrListProperties = errors.New("list properties") + // ErrListOrigins is returned when ListOrigins fails + ErrListOrigins = errors.New("list origins") +) + +func (c *cloudwrapper) ListProperties(ctx context.Context, params ListPropertiesRequest) (*ListPropertiesResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListProperties") + + uri, err := url.Parse("/cloud-wrapper/v1/properties") + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrListProperties, err) + } + + q := uri.Query() + q.Add("unused", strconv.FormatBool(params.Unused)) + for _, ctr := range params.ContractIDs { + q.Add("contractIds", ctr) + } + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListProperties, err) + } + + var result ListPropertiesResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListProperties, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListProperties, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudwrapper) ListOrigins(ctx context.Context, params ListOriginsRequest) (*ListOriginsResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListOrigins") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrListOrigins, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cloud-wrapper/v1/properties/%d/origins", params.PropertyID)) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrListOrigins, err) + } + + q := uri.Query() + q.Add("contractId", params.ContractID) + q.Add("groupId", strconv.FormatInt(params.GroupID, 10)) + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListOrigins, err) + } + + var result ListOriginsResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListOrigins, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListOrigins, c.Error(resp)) + } + + return &result, nil +} diff --git a/pkg/cloudwrapper/properties_test.go b/pkg/cloudwrapper/properties_test.go new file mode 100644 index 00000000..8fe3bfe4 --- /dev/null +++ b/pkg/cloudwrapper/properties_test.go @@ -0,0 +1,384 @@ +package cloudwrapper + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListProperties(t *testing.T) { + tests := map[string]struct { + params ListPropertiesRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *ListPropertiesResponse + withError error + }{ + "200 OK - multiple properties": { + responseStatus: 200, + responseBody: ` +{ + "properties":[ + { + "propertyId":1, + "propertyName":"TestPropertyName1", + "contractId":"TestContractID1", + "groupId":11, + "type":"MEDIA" + }, + { + "propertyId":2, + "propertyName":"TestPropertyName2", + "contractId":"TestContractID2", + "groupId":22, + "type":"WEB" + }, + { + "propertyId":3, + "propertyName":"TestPropertyName3", + "contractId":"TestContractID3", + "groupId":33, + "type":"WEB" + } + ] +}`, + expectedPath: "/cloud-wrapper/v1/properties?unused=false", + expectedResponse: &ListPropertiesResponse{ + Properties: []Property{ + { + GroupID: 11, + ContractID: "TestContractID1", + PropertyID: 1, + PropertyName: "TestPropertyName1", + Type: PropertyTypeMedia, + }, + { + GroupID: 22, + ContractID: "TestContractID2", + PropertyID: 2, + PropertyName: "TestPropertyName2", + Type: PropertyTypeWeb, + }, + { + GroupID: 33, + ContractID: "TestContractID3", + PropertyID: 3, + PropertyName: "TestPropertyName3", + Type: PropertyTypeWeb, + }, + }, + }, + }, + "200 OK - single property": { + responseStatus: 200, + responseBody: ` +{ + "properties":[ + { + "propertyId":1, + "propertyName":"TestPropertyName1", + "contractId":"TestContractID1", + "groupId":11, + "type":"MEDIA" + } + ] +}`, + expectedPath: "/cloud-wrapper/v1/properties?unused=false", + expectedResponse: &ListPropertiesResponse{ + Properties: []Property{ + { + GroupID: 11, + ContractID: "TestContractID1", + PropertyID: 1, + PropertyName: "TestPropertyName1", + Type: PropertyTypeMedia, + }, + }, + }, + }, + "200 OK - properties with query params": { + params: ListPropertiesRequest{ + Unused: true, + ContractIDs: []string{ + "TestContractID1", + "TestContractID2", + }, + }, + responseStatus: 200, + responseBody: ` +{ + "properties":[ + { + "propertyId":1, + "propertyName":"TestPropertyName1", + "contractId":"TestContractID1", + "groupId":11, + "type":"MEDIA" + }, + { + "propertyId":2, + "propertyName":"TestPropertyName2", + "contractId":"TestContractID2", + "groupId":22, + "type":"MEDIA" + } + ] +}`, + expectedPath: "/cloud-wrapper/v1/properties?contractIds=TestContractID1&contractIds=TestContractID2&unused=true", + expectedResponse: &ListPropertiesResponse{ + Properties: []Property{ + { + GroupID: 11, + ContractID: "TestContractID1", + PropertyID: 1, + PropertyName: "TestPropertyName1", + Type: PropertyTypeMedia, + }, + { + GroupID: 22, + ContractID: "TestContractID2", + PropertyID: 2, + PropertyName: "TestPropertyName2", + Type: PropertyTypeMedia, + }, + }, + }, + }, + "500 internal server error": { + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/properties?unused=false", + withError: &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.ListProperties(context.Background(), test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestListOrigins(t *testing.T) { + tests := map[string]struct { + params ListOriginsRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *ListOriginsResponse + withError func(*testing.T, error) + }{ + "200 OK - multiple objects": { + params: ListOriginsRequest{ + PropertyID: 1, + ContractID: "TestContractID", + GroupID: 11, + }, + responseStatus: 200, + responseBody: ` +{ + "default":[ + { + "originType":"CUSTOMER", + "hostname":"origin-www.example.com" + }, + { + "originType":"NET_STORAGE", + "hostname":"origin-www.example2.com" + } + ], + "children":[ + { + "name":"Default CORS Policy", + "behaviors":[ + { + "originType":"NET_STORAGE", + "hostname":"origin-www.example3.com" + } + ] + }, + { + "name":"Cloud Wrapper", + "behaviors":[ + { + "originType":"CUSTOMER", + "hostname":"origin-www.example4.com" + }, + { + "originType":"CUSTOMER", + "hostname":"origin-www.example5.com" + } + ] + } + ] +}`, + expectedPath: "/cloud-wrapper/v1/properties/1/origins?contractId=TestContractID&groupId=11", + expectedResponse: &ListOriginsResponse{ + Children: []Child{ + { + Name: "Default CORS Policy", + Behaviors: []Behavior{ + { + Hostname: "origin-www.example3.com", + OriginType: OriginTypeNetStorage, + }, + }, + }, + { + Name: "Cloud Wrapper", + Behaviors: []Behavior{ + { + Hostname: "origin-www.example4.com", + OriginType: OriginTypeCustomer, + }, + { + Hostname: "origin-www.example5.com", + OriginType: OriginTypeCustomer, + }, + }, + }, + }, + Default: []Behavior{ + { + Hostname: "origin-www.example.com", + OriginType: OriginTypeCustomer, + }, + { + Hostname: "origin-www.example2.com", + OriginType: OriginTypeNetStorage, + }, + }, + }, + }, + "200 OK - empty behaviors": { + params: ListOriginsRequest{ + PropertyID: 1, + ContractID: "TestContractID", + GroupID: 11, + }, + responseStatus: 200, + responseBody: ` +{ + "default":[ + { + "originType":"CUSTOMER", + "hostname":"test.com" + } + ], + "children":[ + { + "name":"Default CORS Policy", + "behaviors":[ + + ] + } + ] +}`, + expectedPath: "/cloud-wrapper/v1/properties/1/origins?contractId=TestContractID&groupId=11", + expectedResponse: &ListOriginsResponse{ + Children: []Child{ + { + Name: "Default CORS Policy", + Behaviors: []Behavior{}, + }, + }, + Default: []Behavior{ + { + Hostname: "test.com", + OriginType: OriginTypeCustomer, + }, + }, + }, + }, + "missing required params - validation errors": { + params: ListOriginsRequest{ + PropertyID: 0, + ContractID: "", + GroupID: 0, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "list origins: struct validation: ContractID: cannot be blank\nGroupID: cannot be blank\nPropertyID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: ListOriginsRequest{ + PropertyID: 1, + ContractID: "TestContractID", + GroupID: 11, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/properties/1/origins?contractId=TestContractID&groupId=11", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.ListOrigins(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} From 1ff8979dbf6cf8247541da858fc1e1f5fce37502 Mon Sep 17 00:00:00 2001 From: Dawid Dzhafarov Date: Tue, 30 May 2023 07:58:38 +0000 Subject: [PATCH 05/17] DXE-2622 Implement Configurations interface for CloudWrapper --- CHANGELOG.md | 10 +- pkg/cloudwrapper/cloudwrapper.go | 1 + pkg/cloudwrapper/configurations.go | 501 ++++ pkg/cloudwrapper/configurations_test.go | 2810 +++++++++++++++++++++++ pkg/cloudwrapper/mocks.go | 47 +- 5 files changed, 3365 insertions(+), 4 deletions(-) create mode 100644 pkg/cloudwrapper/configurations.go create mode 100644 pkg/cloudwrapper/configurations_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a068f0cb..233b4ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,16 @@ #### FEATURES/ENHANCEMENTS: * [IMPORTANT] Added CloudWrapper API support - * Locations - * [ListLocations](https://techdocs.akamai.com/cloud-wrapper/reference/get-locations) * Capacities * [ListCapacities](https://techdocs.akamai.com/cloud-wrapper/reference/get-capacity-inventory) + * Configurations + * [GetConfiguration](https://techdocs.akamai.com/cloud-wrapper/reference/get-configuration) + * [ListConfigurations](https://techdocs.akamai.com/cloud-wrapper/reference/get-configurations) + * [CreateConfiguration](https://techdocs.akamai.com/cloud-wrapper/reference/post-configuration) + * [UpdateConfiguration](https://techdocs.akamai.com/cloud-wrapper/reference/put-configuration) + * [ActivateConfiguration](https://techdocs.akamai.com/cloud-wrapper/reference/post-configuration-activations) + * Locations + * [ListLocations](https://techdocs.akamai.com/cloud-wrapper/reference/get-locations) * Properties * [ListProperties](https://techdocs.akamai.com/cloud-wrapper/reference/get-properties) * [ListOrigins](https://techdocs.akamai.com/cloud-wrapper/reference/get-origins) diff --git a/pkg/cloudwrapper/cloudwrapper.go b/pkg/cloudwrapper/cloudwrapper.go index 8ce0491f..0645e214 100644 --- a/pkg/cloudwrapper/cloudwrapper.go +++ b/pkg/cloudwrapper/cloudwrapper.go @@ -16,6 +16,7 @@ type ( // CloudWrapper is the api interface for Cloud Wrapper CloudWrapper interface { Capacities + Configurations Locations Properties } diff --git a/pkg/cloudwrapper/configurations.go b/pkg/cloudwrapper/configurations.go new file mode 100644 index 00000000..deb1785e --- /dev/null +++ b/pkg/cloudwrapper/configurations.go @@ -0,0 +1,501 @@ +package cloudwrapper + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // Configurations is a CloudWrapper configurations API interface + Configurations interface { + // GetConfiguration gets a specific Cloud Wrapper configuration + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-configuration + GetConfiguration(context.Context, GetConfigurationRequest) (*Configuration, error) + // ListConfigurations lists all Cloud Wrapper configurations on your contract + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-configurations + ListConfigurations(context.Context) (*ListConfigurationsResponse, error) + // CreateConfiguration creates a Cloud Wrapper configuration + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/post-configuration + CreateConfiguration(context.Context, CreateConfigurationRequest) (*Configuration, error) + // UpdateConfiguration updates a saved or inactive configuration + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/put-configuration + UpdateConfiguration(context.Context, UpdateConfigurationRequest) (*Configuration, error) + // ActivateConfiguration activates a Cloud Wrapper configuration + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/post-configuration-activations + ActivateConfiguration(context.Context, ActivateConfigurationRequest) error + } + + // GetConfigurationRequest holds parameters for GetConfiguration + GetConfigurationRequest struct { + ConfigID int64 + } + + // CreateConfigurationRequest holds parameters for CreateConfiguration + CreateConfigurationRequest struct { + Activate bool + Body CreateConfigurationBody + } + + // CreateConfigurationBody holds request body parameters for CreateConfiguration + CreateConfigurationBody struct { + CapacityAlertsThreshold *int `json:"capacityAlertsThreshold,omitempty"` + Comments string `json:"comments"` + ContractID string `json:"contractId"` + Locations []ConfigurationLocation `json:"locations"` + MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings,omitempty"` + ConfigName string `json:"configName"` + NotificationEmails []string `json:"notificationEmails,omitempty"` + PropertyIDs []string `json:"propertyIds"` + RetainIdleObjects bool `json:"retainIdleObjects,omitempty"` + } + + // UpdateConfigurationRequest holds parameters for UpdateConfiguration + UpdateConfigurationRequest struct { + ConfigID int64 + Activate bool + Body UpdateConfigurationBody + } + + // UpdateConfigurationBody holds request body parameters for UpdateConfiguration + UpdateConfigurationBody struct { + CapacityAlertsThreshold *int `json:"capacityAlertsThreshold,omitempty"` + Comments string `json:"comments"` + Locations []ConfigurationLocation `json:"locations"` + MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings,omitempty"` + NotificationEmails []string `json:"notificationEmails,omitempty"` + PropertyIDs []string `json:"propertyIds"` + RetainIdleObjects bool `json:"retainIdleObjects,omitempty"` + } + + // ActivateConfigurationRequest holds parameters for ActivateConfiguration + ActivateConfigurationRequest struct { + ConfigurationIDs []int `json:"configurationIds"` + } + + // Configuration represents CloudWrapper configuration + Configuration struct { + CapacityAlertsThreshold *int `json:"capacityAlertsThreshold"` + Comments string `json:"comments"` + ContractID string `json:"contractId"` + ConfigID int64 `json:"configId"` + Locations []ConfigurationLocation `json:"locations"` + MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings"` + Status string `json:"status"` + ConfigName string `json:"configName"` + LastUpdatedBy string `json:"lastUpdatedBy"` + LastUpdatedDate string `json:"lastUpdatedDate"` + LastActivatedBy *string `json:"lastActivatedBy"` + LastActivatedDate *string `json:"lastActivatedDate"` + NotificationEmails []string `json:"notificationEmails"` + PropertyIDs []string `json:"propertyIds"` + RetainIdleObjects bool `json:"retainIdleObjects"` + } + + // ListConfigurationsResponse contains response from ListConfigurations + ListConfigurationsResponse struct { + Configurations []Configuration `json:"configurations"` + } + + // ConfigurationLocation represents location to be configured for the configuration + ConfigurationLocation struct { + Comments string `json:"comments"` + TrafficTypeID int `json:"trafficTypeId"` + Capacity Capacity `json:"capacity"` + } + + // MultiCDNSettings represents details about Multi CDN Settings + MultiCDNSettings struct { + BOCC *BOCC `json:"bocc"` + CDNs []CDN `json:"cdns"` + DataStreams *DataStreams `json:"dataStreams"` + EnableSoftAlerts bool `json:"enableSoftAlerts,omitempty"` + Origins []Origin `json:"origins"` + } + + // BOCC represents diagnostic data beacon details + BOCC struct { + ConditionalSamplingFrequency SamplingFrequency `json:"conditionalSamplingFrequency,omitempty"` + Enabled bool `json:"enabled"` + ForwardType ForwardType `json:"forwardType,omitempty"` + RequestType RequestType `json:"requestType,omitempty"` + SamplingFrequency SamplingFrequency `json:"samplingFrequency,omitempty"` + } + + // CDN represents a CDN added for the configuration + CDN struct { + CDNAuthKeys []CDNAuthKey `json:"cdnAuthKeys,omitempty"` + CDNCode string `json:"cdnCode"` + Enabled bool `json:"enabled"` + HTTPSOnly bool `json:"httpsOnly,omitempty"` + IPACLCIDRs []string `json:"ipAclCidrs,omitempty"` + } + + // CDNAuthKey represents auth key configured for the CDN + CDNAuthKey struct { + AuthKeyName string `json:"authKeyName"` + ExpiryDate string `json:"expiryDate,omitempty"` + HeaderName string `json:"headerName,omitempty"` + Secret string `json:"secret,omitempty"` + } + + // DataStreams represents data streams details + DataStreams struct { + DataStreamIDs []int64 `json:"dataStreamIds,omitempty"` + Enabled bool `json:"enabled"` + SamplingRate *int `json:"samplingRate,omitempty"` + } + + // Origin represents origin corresponding to the properties selected in the configuration + Origin struct { + Hostname string `json:"hostname"` + OriginID string `json:"originId"` + PropertyID int `json:"propertyId"` + } + + // SamplingFrequency is a type of sampling frequency. Either 'ZERO' or 'ONE_TENTH' + SamplingFrequency string + + // ForwardType is a type of forward + ForwardType string + + // RequestType is a type of request + RequestType string +) + +const ( + // SamplingFrequencyZero represents SamplingFrequency value of 'ZERO' + SamplingFrequencyZero SamplingFrequency = "ZERO" + // SamplingFrequencyOneTenth represents SamplingFrequency value of 'ONE_TENTH' + SamplingFrequencyOneTenth SamplingFrequency = "ONE_TENTH" + // ForwardTypeOriginOnly represents ForwardType value of 'ORIGIN_ONLY' + ForwardTypeOriginOnly ForwardType = "ORIGIN_ONLY" + // ForwardTypeMidgressOnly represents ForwardType value of 'MIDGRESS_ONLY' + ForwardTypeMidgressOnly ForwardType = "MIDGRESS_ONLY" + // ForwardTypeOriginAndMidgress represents ForwardType value of 'ORIGIN_AND_MIDGRESS' + ForwardTypeOriginAndMidgress ForwardType = "ORIGIN_AND_MIDGRESS" + // RequestTypeEdgeOnly represents RequestType value of 'EDGE_ONLY' + RequestTypeEdgeOnly RequestType = "EDGE_ONLY" + // RequestTypeEdgeAndMidgress represents RequestType value of 'EDGE_AND_MIDGRESS' + RequestTypeEdgeAndMidgress RequestType = "EDGE_AND_MIDGRESS" +) + +// Validate validates GetConfigurationRequest +func (r GetConfigurationRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(r.ConfigID, validation.Required), + }) +} + +// Validate validates CreateConfigurationRequest +func (r CreateConfigurationRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "Body": validation.Validate(r.Body, validation.Required), + }) +} + +// Validate validates CreateConfigurationBody +func (b CreateConfigurationBody) Validate() error { + return validation.Errors{ + "Comments": validation.Validate(b.Comments, validation.Required), + "Locations": validation.Validate(b.Locations, validation.Required), + "ConfigName": validation.Validate(b.ConfigName, validation.Required), + "ContractID": validation.Validate(b.ContractID, validation.Required), + "PropertyIDs": validation.Validate(b.PropertyIDs, validation.Required), + "MultiCDNSettings": validation.Validate(b.MultiCDNSettings), + "CapacityAlertsThreshold": validation.Validate(b.CapacityAlertsThreshold, validation.Min(50), validation.Max(100).Error(fmt.Sprintf("value '%d' is invalid. Must be between 50 and 100", b.CapacityAlertsThreshold))), + }.Filter() +} + +// Validate validates UpdateConfigurationRequest +func (r UpdateConfigurationRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(r.ConfigID, validation.Required), + "Body": validation.Validate(r.Body, validation.Required), + }) +} + +// Validate validates UpdateConfigurationBody +func (b UpdateConfigurationBody) Validate() error { + return validation.Errors{ + "Comments": validation.Validate(b.Comments, validation.Required), + "Locations": validation.Validate(b.Locations, validation.Required), + "PropertyIDs": validation.Validate(b.PropertyIDs, validation.Required), + "MultiCDNSettings": validation.Validate(b.MultiCDNSettings), + "CapacityAlertsThreshold": validation.Validate(b.CapacityAlertsThreshold, validation.Min(50), validation.Max(100).Error(fmt.Sprintf("value '%d' is invalid. Must be between 50 and 100", b.CapacityAlertsThreshold))), + }.Filter() +} + +// Validate validates ActivateConfigurationRequest +func (r ActivateConfigurationRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigurationIDs": validation.Validate(r.ConfigurationIDs, validation.Required), + }) +} + +// Validate validates ConfigurationLocation +func (c ConfigurationLocation) Validate() error { + return validation.Errors{ + "Comments": validation.Validate(c.Comments, validation.Required), + "Capacity": validation.Validate(c.Capacity, validation.Required), + "TrafficTypeID": validation.Validate(c.TrafficTypeID, validation.Required), + }.Filter() +} + +// Validate validates Capacity +func (c Capacity) Validate() error { + return validation.Errors{ + "Unit": validation.Validate(c.Unit, validation.Required, validation.In(UnitGB, UnitTB).Error(fmt.Sprintf("value '%s' is invalid. Must be one of: '%s', '%s'", c.Unit, UnitGB, UnitTB))), + "Value": validation.Validate(c.Value, validation.Required, validation.Min(1), validation.Max(10000000000)), + }.Filter() +} + +// Validate validates MultiCDNSettings +func (m MultiCDNSettings) Validate() error { + return validation.Errors{ + "BOCC": validation.Validate(m.BOCC, validation.Required), + "CDNs": validation.Validate(m.CDNs, validation.By(validateCDNs)), + "DataStreams": validation.Validate(m.DataStreams, validation.Required), + "Origins": validation.Validate(m.Origins, validation.Required), + }.Filter() +} + +// Validate validates BOCC +func (b BOCC) Validate() error { + return validation.Errors{ + "Enabled": validation.Validate(b.Enabled, validation.NotNil), + "ConditionalSamplingFrequency": validation.Validate(b.ConditionalSamplingFrequency, validation.Required.When(b.Enabled), validation.In(SamplingFrequencyZero, SamplingFrequencyOneTenth).Error(fmt.Sprintf("value '%s' is invalid. Must be one of: '%s', '%s'", b.ConditionalSamplingFrequency, SamplingFrequencyZero, SamplingFrequencyOneTenth))), + "ForwardType": validation.Validate(b.ForwardType, validation.Required.When(b.Enabled), validation.In(ForwardTypeOriginOnly, ForwardTypeMidgressOnly, ForwardTypeOriginAndMidgress).Error(fmt.Sprintf("value '%s' is invalid. Must be one of: '%s', '%s', '%s'", b.ForwardType, ForwardTypeOriginOnly, ForwardTypeMidgressOnly, ForwardTypeOriginAndMidgress))), + "RequestType": validation.Validate(b.RequestType, validation.Required.When(b.Enabled), validation.In(RequestTypeEdgeOnly, RequestTypeEdgeAndMidgress).Error(fmt.Sprintf("value '%s' is invalid. Must be one of: '%s', '%s'", b.RequestType, RequestTypeEdgeOnly, RequestTypeEdgeAndMidgress))), + "SamplingFrequency": validation.Validate(b.SamplingFrequency, validation.Required.When(b.Enabled), validation.In(SamplingFrequencyZero, SamplingFrequencyOneTenth).Error(fmt.Sprintf("value '%s' is invalid. Must be one of: '%s', '%s'", b.RequestType, SamplingFrequencyZero, SamplingFrequencyOneTenth))), + }.Filter() +} + +// Validate validates CDN +func (c CDN) Validate() error { + return validation.Errors{ + "CDNAuthKeys": validation.Validate(c.CDNAuthKeys), + "Enabled": validation.Validate(c.Enabled, validation.NotNil), + "CDNCode": validation.Validate(c.CDNCode, validation.Required), + }.Filter() +} + +// Validate validates CDNAuthKey +func (c CDNAuthKey) Validate() error { + return validation.Errors{ + "AuthKeyName": validation.Validate(c.AuthKeyName, validation.Required), + "Secret": validation.Validate(c.Secret, validation.Length(24, 24)), + }.Filter() +} + +// Validate validates DataStreams +func (d DataStreams) Validate() error { + return validation.Errors{ + "Enabled": validation.Validate(d.Enabled, validation.NotNil), + "SamplingRate": validation.Validate(d.SamplingRate, validation.When(d.SamplingRate != nil, validation.Min(1), validation.Max(100).Error(fmt.Sprintf("value '%d' is invalid. Must be between 1 and 100", d.SamplingRate)))), + }.Filter() +} + +// Validate validates Origin +func (o Origin) Validate() error { + return validation.Errors{ + "PropertyID": validation.Validate(o.PropertyID, validation.Required), + "Hostname": validation.Validate(o.Hostname, validation.Required), + "OriginID": validation.Validate(o.OriginID, validation.Required), + }.Filter() +} + +// validateCDNs validates CDNs by checking if at least one is enabled and one of authKeys or IP ACLs is specified +func validateCDNs(value interface{}) error { + v, ok := value.([]CDN) + if !ok { + return fmt.Errorf("type %T is invalid. Must be []CDN", value) + } + if v == nil { + return fmt.Errorf("cannot be blank") + } + var isEnabled bool + for _, cdn := range v { + if cdn.Enabled { + isEnabled = true + } + if cdn.CDNAuthKeys == nil && cdn.IPACLCIDRs == nil { + return fmt.Errorf("at least one authentication method is required for CDN. Either IP ACL or header authentication must be enabled") + } + } + if !isEnabled { + return fmt.Errorf("at least one of CDNs must be enabled") + } + + return nil +} + +var ( + // ErrGetConfiguration is returned when GetConfiguration fails + ErrGetConfiguration = errors.New("get configuration") + // ErrListConfigurations is returned when ListConfigurations fails + ErrListConfigurations = errors.New("list configurations") + // ErrCreateConfiguration is returned when CreateConfiguration fails + ErrCreateConfiguration = errors.New("create configuration") + // ErrUpdateConfiguration is returned when UpdateConfiguration fails + ErrUpdateConfiguration = errors.New("update configuration") + // ErrActivateConfiguration is returned when ActivateConfiguration fails + ErrActivateConfiguration = errors.New("activate configuration") +) + +func (c *cloudwrapper) GetConfiguration(ctx context.Context, params GetConfigurationRequest) (*Configuration, error) { + logger := c.Log(ctx) + logger.Debug("GetConfiguration") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetConfiguration, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/cloud-wrapper/v1/configurations/%d", params.ConfigID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetConfiguration, err) + } + + var result Configuration + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrGetConfiguration, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrGetConfiguration, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudwrapper) ListConfigurations(ctx context.Context) (*ListConfigurationsResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListConfigurations") + + uri := fmt.Sprintf("/cloud-wrapper/v1/configurations") + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListConfigurations, err) + } + + var result ListConfigurationsResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListConfigurations, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListConfigurations, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudwrapper) CreateConfiguration(ctx context.Context, params CreateConfigurationRequest) (*Configuration, error) { + logger := c.Log(ctx) + logger.Debug("CreateConfiguration") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrCreateConfiguration, ErrStructValidation, err) + } + + uri, err := url.Parse("/cloud-wrapper/v1/configurations") + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrCreateConfiguration, err) + } + + q := uri.Query() + q.Add("activate", strconv.FormatBool(params.Activate)) + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrCreateConfiguration, err) + } + + var result Configuration + resp, err := c.Exec(req, &result, params.Body) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrCreateConfiguration, err) + } + + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("%s: %w", ErrCreateConfiguration, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudwrapper) UpdateConfiguration(ctx context.Context, params UpdateConfigurationRequest) (*Configuration, error) { + logger := c.Log(ctx) + logger.Debug("UpdateConfiguration") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrUpdateConfiguration, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cloud-wrapper/v1/configurations/%d", params.ConfigID)) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrUpdateConfiguration, err) + } + + q := uri.Query() + q.Add("activate", strconv.FormatBool(params.Activate)) + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrUpdateConfiguration, err) + } + + var result Configuration + resp, err := c.Exec(req, &result, params.Body) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrUpdateConfiguration, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrUpdateConfiguration, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudwrapper) ActivateConfiguration(ctx context.Context, params ActivateConfigurationRequest) error { + logger := c.Log(ctx) + logger.Debug("ActivateConfiguration") + + if err := params.Validate(); err != nil { + return fmt.Errorf("%s: %w: %s", ErrActivateConfiguration, ErrStructValidation, err) + } + + uri := "/cloud-wrapper/v1/configurations/activate" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return fmt.Errorf("%w: failed to create request: %s", ErrActivateConfiguration, err) + } + + resp, err := c.Exec(req, nil, params) + if err != nil { + return fmt.Errorf("%w: request failed: %s", ErrActivateConfiguration, err) + } + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("%s: %w", ErrActivateConfiguration, c.Error(resp)) + } + + return nil +} diff --git a/pkg/cloudwrapper/configurations_test.go b/pkg/cloudwrapper/configurations_test.go new file mode 100644 index 00000000..170b0a69 --- /dev/null +++ b/pkg/cloudwrapper/configurations_test.go @@ -0,0 +1,2810 @@ +package cloudwrapper + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/tools" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetConfiguration(t *testing.T) { + tests := map[string]struct { + params GetConfigurationRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *Configuration + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetConfigurationRequest{ + ConfigID: 1, + }, + responseStatus: 200, + responseBody: ` +{ + "configId": 1, + "configName": "TestConfigName", + "contractId": "TestContractID", + "propertyIds": [ + "321", + "654" + ], + "comments": "TestComments", + "status": "ACTIVE", + "retainIdleObjects": false, + "locations": [ + { + "trafficTypeId": 1, + "comments": "TestComments", + "capacity": { + "value": 1, + "unit": "GB" + } + }, + { + "trafficTypeId": 2, + "comments": "TestComments", + "capacity": { + "value": 2, + "unit": "TB" + } + } + ], + "multiCdnSettings": { + "origins": [ + { + "originId": "TestOriginID", + "hostname": "TestHostname", + "propertyId": 321 + }, + { + "originId": "TestOriginID2", + "hostname": "TestHostname", + "propertyId": 654 + } + ], + "cdns": [ + { + "cdnCode": "TestCDNCode", + "enabled": true, + "cdnAuthKeys": [ + { + "authKeyName": "TestAuthKeyName" + } + ], + "ipAclCidrs": [], + "httpsOnly": false + }, + { + "cdnCode": "TestCDNCode", + "enabled": false, + "cdnAuthKeys": [ + { + "authKeyName": "TestAuthKeyName" + }, + { + "authKeyName": "TestAuthKeyName2" + } + ], + "ipAclCidrs": [ + "test1", + "test2" + ], + "httpsOnly": true + } + ], + "dataStreams": { + "enabled": true, + "dataStreamIds": [ + 11, + 22 + ], + "samplingRate": 999 + }, + "bocc": { + "enabled": false, + "conditionalSamplingFrequency": "ONE_TENTH", + "forwardType": "ORIGIN_AND_MIDGRESS", + "requestType": "EDGE_ONLY", + "samplingFrequency": "ZERO" + }, + "enableSoftAlerts": true + }, + "capacityAlertsThreshold": 75, + "notificationEmails": [ + "test@akamai.com" + ], + "lastUpdatedDate": "2023-05-10T09:55:37.000Z", + "lastUpdatedBy": "user", + "lastActivatedDate": "2023-05-10T10:14:49.379Z", + "lastActivatedBy": "user" +}`, + expectedPath: "/cloud-wrapper/v1/configurations/1", + expectedResponse: &Configuration{ + CapacityAlertsThreshold: tools.IntPtr(75), + Comments: "TestComments", + ContractID: "TestContractID", + ConfigID: 1, + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + { + Comments: "TestComments", + TrafficTypeID: 2, + Capacity: Capacity{ + Unit: "TB", + Value: 2, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + ConditionalSamplingFrequency: SamplingFrequencyOneTenth, + Enabled: false, + ForwardType: ForwardTypeOriginAndMidgress, + RequestType: RequestTypeEdgeOnly, + SamplingFrequency: SamplingFrequencyZero, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + HTTPSOnly: false, + IPACLCIDRs: []string{}, + }, + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + }, + { + AuthKeyName: "TestAuthKeyName2", + }, + }, + CDNCode: "TestCDNCode", + Enabled: false, + HTTPSOnly: true, + IPACLCIDRs: []string{ + "test1", + "test2", + }, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{11, 22}, + Enabled: true, + SamplingRate: tools.IntPtr(999), + }, + EnableSoftAlerts: true, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 321, + }, + { + Hostname: "TestHostname", + OriginID: "TestOriginID2", + PropertyID: 654, + }, + }, + }, + Status: "ACTIVE", + ConfigName: "TestConfigName", + LastUpdatedBy: "user", + LastUpdatedDate: "2023-05-10T09:55:37.000Z", + LastActivatedBy: tools.StringPtr("user"), + LastActivatedDate: tools.StringPtr("2023-05-10T10:14:49.379Z"), + NotificationEmails: []string{"test@akamai.com"}, + PropertyIDs: []string{ + "321", + "654", + }, + RetainIdleObjects: false, + }, + }, + "200 OK - minimal": { + params: GetConfigurationRequest{ + ConfigID: 1, + }, + responseStatus: 200, + responseBody: ` +{ + "configId":1, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"ACTIVE", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":null, + "capacityAlertsThreshold":null, + "notificationEmails":[], + "lastUpdatedDate":"2023-05-10T09:55:37.000Z", + "lastUpdatedBy":"user", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedPath: "/cloud-wrapper/v1/configurations/1", + expectedResponse: &Configuration{ + Comments: "TestComments", + ContractID: "TestContractID", + ConfigID: 1, + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + Status: "ACTIVE", + ConfigName: "TestConfigName", + LastUpdatedBy: "user", + LastUpdatedDate: "2023-05-10T09:55:37.000Z", + NotificationEmails: []string{}, + PropertyIDs: []string{ + "123", + }, + RetainIdleObjects: false, + }, + }, + "missing required params - validation error": { + params: GetConfigurationRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get configuration: struct validation: ConfigID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: GetConfigurationRequest{ + ConfigID: 3, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/configurations/3", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetConfiguration(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestListConfigurations(t *testing.T) { + tests := map[string]struct { + responseStatus int + responseBody string + expectedPath string + expectedResponse *ListConfigurationsResponse + withError error + }{ + "200 OK": { + responseStatus: 200, + responseBody: ` +{ + "configurations":[ + { + "configId":1, + "configName":"testcloudwrapper", + "contractId":"testContract", + "propertyIds":[ + "11" + ], + "comments":"testComments", + "status":"ACTIVE", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"usageNotes", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":null, + "capacityAlertsThreshold":75, + "notificationEmails":[ + "user@akamai.com" + ], + "lastUpdatedDate":"2023-05-10T09:55:37.000Z", + "lastUpdatedBy":"user", + "lastActivatedDate":"2023-05-10T10:14:49.379Z", + "lastActivatedBy":"user" + }, + { + "configId":2, + "configName":"testcloudwrappermcdn", + "contractId":"testContract2", + "propertyIds":[ + "22" + ], + "comments":"mcdn", + "status":"ACTIVE", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":2, + "comments":"mcdn", + "capacity":{ + "value":2, + "unit":"TB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"testOrigin", + "hostname":"hostname.example.com", + "propertyId":222 + } + ], + "cdns":[ + { + "cdnCode":"testCode2", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"authKeyTest2" + } + ], + "ipAclCidrs":[ + "2.2.2.2/22" + ], + "httpsOnly":true + } + ], + "dataStreams":{ + "enabled":false, + "dataStreamIds":[ + 2 + ] + }, + "bocc":{ + "enabled":false + }, + "enableSoftAlerts":true + }, + "capacityAlertsThreshold":75, + "notificationEmails":[ + "user@akamai.com" + ], + "lastUpdatedDate":"2023-05-10T09:55:37.000Z", + "lastUpdatedBy":"user", + "lastActivatedDate":"2023-05-10T10:14:49.379Z", + "lastActivatedBy":"user" + } + ] +}`, + expectedPath: "/cloud-wrapper/v1/configurations", + expectedResponse: &ListConfigurationsResponse{ + Configurations: []Configuration{ + { + CapacityAlertsThreshold: tools.IntPtr(75), + Comments: "testComments", + ContractID: "testContract", + ConfigID: 1, + Locations: []ConfigurationLocation{ + { + Comments: "usageNotes", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + Status: "ACTIVE", + ConfigName: "testcloudwrapper", + LastUpdatedBy: "user", + LastUpdatedDate: "2023-05-10T09:55:37.000Z", + LastActivatedBy: tools.StringPtr("user"), + LastActivatedDate: tools.StringPtr("2023-05-10T10:14:49.379Z"), + NotificationEmails: []string{"user@akamai.com"}, + PropertyIDs: []string{ + "11", + }, + RetainIdleObjects: false, + }, + { + CapacityAlertsThreshold: tools.IntPtr(75), + Comments: "mcdn", + ContractID: "testContract2", + ConfigID: 2, + Locations: []ConfigurationLocation{ + { + Comments: "mcdn", + TrafficTypeID: 2, + Capacity: Capacity{ + Unit: "TB", + Value: 2, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "authKeyTest2", + }, + }, + CDNCode: "testCode2", + Enabled: true, + HTTPSOnly: true, + IPACLCIDRs: []string{ + "2.2.2.2/22", + }, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{2}, + Enabled: false, + }, + EnableSoftAlerts: true, + Origins: []Origin{ + { + Hostname: "hostname.example.com", + OriginID: "testOrigin", + PropertyID: 222, + }, + }, + }, + Status: "ACTIVE", + ConfigName: "testcloudwrappermcdn", + LastUpdatedBy: "user", + LastUpdatedDate: "2023-05-10T09:55:37.000Z", + LastActivatedBy: tools.StringPtr("user"), + LastActivatedDate: tools.StringPtr("2023-05-10T10:14:49.379Z"), + NotificationEmails: []string{"user@akamai.com"}, + PropertyIDs: []string{"22"}, + RetainIdleObjects: false, + }, + }, + }, + }, + "500 internal server error": { + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/configurations", + withError: &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.ListConfigurations(context.Background()) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestCreateConfiguration(t *testing.T) { + tests := map[string]struct { + params CreateConfigurationRequest + expectedRequestBody string + expectedPath string + responseStatus int + responseBody string + expectedResponse *Configuration + withError func(*testing.T, error) + }{ + "200 OK - minimal": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: UnitGB, + Value: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestComments", + "trafficTypeId":1 + } + ], + "propertyIds":[ + "123" + ], + "contractId":"TestContractID", + "comments":"TestComments", + "configName":"TestConfigName" +}`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=false", + responseStatus: 201, + responseBody: ` +{ + "configId":111, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":null, + "capacityAlertsThreshold":50, + "notificationEmails":[ + + ], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + CapacityAlertsThreshold: tools.IntPtr(50), + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: UnitGB, + Value: 1, + }, + }, + }, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{}, + PropertyIDs: []string{"123"}, + ConfigID: 111, + }, + }, + "200 OK - minimal with activate query param": { + params: CreateConfigurationRequest{ + Activate: true, + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestComments", + "trafficTypeId":1 + } + ], + "propertyIds":[ + "123" + ], + "contractId":"TestContractID", + "comments":"TestComments", + "configName":"TestConfigName" +}`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=true", + responseStatus: 201, + responseBody: ` +{ + "configId":111, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":null, + "capacityAlertsThreshold":50, + "notificationEmails":[ + + ], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + CapacityAlertsThreshold: tools.IntPtr(50), + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{}, + PropertyIDs: []string{"123"}, + ConfigID: 111, + }, + }, + "200 OK - minimal MultiCDNSettings": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + ExpiryDate: "TestExpiryDate", + HeaderName: "TestHeaderName", + Secret: "testtesttesttesttesttest", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestComments", + "trafficTypeId":1 + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName", + "headerName":"TestHeaderName", + "secret":"testtesttesttesttesttest", + "expiryDate":"TestExpiryDate" + } + ] + } + ], + "dataStreams":{ + "enabled":false + }, + "bocc":{ + "enabled":false + } + }, + "propertyIds":[ + "123" + ], + "contractId":"TestContractID", + "comments":"TestComments", + "configName":"TestConfigName" +}`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=false", + responseStatus: 201, + responseBody: ` +{ + "configId":111, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName", + "headerName":"TestHeaderName", + "secret":"testtesttesttesttesttest", + "expiryDate":"TestExpiryDate" + } + ], + "ipAclCidrs":[], + "httpsOnly":false + } + ], + "dataStreams":{ + "enabled":false + }, + "bocc":{ + "enabled":false + }, + "enableSoftAlerts":false + }, + "capacityAlertsThreshold":null, + "notificationEmails":[], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + CapacityAlertsThreshold: nil, + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + ExpiryDate: "TestExpiryDate", + HeaderName: "TestHeaderName", + Secret: "testtesttesttesttesttest", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{}, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + EnableSoftAlerts: false, + }, + RetainIdleObjects: false, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{}, + PropertyIDs: []string{"123"}, + ConfigID: 111, + }, + }, + "200 OK - full MultiCDNSettings": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + CapacityAlertsThreshold: tools.IntPtr(70), + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + { + Comments: "TestComments2", + TrafficTypeID: 2, + Capacity: Capacity{ + Unit: "TB", + Value: 2, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + ConditionalSamplingFrequency: SamplingFrequencyZero, + Enabled: true, + ForwardType: ForwardTypeOriginAndMidgress, + RequestType: RequestTypeEdgeAndMidgress, + SamplingFrequency: SamplingFrequencyZero, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + ExpiryDate: "TestExpiryDate", + HeaderName: "TestHeaderName", + Secret: "testtesttesttesttesttest", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + HTTPSOnly: true, + }, + { + CDNCode: "TestCDNCode", + Enabled: true, + HTTPSOnly: true, + IPACLCIDRs: []string{ + "1.1.1.1/1", + }, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{1}, + Enabled: true, + SamplingRate: tools.IntPtr(10), + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + { + Hostname: "TestHostname2", + OriginID: "TestOriginID2", + PropertyID: 1234, + }, + }, + EnableSoftAlerts: true, + }, + ConfigName: "TestConfigName", + NotificationEmails: []string{ + "test@test.com", + }, + PropertyIDs: []string{"123"}, + RetainIdleObjects: true, + }, + }, + expectedRequestBody: ` +{ + "capacityAlertsThreshold":70, + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestComments", + "trafficTypeId":1 + }, + { + "capacity":{ + "value":2, + "unit":"TB" + }, + "comments":"TestComments2", + "trafficTypeId":2 + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + }, + { + "originId":"TestOriginID2", + "hostname":"TestHostname2", + "propertyId":1234 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName", + "headerName":"TestHeaderName", + "secret":"testtesttesttesttesttest", + "expiryDate":"TestExpiryDate" + } + ], + "httpsOnly":true + }, + { + "cdnCode":"TestCDNCode", + "enabled":true, + "httpsOnly":true, + "ipAclCidrs": [ + "1.1.1.1/1" + ] + } + ], + "dataStreams":{ + "enabled":true, + "dataStreamIds": [ + 1 + ], + "samplingRate": 10 + }, + "bocc":{ + "enabled":true, + "conditionalSamplingFrequency": "ZERO", + "forwardType": "ORIGIN_AND_MIDGRESS", + "requestType": "EDGE_AND_MIDGRESS", + "samplingFrequency": "ZERO" + }, + "enableSoftAlerts":true + }, + "propertyIds":[ + "123" + ], + "notificationEmails": [ + "test@test.com" + ], + "retainIdleObjects":true, + "contractId":"TestContractID", + "comments":"TestComments", + "configName":"TestConfigName" +} +`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=false", + responseStatus: 201, + responseBody: ` +{ + "configId":111, + "capacityAlertsThreshold": 70, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"IN_PROGRESS", + "retainIdleObjects":true, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":1, + "unit":"GB" + } + }, + { + "trafficTypeId":2, + "comments":"TestComments2", + "capacity":{ + "value":2, + "unit":"TB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + }, + { + "originId":"TestOriginID2", + "hostname":"TestHostname2", + "propertyId":1234 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName", + "headerName":"TestHeaderName", + "secret":"testtesttesttesttesttest", + "expiryDate":"TestExpiryDate" + } + ], + "ipAclCidrs":[], + "httpsOnly":true + }, + { + "cdnCode":"TestCDNCode", + "enabled":true, + "httpsOnly":true, + "ipAclCidrs": [ + "1.1.1.1/1" + ] + } + ], + "dataStreams":{ + "enabled":true, + "dataStreamIds": [ + 1 + ], + "samplingRate": 10 + }, + "bocc":{ + "enabled":true, + "conditionalSamplingFrequency": "ZERO", + "forwardType": "ORIGIN_AND_MIDGRESS", + "requestType": "EDGE_AND_MIDGRESS", + "samplingFrequency": "ZERO" + }, + "enableSoftAlerts": true + }, + "notificationEmails":[ + "test@test.com" + ], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + CapacityAlertsThreshold: tools.IntPtr(70), + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + { + Comments: "TestComments2", + TrafficTypeID: 2, + Capacity: Capacity{ + Unit: "TB", + Value: 2, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + ConditionalSamplingFrequency: SamplingFrequencyZero, + Enabled: true, + ForwardType: ForwardTypeOriginAndMidgress, + RequestType: RequestTypeEdgeAndMidgress, + SamplingFrequency: SamplingFrequencyZero, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + ExpiryDate: "TestExpiryDate", + HeaderName: "TestHeaderName", + Secret: "testtesttesttesttesttest", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{}, + HTTPSOnly: true, + }, + { + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{"1.1.1.1/1"}, + HTTPSOnly: true, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{1}, + Enabled: true, + SamplingRate: tools.IntPtr(10), + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + { + Hostname: "TestHostname2", + OriginID: "TestOriginID2", + PropertyID: 1234, + }, + }, + EnableSoftAlerts: true, + }, + RetainIdleObjects: true, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{"test@test.com"}, + PropertyIDs: []string{"123"}, + ConfigID: 111, + }, + }, + "200 OK - BOCC struct fields default values": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{}, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + }, + }, + DataStreams: &DataStreams{ + Enabled: true, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":10, + "unit":"GB" + }, + "comments":"TestComments", + "trafficTypeId":1 + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName" + } + ] + } + ], + "dataStreams":{ + "enabled":true + }, + "bocc":{ + "enabled":false + } + }, + "propertyIds":[ + "123" + ], + "contractId":"TestContractID", + "comments":"TestComments", + "configName":"TestConfigName" +}`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=false", + responseBody: ` +{ + "configId":111, + "capacityAlertsThreshold":null, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":10, + "unit":"GB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName" + } + ], + "ipAclCidrs":[] + } + ], + "dataStreams":{ + "enabled":true + }, + "bocc":{ + "enabled":false + }, + "enableSoftAlerts":false + }, + "notificationEmails":[ + + ], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + responseStatus: 201, + expectedResponse: &Configuration{ + Comments: "TestComments", + ContractID: "TestContractID", + Status: "IN_PROGRESS", + ConfigID: 111, + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{}, + }, + }, + DataStreams: &DataStreams{ + Enabled: true, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + NotificationEmails: []string{}, + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + }, + }, + "200 OK - DataStreams struct fields default values": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{}, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + }, + }, + DataStreams: &DataStreams{}, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":10, + "unit":"GB" + }, + "comments":"TestComments", + "trafficTypeId":1 + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName" + } + ] + } + ], + "dataStreams":{ + "enabled":false + }, + "bocc":{ + "enabled":false + } + }, + "propertyIds":[ + "123" + ], + "contractId":"TestContractID", + "comments":"TestComments", + "configName":"TestConfigName" +}`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=false", + responseBody: ` +{ + "configId":111, + "capacityAlertsThreshold":null, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestComments", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestComments", + "capacity":{ + "value":10, + "unit":"GB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName" + } + ], + "ipAclCidrs":[] + } + ], + "dataStreams":{ + "enabled":false, + "dataStreamsIds": [] + }, + "bocc":{ + "enabled":false + }, + "enableSoftAlerts":false + }, + "notificationEmails":[], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + responseStatus: 201, + expectedResponse: &Configuration{ + Comments: "TestComments", + ContractID: "TestContractID", + Status: "IN_PROGRESS", + ConfigID: 111, + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{}, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + NotificationEmails: []string{}, + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + }, + }, + "missing required params: comments, configName, contractID, locations and propertyIDs - validation error": { + params: CreateConfigurationRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: Comments: cannot be blank\nConfigName: cannot be blank\nContractID: cannot be blank\nLocations: cannot be blank\nPropertyIDs: cannot be blank", err.Error()) + }, + }, + "missing required params - location fields": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "", + TrafficTypeID: 0, + Capacity: Capacity{}, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: Locations[0]: {\n\tUnit: cannot be blank\n\tValue: cannot be blank\n\tComments: cannot be blank\n\tTrafficTypeID: cannot be blank\n}", err.Error()) + }, + }, + "missing required params - multiCDN fields": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 5, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{}, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: BOCC: cannot be blank\nCDNs: cannot be blank\nDataStreams: cannot be blank\nOrigins: cannot be blank", err.Error()) + }, + }, + "missing required params - BOCC struct fields when enabled": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 5, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: true, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + {AuthKeyName: "TestAuthKeyName"}, + }, + CDNCode: "TestCDNCode", + Enabled: true, + }, + }, + DataStreams: &DataStreams{ + Enabled: true, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: ConditionalSamplingFrequency: cannot be blank\nForwardType: cannot be blank\nRequestType: cannot be blank\nSamplingFrequency: cannot be blank", err.Error()) + }, + }, + "missing required params - Origin struct fields": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 5, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{"1.1.1.1/1"}, + }, + }, + DataStreams: &DataStreams{ + Enabled: true, + }, + Origins: []Origin{ + {}, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: Origins[0]: {\n\tHostname: cannot be blank\n\tOriginID: cannot be blank\n\tPropertyID: cannot be blank\n}", err.Error()) + }, + }, + "validation error - at least one CDN must be enabled": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 5, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNCode: "TestCDNCode", + Enabled: false, + IPACLCIDRs: []string{"1.1.1.1/1"}, + }, + { + CDNCode: "TestCDNCode", + Enabled: false, + IPACLCIDRs: []string{"1.1.1.1/1"}, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: CDNs: at least one of CDNs must be enabled", err.Error()) + }, + }, + "validation error - authKeys nor IPACLCIDRs specified": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 5, + Capacity: Capacity{ + Unit: "GB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNCode: "TestCDNCode", + Enabled: false, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: CDNs: at least one authentication method is required for CDN. Either IP ACL or header authentication must be enabled", err.Error()) + }, + }, + "struct fields validations": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + CapacityAlertsThreshold: tools.IntPtr(20), + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 5, + Capacity: Capacity{ + Unit: "MB", + Value: 10, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + ConditionalSamplingFrequency: "a", + Enabled: false, + ForwardType: "a", + RequestType: "a", + SamplingFrequency: "a", + }, + CDNs: []CDN{ + { + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{"1.1.1.1/1"}, + }, + { + CDNAuthKeys: []CDNAuthKey{ + {}, + }, + CDNCode: "TestCDNCode", + Enabled: true, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{1}, + Enabled: true, + SamplingRate: tools.IntPtr(-10), + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"1"}, + }, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create configuration: struct validation: CapacityAlertsThreshold: must be no less than 50\nLocations[0]: {\n\tUnit: value 'MB' is invalid. Must be one of: 'GB', 'TB'\n}\nConditionalSamplingFrequency: value 'a' is invalid. Must be one of: 'ZERO', 'ONE_TENTH'\nForwardType: value 'a' is invalid. Must be one of: 'ORIGIN_ONLY', 'MIDGRESS_ONLY', 'ORIGIN_AND_MIDGRESS'\nRequestType: value 'a' is invalid. Must be one of: 'EDGE_ONLY', 'EDGE_AND_MIDGRESS'\nSamplingFrequency: value 'a' is invalid. Must be one of: 'ZERO', 'ONE_TENTH'\nCDNs[1]: {\n\tCDNAuthKeys[0]: {\n\t\tAuthKeyName: cannot be blank\n\t}\n}\nSamplingRate: must be no less than 1", err.Error()) + }, + }, + "500 internal server error": { + params: CreateConfigurationRequest{ + Body: CreateConfigurationBody{ + Comments: "TestComments", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestComments", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + ConfigName: "TestConfigName", + PropertyIDs: []string{"123"}, + }, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/configurations?activate=false", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + if test.expectedRequestBody != "" { + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.JSONEq(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreateConfiguration(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestUpdateConfiguration(t *testing.T) { + tests := map[string]struct { + params UpdateConfigurationRequest + expectedRequestBody string + expectedPath string + responseStatus int + responseBody string + expectedResponse *Configuration + withError func(*testing.T, error) + }{ + "200 OK - minimal": { + params: UpdateConfigurationRequest{ + ConfigID: 111, + Body: UpdateConfigurationBody{ + Comments: "TestCommentsUpdated", + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + PropertyIDs: []string{ + "123", + }, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestCommentsUpdated", + "trafficTypeId":1 + } + ], + "propertyIds":[ + "123" + ], + "comments":"TestCommentsUpdated" +}`, + expectedPath: "/cloud-wrapper/v1/configurations/111?activate=false", + responseStatus: 200, + responseBody: ` +{ + "configId":111, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestCommentsUpdated", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestCommentsUpdated", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":null, + "capacityAlertsThreshold":50, + "notificationEmails":[ + + ], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + ConfigID: 111, + CapacityAlertsThreshold: tools.IntPtr(50), + Comments: "TestCommentsUpdated", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{}, + PropertyIDs: []string{"123"}, + RetainIdleObjects: false, + }, + }, + "200 OK - minimal MultiCDNSettings": { + params: UpdateConfigurationRequest{ + ConfigID: 111, + Body: UpdateConfigurationBody{ + Comments: "TestCommentsUpdated", + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + PropertyIDs: []string{ + "123", + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{ + "1.1.1.1/1", + }, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + }, + }, + expectedRequestBody: ` +{ + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestCommentsUpdated", + "trafficTypeId":1 + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "ipAclCidrs":[ + "1.1.1.1/1" + ] + } + ], + "dataStreams":{ + "enabled":false + }, + "bocc":{ + "enabled":false + } + }, + "propertyIds":[ + "123" + ], + "comments":"TestCommentsUpdated" +}`, + expectedPath: "/cloud-wrapper/v1/configurations/111?activate=false", + responseStatus: 200, + responseBody: ` +{ + "configId":111, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestCommentsUpdated", + "status":"IN_PROGRESS", + "retainIdleObjects":false, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestCommentsUpdated", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[], + "ipAclCidrs":[ + "1.1.1.1/1" + ], + "httpsOnly":false + } + ], + "dataStreams":{ + "enabled":false + }, + "bocc":{ + "enabled":false + }, + "enableSoftAlerts":false + }, + "capacityAlertsThreshold":null, + "notificationEmails":[], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + ConfigID: 111, + Comments: "TestCommentsUpdated", + ContractID: "TestContractID", + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + Enabled: false, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{}, + CDNCode: "TestCDNCode", + Enabled: true, + HTTPSOnly: false, + IPACLCIDRs: []string{ + "1.1.1.1/1", + }, + }, + }, + DataStreams: &DataStreams{ + Enabled: false, + }, + EnableSoftAlerts: false, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{}, + PropertyIDs: []string{"123"}, + RetainIdleObjects: false, + }, + }, + "200 OK - all fields": { + params: UpdateConfigurationRequest{ + ConfigID: 111, + Body: UpdateConfigurationBody{ + CapacityAlertsThreshold: tools.IntPtr(80), + Comments: "TestCommentsUpdated", + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + ConditionalSamplingFrequency: SamplingFrequencyZero, + Enabled: true, + ForwardType: ForwardTypeOriginAndMidgress, + RequestType: RequestTypeEdgeAndMidgress, + SamplingFrequency: SamplingFrequencyZero, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + ExpiryDate: "TestExpiryDate", + HeaderName: "TestHeaderName", + Secret: "TestSecretTestSecret1234", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{ + "1.1.1.1/1", + }, + HTTPSOnly: true, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{1}, + Enabled: true, + SamplingRate: tools.IntPtr(10), + }, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + NotificationEmails: []string{ + "test@test.com", + }, + PropertyIDs: []string{ + "123", + }, + RetainIdleObjects: true, + }, + }, + expectedRequestBody: ` +{ + "capacityAlertsThreshold":80, + "locations":[ + { + "capacity":{ + "value":1, + "unit":"GB" + }, + "comments":"TestCommentsUpdated", + "trafficTypeId":1 + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName", + "expiryDate":"TestExpiryDate", + "headerName":"TestHeaderName", + "secret":"TestSecretTestSecret1234" + } + ], + "cdnCode":"TestCDNCode", + "enabled":true, + "ipAclCidrs":[ + "1.1.1.1/1" + ], + "httpsOnly":true + } + ], + "dataStreams":{ + "enabled":true, + "dataStreamIds":[ + 1 + ], + "samplingRate":10 + }, + "bocc":{ + "enabled":true, + "conditionalSamplingFrequency":"ZERO", + "forwardType":"ORIGIN_AND_MIDGRESS", + "requestType":"EDGE_AND_MIDGRESS", + "samplingFrequency":"ZERO" + } + }, + "propertyIds":[ + "123" + ], + "notificationEmails":[ + "test@test.com" + ], + "retainIdleObjects":true, + "comments":"TestCommentsUpdated" +} +`, + expectedPath: "/cloud-wrapper/v1/configurations/111?activate=false", + responseStatus: 200, + responseBody: ` +{ + "configId":111, + "configName":"TestConfigName", + "contractId":"TestContractID", + "propertyIds":[ + "123" + ], + "comments":"TestCommentsUpdated", + "status":"IN_PROGRESS", + "retainIdleObjects":true, + "locations":[ + { + "trafficTypeId":1, + "comments":"TestCommentsUpdated", + "capacity":{ + "value":1, + "unit":"GB" + } + } + ], + "multiCdnSettings":{ + "origins":[ + { + "originId":"TestOriginID", + "hostname":"TestHostname", + "propertyId":123 + } + ], + "cdns":[ + { + "cdnCode":"TestCDNCode", + "enabled":true, + "cdnAuthKeys":[ + { + "authKeyName":"TestAuthKeyName", + "expiryDate":"TestExpiryDate", + "headerName":"TestHeaderName", + "secret":"TestSecretTestSecret1234" + } + ], + "ipAclCidrs":[ + "1.1.1.1/1" + ], + "httpsOnly":true + } + ], + "dataStreams":{ + "enabled":true, + "dataStreamIds":[ + 1 + ], + "samplingRate":10 + }, + "bocc":{ + "enabled":true, + "conditionalSamplingFrequency":"ZERO", + "forwardType":"ORIGIN_AND_MIDGRESS", + "requestType":"EDGE_AND_MIDGRESS", + "samplingFrequency":"ZERO" + }, + "enableSoftAlerts":true + }, + "capacityAlertsThreshold":80, + "notificationEmails":[ + "test@test.com" + ], + "lastUpdatedDate":"2022-06-10T13:21:14.488Z", + "lastUpdatedBy":"johndoe", + "lastActivatedDate":null, + "lastActivatedBy":null +}`, + expectedResponse: &Configuration{ + CapacityAlertsThreshold: tools.IntPtr(80), + Comments: "TestCommentsUpdated", + ContractID: "TestContractID", + ConfigID: 111, + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + MultiCDNSettings: &MultiCDNSettings{ + BOCC: &BOCC{ + ConditionalSamplingFrequency: SamplingFrequencyZero, + Enabled: true, + ForwardType: ForwardTypeOriginAndMidgress, + RequestType: RequestTypeEdgeAndMidgress, + SamplingFrequency: SamplingFrequencyZero, + }, + CDNs: []CDN{ + { + CDNAuthKeys: []CDNAuthKey{ + { + AuthKeyName: "TestAuthKeyName", + ExpiryDate: "TestExpiryDate", + HeaderName: "TestHeaderName", + Secret: "TestSecretTestSecret1234", + }, + }, + CDNCode: "TestCDNCode", + Enabled: true, + IPACLCIDRs: []string{ + "1.1.1.1/1", + }, + HTTPSOnly: true, + }, + }, + DataStreams: &DataStreams{ + DataStreamIDs: []int64{1}, + Enabled: true, + SamplingRate: tools.IntPtr(10), + }, + EnableSoftAlerts: true, + Origins: []Origin{ + { + Hostname: "TestHostname", + OriginID: "TestOriginID", + PropertyID: 123, + }, + }, + }, + Status: "IN_PROGRESS", + ConfigName: "TestConfigName", + LastUpdatedBy: "johndoe", + LastUpdatedDate: "2022-06-10T13:21:14.488Z", + NotificationEmails: []string{ + "test@test.com", + }, + PropertyIDs: []string{"123"}, + RetainIdleObjects: true, + }, + }, + "missing required params - validation error": { + params: UpdateConfigurationRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "update configuration: struct validation: Comments: cannot be blank\nLocations: cannot be blank\nPropertyIDs: cannot be blank\nConfigID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: UpdateConfigurationRequest{ + ConfigID: 1, + Body: UpdateConfigurationBody{ + Comments: "TestCommentsUpdated", + Locations: []ConfigurationLocation{ + { + Comments: "TestCommentsUpdated", + TrafficTypeID: 1, + Capacity: Capacity{ + Unit: "GB", + Value: 1, + }, + }, + }, + PropertyIDs: []string{"1"}, + }, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/configurations/1?activate=false", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + if test.expectedRequestBody != "" { + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.JSONEq(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateConfiguration(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestActivateConfiguration(t *testing.T) { + tests := map[string]struct { + params ActivateConfigurationRequest + expectedRequestBody string + responseStatus int + responseBody string + expectedPath string + withError func(*testing.T, error) + }{ + "204 - single configID": { + params: ActivateConfigurationRequest{ + ConfigurationIDs: []int{1}, + }, + expectedRequestBody: ` +{ + "configurationIds":[ + 1 + ] +}`, + responseStatus: 204, + expectedPath: "/cloud-wrapper/v1/configurations/activate", + }, + "204 - multiple configIDs": { + params: ActivateConfigurationRequest{ + ConfigurationIDs: []int{1, 2, 3}, + }, + expectedRequestBody: ` +{ + "configurationIds": [ + 1, + 2, + 3 + ] +}`, + responseStatus: 204, + expectedPath: "/cloud-wrapper/v1/configurations/activate", + }, + "missing required params - validation error": { + params: ActivateConfigurationRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "activate configuration: struct validation: ConfigurationIDs: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: ActivateConfigurationRequest{ + ConfigurationIDs: []int{1}, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/configurations/activate", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + if test.expectedRequestBody != "" { + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.JSONEq(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + err := client.ActivateConfiguration(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go index c37b0725..de943f66 100644 --- a/pkg/cloudwrapper/mocks.go +++ b/pkg/cloudwrapper/mocks.go @@ -14,7 +14,6 @@ type Mock struct { var _ CloudWrapper = &Mock{} -// ListCapacities implements CloudWrapper func (m *Mock) ListCapacities(ctx context.Context, req ListCapacitiesRequest) (*ListCapacitiesResponse, error) { args := m.Called(ctx, req) if args.Get(0) == nil { @@ -23,7 +22,6 @@ func (m *Mock) ListCapacities(ctx context.Context, req ListCapacitiesRequest) (* return args.Get(0).(*ListCapacitiesResponse), args.Error(1) } -// ListLocations implements CloudWrapper func (m *Mock) ListLocations(ctx context.Context) (*ListLocationResponse, error) { args := m.Called(ctx) @@ -53,3 +51,48 @@ func (m *Mock) ListOrigins(ctx context.Context, r ListOriginsRequest) (*ListOrig return args.Get(0).(*ListOriginsResponse), args.Error(1) } + +func (m *Mock) GetConfiguration(ctx context.Context, r GetConfigurationRequest) (*Configuration, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*Configuration), args.Error(1) +} + +func (m *Mock) ListConfigurations(ctx context.Context) (*ListConfigurationsResponse, error) { + args := m.Called(ctx) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListConfigurationsResponse), args.Error(1) +} + +func (m *Mock) CreateConfiguration(ctx context.Context, r CreateConfigurationRequest) (*Configuration, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*Configuration), args.Error(1) +} + +func (m *Mock) UpdateConfiguration(ctx context.Context, r UpdateConfigurationRequest) (*Configuration, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*Configuration), args.Error(1) +} + +func (m *Mock) ActivateConfiguration(ctx context.Context, r ActivateConfigurationRequest) error { + args := m.Called(ctx, r) + return args.Error(0) +} From 58229a49af877ecf4448ef971e50118c91145d06 Mon Sep 17 00:00:00 2001 From: Tatiana Slonimskaia Date: Tue, 30 May 2023 11:24:18 +0000 Subject: [PATCH 06/17] DXE-2649 Implementation for CloudWrapper Multi CDN --- CHANGELOG.md | 3 + pkg/cloudwrapper/cloudwrapper.go | 1 + pkg/cloudwrapper/mocks.go | 16 ++- pkg/cloudwrapper/multi_cdn.go | 130 ++++++++++++++++++++ pkg/cloudwrapper/multi_cdn_test.go | 190 +++++++++++++++++++++++++++++ 5 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 pkg/cloudwrapper/multi_cdn.go create mode 100644 pkg/cloudwrapper/multi_cdn_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 233b4ab3..820bb8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ * [ActivateConfiguration](https://techdocs.akamai.com/cloud-wrapper/reference/post-configuration-activations) * Locations * [ListLocations](https://techdocs.akamai.com/cloud-wrapper/reference/get-locations) + * MultiCDN + * [ListAuthKeys](https://techdocs.akamai.com/cloud-wrapper/reference/get-auth-keys) + * [ListCDNProviders](https://techdocs.akamai.com/cloud-wrapper/reference/get-providers) * Properties * [ListProperties](https://techdocs.akamai.com/cloud-wrapper/reference/get-properties) * [ListOrigins](https://techdocs.akamai.com/cloud-wrapper/reference/get-origins) diff --git a/pkg/cloudwrapper/cloudwrapper.go b/pkg/cloudwrapper/cloudwrapper.go index 0645e214..1125da47 100644 --- a/pkg/cloudwrapper/cloudwrapper.go +++ b/pkg/cloudwrapper/cloudwrapper.go @@ -18,6 +18,7 @@ type ( Capacities Configurations Locations + MultiCDN Properties } diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go index de943f66..1343b5cc 100644 --- a/pkg/cloudwrapper/mocks.go +++ b/pkg/cloudwrapper/mocks.go @@ -24,12 +24,26 @@ func (m *Mock) ListCapacities(ctx context.Context, req ListCapacitiesRequest) (* func (m *Mock) ListLocations(ctx context.Context) (*ListLocationResponse, error) { args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*ListLocationResponse), args.Error(1) +} +func (m *Mock) ListAuthKeys(ctx context.Context, req ListAuthKeysRequest) (*ListAuthKeysResponse, error) { + args := m.Called(ctx, req) if args.Get(0) == nil { return nil, args.Error(1) } + return args.Get(0).(*ListAuthKeysResponse), args.Error(1) +} - return args.Get(0).(*ListLocationResponse), args.Error(1) +func (m *Mock) ListCDNProviders(ctx context.Context) (*ListCDNProvidersResponse, error) { + args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*ListCDNProvidersResponse), args.Error(1) } func (m *Mock) ListProperties(ctx context.Context, r ListPropertiesRequest) (*ListPropertiesResponse, error) { diff --git a/pkg/cloudwrapper/multi_cdn.go b/pkg/cloudwrapper/multi_cdn.go new file mode 100644 index 00000000..3326d2db --- /dev/null +++ b/pkg/cloudwrapper/multi_cdn.go @@ -0,0 +1,130 @@ +package cloudwrapper + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // MultiCDN is the cloudwrapper Multi-CDN API interface + MultiCDN interface { + // ListAuthKeys lists the cdnAuthKeys for a specified contractId and cdnCode + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-auth-keys + ListAuthKeys(context.Context, ListAuthKeysRequest) (*ListAuthKeysResponse, error) + + // ListCDNProviders lists CDN providers + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-providers + ListCDNProviders(context.Context) (*ListCDNProvidersResponse, error) + } + + // ListAuthKeysRequest is a request struct + ListAuthKeysRequest struct { + ContractID string + CDNCode string + } + + // ListAuthKeysResponse contains response list of CDN auth keys + ListAuthKeysResponse struct { + CDNAuthKeys []MultiCDNAuthKey `json:"cdnAuthKeys"` + } + + // MultiCDNAuthKey contains CDN auth key information + MultiCDNAuthKey struct { + AuthKeyName string `json:"authKeyName"` + ExpiryDate string `json:"expiryDate"` + HeaderName string `json:"headerName"` + } + + // ListCDNProvidersResponse contains response list of CDN providers + ListCDNProvidersResponse struct { + CDNProviders []CDNProvider `json:"cdnProviders"` + } + + // CDNProvider contains CDN provider information + CDNProvider struct { + CDNCode string `json:"cdnCode"` + CDNName string `json:"cdnName"` + } +) + +// Validate validates ListAuthKeysRequest +func (r ListAuthKeysRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ContractID": validation.Validate(r.ContractID, validation.Required), + "CDNCode": validation.Validate(r.CDNCode, validation.Required), + }) +} + +var ( + // ErrListAuthKeys is returned in case an error occurs on ListAuthKeys operation + ErrListAuthKeys = errors.New("list auth keys") + // ErrListCDNProviders is returned in case an error occurs on ListCDNProviders operation + ErrListCDNProviders = errors.New("list CDN providers") +) + +func (c *cloudwrapper) ListAuthKeys(ctx context.Context, params ListAuthKeysRequest) (*ListAuthKeysResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListAuthKeys") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrListAuthKeys, ErrStructValidation, err) + } + + uri, err := url.Parse("/cloud-wrapper/v1/multi-cdn/auth-keys") + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrListAuthKeys, err) + } + q := uri.Query() + q.Add("contractId", params.ContractID) + q.Add("cdnCode", params.CDNCode) + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListAuthKeys, err) + } + + var result ListAuthKeysResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListAuthKeys, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListAuthKeys, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudwrapper) ListCDNProviders(ctx context.Context) (*ListCDNProvidersResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListCDNProviders") + + uri := "/cloud-wrapper/v1/multi-cdn/providers" + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListCDNProviders, err) + } + + var result ListCDNProvidersResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListCDNProviders, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListCDNProviders, c.Error(resp)) + } + + return &result, nil +} diff --git a/pkg/cloudwrapper/multi_cdn_test.go b/pkg/cloudwrapper/multi_cdn_test.go new file mode 100644 index 00000000..5483f905 --- /dev/null +++ b/pkg/cloudwrapper/multi_cdn_test.go @@ -0,0 +1,190 @@ +package cloudwrapper + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCloudwrapper_ListAuthKeys(t *testing.T) { + tests := map[string]struct { + params ListAuthKeysRequest + expectedPath string + responseBody string + expectedResponse *ListAuthKeysResponse + responseStatus int + withError func(*testing.T, error) + }{ + "200 OK": { + params: ListAuthKeysRequest{ + ContractID: "test_contract", + CDNCode: "dn123", + }, + expectedPath: "/cloud-wrapper/v1/multi-cdn/auth-keys?cdnCode=dn123&contractId=test_contract", + responseStatus: http.StatusOK, + responseBody: `{ + "cdnAuthKeys": [ + { + "authKeyName": "test7", + "expiryDate": "2023-08-08", + "headerName": "key" + } + ] +}`, + expectedResponse: &ListAuthKeysResponse{ + CDNAuthKeys: []MultiCDNAuthKey{ + { + AuthKeyName: "test7", + ExpiryDate: "2023-08-08", + HeaderName: "key", + }, + }, + }, + }, + "missing CDNCode": { + params: ListAuthKeysRequest{ContractID: "test_contract"}, + withError: func(t *testing.T, err error) { + assert.Equal(t, err.Error(), "list auth keys: struct validation: CDNCode: cannot be blank") + }, + }, + "missing ContractID": { + params: ListAuthKeysRequest{CDNCode: "dn123"}, + withError: func(t *testing.T, err error) { + assert.Equal(t, err.Error(), "list auth keys: struct validation: ContractID: cannot be blank") + }, + }, + "500 internal server error": { + params: ListAuthKeysRequest{ + ContractID: "test_contract", + CDNCode: "dn123", + }, + expectedPath: "/cloud-wrapper/v1/multi-cdn/auth-keys?cdnCode=dn123&contractId=test_contract", + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error processing request", + "status": 500 + }`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error processing request", + Status: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + users, err := client.ListAuthKeys(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedResponse, users) + }) + } +} + +func TestCloudwrapper_ListCDNProviders(t *testing.T) { + tests := map[string]struct { + expectedPath string + responseBody string + expectedResponse *ListCDNProvidersResponse + responseStatus int + withError func(*testing.T, error) + }{ + "200 OK": { + expectedPath: "/cloud-wrapper/v1/multi-cdn/providers", + responseStatus: http.StatusOK, + responseBody: `{ + "cdnProviders": [ + { + "cdnCode": "dn002", + "cdnName": "Level 3 (Centurylink)" + }, + { + "cdnCode": "dn003", + "cdnName": "Limelight" + }, + { + "cdnCode": "dn004", + "cdnName": "CloudFront" + } + ] +}`, + expectedResponse: &ListCDNProvidersResponse{ + CDNProviders: []CDNProvider{ + { + CDNCode: "dn002", + CDNName: "Level 3 (Centurylink)", + }, + { + CDNCode: "dn003", + CDNName: "Limelight", + }, + { + CDNCode: "dn004", + CDNName: "CloudFront", + }, + }, + }, + }, + "500 internal server error": { + expectedPath: "/cloud-wrapper/v1/multi-cdn/providers", + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error processing request", + "status": 500 + }`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error processing request", + Status: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + users, err := client.ListCDNProviders(context.Background()) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedResponse, users) + }) + } +} From e9eebd0a9570d61dc675369915fbb24fd8459c58 Mon Sep 17 00:00:00 2001 From: Mateusz Jakubiec Date: Wed, 5 Jul 2023 11:25:45 +0000 Subject: [PATCH 07/17] DXE-2714 Adjust Cloudwrapper to api changes --- pkg/cloudwrapper/capacity.go | 12 +- pkg/cloudwrapper/capacity_test.go | 4 +- pkg/cloudwrapper/cloudwrapper_test.go | 2 +- pkg/cloudwrapper/configurations.go | 123 ++++++++++----- pkg/cloudwrapper/configurations_test.go | 199 ++++++++++++++++++------ pkg/cloudwrapper/locations.go | 6 +- pkg/cloudwrapper/locations_test.go | 32 ++-- pkg/cloudwrapper/mocks.go | 5 + pkg/session/request.go | 2 +- 9 files changed, 271 insertions(+), 114 deletions(-) diff --git a/pkg/cloudwrapper/capacity.go b/pkg/cloudwrapper/capacity.go index 4b1c5f20..92dbab34 100644 --- a/pkg/cloudwrapper/capacity.go +++ b/pkg/cloudwrapper/capacity.go @@ -60,12 +60,12 @@ const ( ) const ( - // Media type - Media CapacityType = "MEDIA" - // WebStandardTLS type - WebStandardTLS CapacityType = "WEB_STANDARD_TLS" - // WebEnhancedTLS type - WebEnhancedTLS CapacityType = "WEB_ENHANCED_TLS" + // CapacityTypeMedia type + CapacityTypeMedia CapacityType = "MEDIA" + // CapacityTypeWebStandardTLS type + CapacityTypeWebStandardTLS CapacityType = "WEB_STANDARD_TLS" + // CapacityTypeWebEnhancedTLS type + CapacityTypeWebEnhancedTLS CapacityType = "WEB_ENHANCED_TLS" ) var ( diff --git a/pkg/cloudwrapper/capacity_test.go b/pkg/cloudwrapper/capacity_test.go index da8b4771..e9994e40 100644 --- a/pkg/cloudwrapper/capacity_test.go +++ b/pkg/cloudwrapper/capacity_test.go @@ -55,7 +55,7 @@ func TestListCapacity(t *testing.T) { LocationID: 1, LocationName: "US East", ContractID: "A-BCDEFG", - Type: Media, + Type: CapacityTypeMedia, ApprovedCapacity: Capacity{ Value: 2000, Unit: "GB", @@ -107,7 +107,7 @@ func TestListCapacity(t *testing.T) { LocationID: 1, LocationName: "US East", ContractID: "A-BCDEFG", - Type: WebEnhancedTLS, + Type: CapacityTypeWebEnhancedTLS, ApprovedCapacity: Capacity{ Value: 10, Unit: UnitTB, diff --git a/pkg/cloudwrapper/cloudwrapper_test.go b/pkg/cloudwrapper/cloudwrapper_test.go index 5a1992fe..8f48bdd6 100644 --- a/pkg/cloudwrapper/cloudwrapper_test.go +++ b/pkg/cloudwrapper/cloudwrapper_test.go @@ -27,7 +27,7 @@ func mockAPIClient(t *testing.T, mockServer *httptest.Server) CloudWrapper { }, } s, err := session.New(session.WithClient(httpClient), session.WithSigner(&edgegrid.Config{Host: serverURL.Host})) - assert.NoError(t, err) + require.NoError(t, err) return Client(s) } diff --git a/pkg/cloudwrapper/configurations.go b/pkg/cloudwrapper/configurations.go index deb1785e..08e54c51 100644 --- a/pkg/cloudwrapper/configurations.go +++ b/pkg/cloudwrapper/configurations.go @@ -31,6 +31,10 @@ type ( // // See: https://techdocs.akamai.com/cloud-wrapper/reference/put-configuration UpdateConfiguration(context.Context, UpdateConfigurationRequest) (*Configuration, error) + // DeleteConfiguration deletes configuration + // + // See: https://techdocs.akamai.com/cloud-wrapper/reference/delete-configuration + DeleteConfiguration(context.Context, DeleteConfigurationRequest) error // ActivateConfiguration activates a Cloud Wrapper configuration // // See: https://techdocs.akamai.com/cloud-wrapper/reference/post-configuration-activations @@ -50,15 +54,15 @@ type ( // CreateConfigurationBody holds request body parameters for CreateConfiguration CreateConfigurationBody struct { - CapacityAlertsThreshold *int `json:"capacityAlertsThreshold,omitempty"` - Comments string `json:"comments"` - ContractID string `json:"contractId"` - Locations []ConfigurationLocation `json:"locations"` - MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings,omitempty"` - ConfigName string `json:"configName"` - NotificationEmails []string `json:"notificationEmails,omitempty"` - PropertyIDs []string `json:"propertyIds"` - RetainIdleObjects bool `json:"retainIdleObjects,omitempty"` + CapacityAlertsThreshold *int `json:"capacityAlertsThreshold,omitempty"` + Comments string `json:"comments"` + ContractID string `json:"contractId"` + Locations []ConfigLocationReq `json:"locations"` + MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings,omitempty"` + ConfigName string `json:"configName"` + NotificationEmails []string `json:"notificationEmails,omitempty"` + PropertyIDs []string `json:"propertyIds"` + RetainIdleObjects bool `json:"retainIdleObjects,omitempty"` } // UpdateConfigurationRequest holds parameters for UpdateConfiguration @@ -70,13 +74,18 @@ type ( // UpdateConfigurationBody holds request body parameters for UpdateConfiguration UpdateConfigurationBody struct { - CapacityAlertsThreshold *int `json:"capacityAlertsThreshold,omitempty"` - Comments string `json:"comments"` - Locations []ConfigurationLocation `json:"locations"` - MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings,omitempty"` - NotificationEmails []string `json:"notificationEmails,omitempty"` - PropertyIDs []string `json:"propertyIds"` - RetainIdleObjects bool `json:"retainIdleObjects,omitempty"` + CapacityAlertsThreshold *int `json:"capacityAlertsThreshold,omitempty"` + Comments string `json:"comments"` + Locations []ConfigLocationReq `json:"locations"` + MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings,omitempty"` + NotificationEmails []string `json:"notificationEmails,omitempty"` + PropertyIDs []string `json:"propertyIds"` + RetainIdleObjects bool `json:"retainIdleObjects,omitempty"` + } + + // DeleteConfigurationRequest holds parameters for DeleteConfiguration + DeleteConfigurationRequest struct { + ConfigID int64 } // ActivateConfigurationRequest holds parameters for ActivateConfiguration @@ -86,21 +95,21 @@ type ( // Configuration represents CloudWrapper configuration Configuration struct { - CapacityAlertsThreshold *int `json:"capacityAlertsThreshold"` - Comments string `json:"comments"` - ContractID string `json:"contractId"` - ConfigID int64 `json:"configId"` - Locations []ConfigurationLocation `json:"locations"` - MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings"` - Status string `json:"status"` - ConfigName string `json:"configName"` - LastUpdatedBy string `json:"lastUpdatedBy"` - LastUpdatedDate string `json:"lastUpdatedDate"` - LastActivatedBy *string `json:"lastActivatedBy"` - LastActivatedDate *string `json:"lastActivatedDate"` - NotificationEmails []string `json:"notificationEmails"` - PropertyIDs []string `json:"propertyIds"` - RetainIdleObjects bool `json:"retainIdleObjects"` + CapacityAlertsThreshold *int `json:"capacityAlertsThreshold"` + Comments string `json:"comments"` + ContractID string `json:"contractId"` + ConfigID int64 `json:"configId"` + Locations []ConfigLocationResp `json:"locations"` + MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings"` + Status string `json:"status"` + ConfigName string `json:"configName"` + LastUpdatedBy string `json:"lastUpdatedBy"` + LastUpdatedDate string `json:"lastUpdatedDate"` + LastActivatedBy *string `json:"lastActivatedBy"` + LastActivatedDate *string `json:"lastActivatedDate"` + NotificationEmails []string `json:"notificationEmails"` + PropertyIDs []string `json:"propertyIds"` + RetainIdleObjects bool `json:"retainIdleObjects"` } // ListConfigurationsResponse contains response from ListConfigurations @@ -108,11 +117,19 @@ type ( Configurations []Configuration `json:"configurations"` } - // ConfigurationLocation represents location to be configured for the configuration - ConfigurationLocation struct { + // ConfigLocationReq represents location to be configured for the configuration + ConfigLocationReq struct { + Comments string `json:"comments"` + TrafficTypeID int `json:"trafficTypeId"` + Capacity Capacity `json:"capacity"` + } + + // ConfigLocationResp represents location to be configured for the configuration + ConfigLocationResp struct { Comments string `json:"comments"` TrafficTypeID int `json:"trafficTypeId"` Capacity Capacity `json:"capacity"` + MapName string `json:"mapName"` } // MultiCDNSettings represents details about Multi CDN Settings @@ -237,6 +254,13 @@ func (b UpdateConfigurationBody) Validate() error { }.Filter() } +// Validate validates DeleteConfigurationRequest +func (r DeleteConfigurationRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(r.ConfigID, validation.Required), + }) +} + // Validate validates ActivateConfigurationRequest func (r ActivateConfigurationRequest) Validate() error { return edgegriderr.ParseValidationErrors(validation.Errors{ @@ -245,7 +269,7 @@ func (r ActivateConfigurationRequest) Validate() error { } // Validate validates ConfigurationLocation -func (c ConfigurationLocation) Validate() error { +func (c ConfigLocationReq) Validate() error { return validation.Errors{ "Comments": validation.Validate(c.Comments, validation.Required), "Capacity": validation.Validate(c.Capacity, validation.Required), @@ -350,6 +374,8 @@ var ( ErrCreateConfiguration = errors.New("create configuration") // ErrUpdateConfiguration is returned when UpdateConfiguration fails ErrUpdateConfiguration = errors.New("update configuration") + // ErrDeleteConfiguration is returned when DeleteConfiguration fails + ErrDeleteConfiguration = errors.New("delete configuration") // ErrActivateConfiguration is returned when ActivateConfiguration fails ErrActivateConfiguration = errors.New("activate configuration") ) @@ -385,7 +411,7 @@ func (c *cloudwrapper) ListConfigurations(ctx context.Context) (*ListConfigurati logger := c.Log(ctx) logger.Debug("ListConfigurations") - uri := fmt.Sprintf("/cloud-wrapper/v1/configurations") + uri := "/cloud-wrapper/v1/configurations" req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { return nil, fmt.Errorf("%w: failed to create request: %s", ErrListConfigurations, err) @@ -474,6 +500,33 @@ func (c *cloudwrapper) UpdateConfiguration(ctx context.Context, params UpdateCon return &result, nil } +func (c *cloudwrapper) DeleteConfiguration(ctx context.Context, params DeleteConfigurationRequest) error { + logger := c.Log(ctx) + logger.Debug("DeleteConfiguration") + + if err := params.Validate(); err != nil { + return fmt.Errorf("%s: %w: %s", ErrDeleteConfiguration, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/cloud-wrapper/v1/configurations/%d", params.ConfigID) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("%w: failed to create request: %s", ErrDeleteConfiguration, err) + } + + resp, err := c.Exec(req, nil) + if err != nil { + return fmt.Errorf("%w: request failed: %s", ErrDeleteConfiguration, err) + } + + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("%s: %w", ErrDeleteConfiguration, c.Error(resp)) + } + + return nil +} + func (c *cloudwrapper) ActivateConfiguration(ctx context.Context, params ActivateConfigurationRequest) error { logger := c.Log(ctx) logger.Debug("ActivateConfiguration") diff --git a/pkg/cloudwrapper/configurations_test.go b/pkg/cloudwrapper/configurations_test.go index 170b0a69..df74596e 100644 --- a/pkg/cloudwrapper/configurations_test.go +++ b/pkg/cloudwrapper/configurations_test.go @@ -46,7 +46,8 @@ func TestGetConfiguration(t *testing.T) { "capacity": { "value": 1, "unit": "GB" - } + }, + "mapName": "cw-s-use" }, { "trafficTypeId": 2, @@ -54,7 +55,8 @@ func TestGetConfiguration(t *testing.T) { "capacity": { "value": 2, "unit": "TB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings": { @@ -62,7 +64,7 @@ func TestGetConfiguration(t *testing.T) { { "originId": "TestOriginID", "hostname": "TestHostname", - "propertyId": 321 + "propertyId": 321 }, { "originId": "TestOriginID2", @@ -132,7 +134,7 @@ func TestGetConfiguration(t *testing.T) { Comments: "TestComments", ContractID: "TestContractID", ConfigID: 1, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -140,6 +142,7 @@ func TestGetConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, { Comments: "TestComments", @@ -148,6 +151,7 @@ func TestGetConfiguration(t *testing.T) { Unit: "TB", Value: 2, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -244,7 +248,8 @@ func TestGetConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":null, @@ -260,7 +265,7 @@ func TestGetConfiguration(t *testing.T) { Comments: "TestComments", ContractID: "TestContractID", ConfigID: 1, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -268,6 +273,7 @@ func TestGetConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, Status: "ACTIVE", @@ -365,7 +371,8 @@ func TestListConfigurations(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":null, @@ -395,7 +402,8 @@ func TestListConfigurations(t *testing.T) { "capacity":{ "value":2, "unit":"TB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":{ @@ -451,7 +459,7 @@ func TestListConfigurations(t *testing.T) { Comments: "testComments", ContractID: "testContract", ConfigID: 1, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "usageNotes", TrafficTypeID: 1, @@ -459,6 +467,7 @@ func TestListConfigurations(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, Status: "ACTIVE", @@ -478,7 +487,7 @@ func TestListConfigurations(t *testing.T) { Comments: "mcdn", ContractID: "testContract2", ConfigID: 2, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "mcdn", TrafficTypeID: 2, @@ -486,6 +495,7 @@ func TestListConfigurations(t *testing.T) { Unit: "TB", Value: 2, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -590,7 +600,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -643,7 +653,8 @@ func TestCreateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":null, @@ -660,7 +671,7 @@ func TestCreateConfiguration(t *testing.T) { CapacityAlertsThreshold: tools.IntPtr(50), Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -668,6 +679,7 @@ func TestCreateConfiguration(t *testing.T) { Unit: UnitGB, Value: 1, }, + MapName: "cw-s-use", }, }, Status: "IN_PROGRESS", @@ -685,7 +697,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -738,7 +750,8 @@ func TestCreateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":null, @@ -755,7 +768,7 @@ func TestCreateConfiguration(t *testing.T) { CapacityAlertsThreshold: tools.IntPtr(50), Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -763,6 +776,7 @@ func TestCreateConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, Status: "IN_PROGRESS", @@ -779,7 +793,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -890,7 +904,8 @@ func TestCreateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":{ @@ -936,7 +951,7 @@ func TestCreateConfiguration(t *testing.T) { CapacityAlertsThreshold: nil, Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -944,6 +959,7 @@ func TestCreateConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -993,7 +1009,7 @@ func TestCreateConfiguration(t *testing.T) { CapacityAlertsThreshold: tools.IntPtr(70), Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -1175,7 +1191,8 @@ func TestCreateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" }, { "trafficTypeId":2, @@ -1183,7 +1200,8 @@ func TestCreateConfiguration(t *testing.T) { "capacity":{ "value":2, "unit":"TB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":{ @@ -1251,7 +1269,7 @@ func TestCreateConfiguration(t *testing.T) { CapacityAlertsThreshold: tools.IntPtr(70), Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -1259,6 +1277,7 @@ func TestCreateConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, { Comments: "TestComments2", @@ -1267,6 +1286,7 @@ func TestCreateConfiguration(t *testing.T) { Unit: "TB", Value: 2, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -1333,7 +1353,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -1481,7 +1501,7 @@ func TestCreateConfiguration(t *testing.T) { ContractID: "TestContractID", Status: "IN_PROGRESS", ConfigID: 111, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -1530,7 +1550,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -1631,7 +1651,8 @@ func TestCreateConfiguration(t *testing.T) { "capacity":{ "value":10, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":{ @@ -1675,7 +1696,7 @@ func TestCreateConfiguration(t *testing.T) { ContractID: "TestContractID", Status: "IN_PROGRESS", ConfigID: 111, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestComments", TrafficTypeID: 1, @@ -1683,6 +1704,7 @@ func TestCreateConfiguration(t *testing.T) { Unit: "GB", Value: 10, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -1730,7 +1752,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "", TrafficTypeID: 0, @@ -1750,7 +1772,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 5, @@ -1774,7 +1796,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 5, @@ -1820,7 +1842,7 @@ func TestCreateConfiguration(t *testing.T) { params: CreateConfigurationRequest{ Body: CreateConfigurationBody{Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 5, @@ -1861,7 +1883,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 5, @@ -1911,7 +1933,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 5, @@ -1956,7 +1978,7 @@ func TestCreateConfiguration(t *testing.T) { CapacityAlertsThreshold: tools.IntPtr(20), Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 5, @@ -2014,7 +2036,7 @@ func TestCreateConfiguration(t *testing.T) { Body: CreateConfigurationBody{ Comments: "TestComments", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestComments", TrafficTypeID: 1, @@ -2052,18 +2074,21 @@ func TestCreateConfiguration(t *testing.T) { } for name, test := range tests { + if name != "200 OK - full MultiCDNSettings" { + continue + } t.Run(name, func(t *testing.T) { mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, test.expectedPath, r.URL.String()) assert.Equal(t, http.MethodPost, r.Method) - w.WriteHeader(test.responseStatus) - _, err := w.Write([]byte(test.responseBody)) - assert.NoError(t, err) if test.expectedRequestBody != "" { body, err := io.ReadAll(r.Body) - assert.NoError(t, err) + require.NoError(t, err) assert.JSONEq(t, test.expectedRequestBody, string(body)) } + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) })) client := mockAPIClient(t, mockServer) result, err := client.CreateConfiguration(context.Background(), test.params) @@ -2092,7 +2117,7 @@ func TestUpdateConfiguration(t *testing.T) { ConfigID: 111, Body: UpdateConfigurationBody{ Comments: "TestCommentsUpdated", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2144,7 +2169,8 @@ func TestUpdateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":null, @@ -2162,7 +2188,7 @@ func TestUpdateConfiguration(t *testing.T) { CapacityAlertsThreshold: tools.IntPtr(50), Comments: "TestCommentsUpdated", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2170,6 +2196,7 @@ func TestUpdateConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, Status: "IN_PROGRESS", @@ -2186,7 +2213,7 @@ func TestUpdateConfiguration(t *testing.T) { ConfigID: 111, Body: UpdateConfigurationBody{ Comments: "TestCommentsUpdated", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2286,7 +2313,8 @@ func TestUpdateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":{ @@ -2327,7 +2355,7 @@ func TestUpdateConfiguration(t *testing.T) { ConfigID: 111, Comments: "TestCommentsUpdated", ContractID: "TestContractID", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2335,6 +2363,7 @@ func TestUpdateConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -2379,7 +2408,7 @@ func TestUpdateConfiguration(t *testing.T) { Body: UpdateConfigurationBody{ CapacityAlertsThreshold: tools.IntPtr(80), Comments: "TestCommentsUpdated", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2521,7 +2550,8 @@ func TestUpdateConfiguration(t *testing.T) { "capacity":{ "value":1, "unit":"GB" - } + }, + "mapName": "cw-s-use" } ], "multiCdnSettings":{ @@ -2580,7 +2610,7 @@ func TestUpdateConfiguration(t *testing.T) { Comments: "TestCommentsUpdated", ContractID: "TestContractID", ConfigID: 111, - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationResp{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2588,6 +2618,7 @@ func TestUpdateConfiguration(t *testing.T) { Unit: "GB", Value: 1, }, + MapName: "cw-s-use", }, }, MultiCDNSettings: &MultiCDNSettings{ @@ -2652,7 +2683,7 @@ func TestUpdateConfiguration(t *testing.T) { ConfigID: 1, Body: UpdateConfigurationBody{ Comments: "TestCommentsUpdated", - Locations: []ConfigurationLocation{ + Locations: []ConfigLocationReq{ { Comments: "TestCommentsUpdated", TrafficTypeID: 1, @@ -2714,6 +2745,74 @@ func TestUpdateConfiguration(t *testing.T) { } } +func TestDeleteConfiguration(t *testing.T) { + tests := map[string]struct { + params DeleteConfigurationRequest + responseStatus int + responseBody string + expectedPath string + withError func(*testing.T, error) + }{ + "202 - Accepted": { + params: DeleteConfigurationRequest{ + ConfigID: 1, + }, + responseStatus: 202, + expectedPath: "/cloud-wrapper/v1/configurations/1", + }, + "missing required params - validation error": { + params: DeleteConfigurationRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "delete configuration: struct validation: ConfigID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: DeleteConfigurationRequest{ + ConfigID: 1, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "/cloudwrapper/error-types/cloudwrapper-server-error", + "title": "An unexpected error has occurred.", + "detail": "Error processing request", + "instance": "/cloudwrapper/error-instances/abc", + "status": 500 +}`, + expectedPath: "/cloud-wrapper/v1/configurations/1", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cloudwrapper/error-types/cloudwrapper-server-error", + Title: "An unexpected error has occurred.", + Detail: "Error processing request", + Instance: "/cloudwrapper/error-instances/abc", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodDelete, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + err := client.DeleteConfiguration(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + }) + } +} + func TestActivateConfiguration(t *testing.T) { tests := map[string]struct { params ActivateConfigurationRequest diff --git a/pkg/cloudwrapper/locations.go b/pkg/cloudwrapper/locations.go index 03fb0c76..e12f8bd9 100644 --- a/pkg/cloudwrapper/locations.go +++ b/pkg/cloudwrapper/locations.go @@ -31,9 +31,9 @@ type ( // TrafficTypeItem represents a TrafficType object for the location TrafficTypeItem struct { - FailoverMapName string `json:"failoverMapName"` - TrafficTypeID int `json:"trafficTypeId"` - TrafficType string `json:"TrafficType"` + TrafficTypeID int `json:"trafficTypeId"` + TrafficType string `json:"trafficType"` + MapName string `json:"mapName"` } ) diff --git a/pkg/cloudwrapper/locations_test.go b/pkg/cloudwrapper/locations_test.go index b120f6c1..aef09bbc 100644 --- a/pkg/cloudwrapper/locations_test.go +++ b/pkg/cloudwrapper/locations_test.go @@ -30,12 +30,12 @@ func TestCloudwrapper_ListLocations(t *testing.T) { { "trafficTypeId": 1, "trafficType": "TEST_TT1", - "failoverMapName": "test_FMN" + "mapName": "cw-essl-use" }, { "trafficTypeId": 2, "trafficType": "TEST_TT2", - "failoverMapName": "test_FMN1" + "mapName": "cw-s-use-live" } ], "multiCdnLocationId": "0123" @@ -47,12 +47,12 @@ func TestCloudwrapper_ListLocations(t *testing.T) { { "trafficTypeId": 3, "trafficType": "TEST_TT1", - "failoverMapName": "test_FMN" + "mapName": "cw-essl-use" }, { "trafficTypeId": 4, "trafficType": "TEST_TT2", - "failoverMapName": "test_FMN1" + "mapName": "cw-s-use-live" } ], "multiCdnLocationId": "4567" @@ -65,14 +65,14 @@ func TestCloudwrapper_ListLocations(t *testing.T) { LocationName: "US East", TrafficTypes: []TrafficTypeItem{ { - TrafficTypeID: 1, - TrafficType: "TEST_TT1", - FailoverMapName: "test_FMN", + TrafficTypeID: 1, + TrafficType: "TEST_TT1", + MapName: "cw-essl-use", }, { - TrafficTypeID: 2, - TrafficType: "TEST_TT2", - FailoverMapName: "test_FMN1", + TrafficTypeID: 2, + TrafficType: "TEST_TT2", + MapName: "cw-s-use-live", }, }, MultiCDNLocationID: "0123", @@ -82,14 +82,14 @@ func TestCloudwrapper_ListLocations(t *testing.T) { LocationName: "US West", TrafficTypes: []TrafficTypeItem{ { - TrafficTypeID: 3, - TrafficType: "TEST_TT1", - FailoverMapName: "test_FMN", + TrafficTypeID: 3, + TrafficType: "TEST_TT1", + MapName: "cw-essl-use", }, { - TrafficTypeID: 4, - TrafficType: "TEST_TT2", - FailoverMapName: "test_FMN1", + TrafficTypeID: 4, + TrafficType: "TEST_TT2", + MapName: "cw-s-use-live", }, }, MultiCDNLocationID: "4567", diff --git a/pkg/cloudwrapper/mocks.go b/pkg/cloudwrapper/mocks.go index 1343b5cc..4f5685b4 100644 --- a/pkg/cloudwrapper/mocks.go +++ b/pkg/cloudwrapper/mocks.go @@ -106,6 +106,11 @@ func (m *Mock) UpdateConfiguration(ctx context.Context, r UpdateConfigurationReq return args.Get(0).(*Configuration), args.Error(1) } +func (m *Mock) DeleteConfiguration(ctx context.Context, r DeleteConfigurationRequest) error { + args := m.Called(ctx, r) + return args.Error(0) +} + func (m *Mock) ActivateConfiguration(ctx context.Context, r ActivateConfigurationRequest) error { args := m.Called(ctx, r) return args.Error(0) diff --git a/pkg/session/request.go b/pkg/session/request.go index bfdc5737..1ac46b63 100644 --- a/pkg/session/request.go +++ b/pkg/session/request.go @@ -91,10 +91,10 @@ func (s *session) Exec(r *http.Request, out interface{}, in ...interface{}) (*ht resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusResetContent { data, err := ioutil.ReadAll(resp.Body) - resp.Body = ioutil.NopCloser(bytes.NewBuffer(data)) if err != nil { return nil, err } + resp.Body = ioutil.NopCloser(bytes.NewBuffer(data)) if err := json.Unmarshal(data, out); err != nil { return nil, fmt.Errorf("%w: %s", ErrUnmarshaling, err) From fcaf40276de3008b06e630e503d9f1ea34dc99ca Mon Sep 17 00:00:00 2001 From: Mateusz Jakubiec Date: Fri, 14 Jul 2023 13:23:04 +0200 Subject: [PATCH 08/17] DXE-2632 Add custom cloudwrapper configuration errors --- pkg/cloudwrapper/errors.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/cloudwrapper/errors.go b/pkg/cloudwrapper/errors.go index 07e642b5..6ee32c6c 100644 --- a/pkg/cloudwrapper/errors.go +++ b/pkg/cloudwrapper/errors.go @@ -35,6 +35,18 @@ type ( } ) +const ( + configurationNotFoundType = "/cloud-wrapper/error-types/not-found" + deletionNotAllowedType = "/cloud-wrapper/error-types/forbidden" +) + +var ( + // ErrConfigurationNotFound is returned when configuration was not found + ErrConfigurationNotFound = errors.New("configuration not found") + // ErrDeletionNotAllowed is returned when user has insufficient permissions to delete configuration + ErrDeletionNotAllowed = errors.New("deletion not allowed") +) + // Error parses an error from the response func (c *cloudwrapper) Error(r *http.Response) error { var result Error @@ -66,6 +78,12 @@ func (e *Error) Error() string { // Is handles error comparisons func (e *Error) Is(target error) bool { + if errors.Is(target, ErrConfigurationNotFound) { + return e.Status == http.StatusNotFound && e.Type == configurationNotFoundType + } + if errors.Is(target, ErrDeletionNotAllowed) { + return e.Status == http.StatusForbidden && e.Type == deletionNotAllowedType + } var t *Error if !errors.As(target, &t) { From d9735c61c28dfd3bbd3792478fde2b05a038bf52 Mon Sep 17 00:00:00 2001 From: Mateusz Jakubiec Date: Fri, 28 Jul 2023 09:36:48 +0200 Subject: [PATCH 09/17] DXE-2632 add status consts --- pkg/cloudwrapper/configurations.go | 16 +++++++++++++++- pkg/cloudwrapper/configurations_test.go | 18 +++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pkg/cloudwrapper/configurations.go b/pkg/cloudwrapper/configurations.go index 08e54c51..a124d16e 100644 --- a/pkg/cloudwrapper/configurations.go +++ b/pkg/cloudwrapper/configurations.go @@ -101,7 +101,7 @@ type ( ConfigID int64 `json:"configId"` Locations []ConfigLocationResp `json:"locations"` MultiCDNSettings *MultiCDNSettings `json:"multiCdnSettings"` - Status string `json:"status"` + Status StatusType `json:"status"` ConfigName string `json:"configName"` LastUpdatedBy string `json:"lastUpdatedBy"` LastUpdatedDate string `json:"lastUpdatedDate"` @@ -189,6 +189,9 @@ type ( // RequestType is a type of request RequestType string + + // StatusType is a type of status + StatusType string ) const ( @@ -206,6 +209,17 @@ const ( RequestTypeEdgeOnly RequestType = "EDGE_ONLY" // RequestTypeEdgeAndMidgress represents RequestType value of 'EDGE_AND_MIDGRESS' RequestTypeEdgeAndMidgress RequestType = "EDGE_AND_MIDGRESS" + + // StatusActive represents Status value of 'ACTIVE' + StatusActive StatusType = "ACTIVE" + // StatusSaved represents Status value of 'SAVED' + StatusSaved StatusType = "SAVED" + // StatusInProgress represents Status value of 'IN_PROGRESS' + StatusInProgress StatusType = "IN_PROGRESS" + // StatusDeleteInProgress represents Status value of 'DELETE_IN_PROGRESS' + StatusDeleteInProgress StatusType = "DELETE_IN_PROGRESS" + // StatusFailed represents Status value of 'FAILED' + StatusFailed StatusType = "FAILED" ) // Validate validates GetConfigurationRequest diff --git a/pkg/cloudwrapper/configurations_test.go b/pkg/cloudwrapper/configurations_test.go index df74596e..93347435 100644 --- a/pkg/cloudwrapper/configurations_test.go +++ b/pkg/cloudwrapper/configurations_test.go @@ -682,7 +682,7 @@ func TestCreateConfiguration(t *testing.T) { MapName: "cw-s-use", }, }, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", @@ -779,7 +779,7 @@ func TestCreateConfiguration(t *testing.T) { MapName: "cw-s-use", }, }, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", @@ -994,7 +994,7 @@ func TestCreateConfiguration(t *testing.T) { EnableSoftAlerts: false, }, RetainIdleObjects: false, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", @@ -1339,7 +1339,7 @@ func TestCreateConfiguration(t *testing.T) { EnableSoftAlerts: true, }, RetainIdleObjects: true, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", @@ -1499,7 +1499,7 @@ func TestCreateConfiguration(t *testing.T) { expectedResponse: &Configuration{ Comments: "TestComments", ContractID: "TestContractID", - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigID: 111, Locations: []ConfigLocationResp{ { @@ -1694,7 +1694,7 @@ func TestCreateConfiguration(t *testing.T) { expectedResponse: &Configuration{ Comments: "TestComments", ContractID: "TestContractID", - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigID: 111, Locations: []ConfigLocationResp{ { @@ -2199,7 +2199,7 @@ func TestUpdateConfiguration(t *testing.T) { MapName: "cw-s-use", }, }, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", @@ -2393,7 +2393,7 @@ func TestUpdateConfiguration(t *testing.T) { }, }, }, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", @@ -2661,7 +2661,7 @@ func TestUpdateConfiguration(t *testing.T) { }, }, }, - Status: "IN_PROGRESS", + Status: StatusInProgress, ConfigName: "TestConfigName", LastUpdatedBy: "johndoe", LastUpdatedDate: "2022-06-10T13:21:14.488Z", From 0bf7f42d41c41d7081a5d12090f5bb71f5da8a2d Mon Sep 17 00:00:00 2001 From: Darek Stopka Date: Fri, 18 Aug 2023 06:13:02 +0000 Subject: [PATCH 10/17] DXE-2922 Update techdocs links --- pkg/cloudwrapper/capacity.go | 2 +- pkg/cloudwrapper/locations.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cloudwrapper/capacity.go b/pkg/cloudwrapper/capacity.go index 92dbab34..f7f97024 100644 --- a/pkg/cloudwrapper/capacity.go +++ b/pkg/cloudwrapper/capacity.go @@ -14,7 +14,7 @@ type ( // ListCapacities fetches capacities available for a given contractId. // If no contract id is provided, lists all available capacity locations // - // See: https://techdocs.akamai.com/cloud-wrapper/reference/getcapacityinventory + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-capacity-inventory ListCapacities(context.Context, ListCapacitiesRequest) (*ListCapacitiesResponse, error) } diff --git a/pkg/cloudwrapper/locations.go b/pkg/cloudwrapper/locations.go index e12f8bd9..1ed99ab2 100644 --- a/pkg/cloudwrapper/locations.go +++ b/pkg/cloudwrapper/locations.go @@ -12,7 +12,7 @@ type ( Locations interface { // ListLocations returns a list of locations available to distribute Cloud Wrapper capacity // - // See: https://techdocs.akamai.com/cloud-wrapper/reference/getlocations + // See: https://techdocs.akamai.com/cloud-wrapper/reference/get-locations ListLocations(context.Context) (*ListLocationResponse, error) } From 9c8ffa331eca96847692edddf4a97751ca6afbf4 Mon Sep 17 00:00:00 2001 From: Hamza Bentebbaa Date: Tue, 20 Jun 2023 12:24:51 +0000 Subject: [PATCH 11/17] SECKSD-20407 Add Client Lists API client --- CHANGELOG.md | 4 + pkg/clientlists/client_list.go | 71 ++++++++++ pkg/clientlists/client_list_test.go | 205 ++++++++++++++++++++++++++++ pkg/clientlists/clientlists.go | 44 ++++++ pkg/clientlists/clientlists_test.go | 87 ++++++++++++ pkg/clientlists/errors.go | 70 ++++++++++ pkg/clientlists/mocks.go | 25 ++++ 7 files changed, 506 insertions(+) create mode 100644 pkg/clientlists/client_list.go create mode 100644 pkg/clientlists/client_list_test.go create mode 100644 pkg/clientlists/clientlists.go create mode 100644 pkg/clientlists/clientlists_test.go create mode 100644 pkg/clientlists/errors.go create mode 100644 pkg/clientlists/mocks.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 820bb8aa..b7ba2160 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ * [ListProperties](https://techdocs.akamai.com/cloud-wrapper/reference/get-properties) * [ListOrigins](https://techdocs.akamai.com/cloud-wrapper/reference/get-origins) +* CLIENTLISTS + * [IMPORTANT] Added Client Lists API Support + * [GetClientLists](https://techdocs.akamai.com/client-lists/reference/get-lists) + ## 7.1.0 (July 25, 2023) ### FEATURES/ENHANCEMENTS: diff --git a/pkg/clientlists/client_list.go b/pkg/clientlists/client_list.go new file mode 100644 index 00000000..b5eb665e --- /dev/null +++ b/pkg/clientlists/client_list.go @@ -0,0 +1,71 @@ +package clientlists + +import ( + "context" + "fmt" + "net/http" +) + +type ( + // Lists interface to support creating, retrieving, updating and removing client lists. + Lists interface { + // GetClientLists lists all client lists accessible for an authenticated user + // + // See: https://techdocs.akamai.com/client-lists/reference/get-lists + GetClientLists(ctx context.Context, params GetClientListsRequest) (*GetClientListsResponse, error) + } + + // GetClientListsRequest contains request parameters for GetClientLists method + GetClientListsRequest struct { + } + + // GetClientListsResponse contains response parameters from GetClientLists method + GetClientListsResponse struct { + Content []ListContent + } + + // ListContent contains list content + ListContent struct { + Name string `json:"name"` + Type string `json:"type"` + Notes string `json:"notes"` + Tags []string `json:"tags"` + ListID string `json:"listId"` + Version int `json:"version"` + ItemsCount int `json:"itemsCount"` + CreateDate string `json:"createDate"` + CreatedBy string `json:"createdBy"` + UpdateDate string `json:"updateDate"` + UpdatedBy string `json:"updatedBy"` + ProductionActivationStatus string `json:"productionActivationStatus"` + StagingActivationStatus string `json:"stagingActivationStatus"` + ListType string `json:"listType"` + Shared bool `json:"shared"` + ReadOnly bool `json:"readOnly"` + Deprecated bool `json:"deprecated"` + } +) + +func (p *clientlists) GetClientLists(ctx context.Context, _ GetClientListsRequest) (*GetClientListsResponse, error) { + logger := p.Log(ctx) + logger.Debug("GetClientLists") + + uri := "/client-list/v1/lists" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create getClientLists request: %s", err.Error()) + } + + var rval GetClientListsResponse + + resp, err := p.Exec(req, &rval) + if err != nil { + return nil, fmt.Errorf("getClientLists request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} diff --git a/pkg/clientlists/client_list_test.go b/pkg/clientlists/client_list_test.go new file mode 100644 index 00000000..edc9ad37 --- /dev/null +++ b/pkg/clientlists/client_list_test.go @@ -0,0 +1,205 @@ +package clientlists + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/require" + "github.com/tj/assert" +) + +func TestClientList_GetClientLists(t *testing.T) { + uri := "/client-list/v1/lists" + + tests := map[string]struct { + params GetClientListsRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetClientListsResponse + withError error + headers http.Header + }{ + "200 OK": { + params: GetClientListsRequest{}, + headers: http.Header{ + "Content-Type": []string{"application/json"}, + }, + responseStatus: http.StatusOK, + responseBody: ` + { + "content": [ + { + "createDate": "2023-06-06T15:58:39.225+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "itemsCount": 1, + "listId": "91596_AUDITLOGSTESTLIST", + "listType": "CL", + "name": "AUDIT LOGS - TEST LIST", + "productionActivationStatus": "INACTIVE", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "tags": ["green"], + "type": "IP", + "updateDate": "2023-06-06T15:58:39.225+00:00", + "updatedBy": "ccare2", + "version": 1 + }, + { + "createDate": "2022-11-10T14:42:04.857+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "itemsCount": 2, + "listId": "85988_ANTHONYGEOLISTOPEN", + "listType": "CL", + "name": "AnthonyGeoListOPEN", + "notes": "This is another Geo client list for Nov 11", + "productionActivationStatus": "INACTIVE", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "tags": [], + "type": "GEO", + "updateDate": "2023-05-11T15:30:10.224+00:00", + "updatedBy": "ccare2", + "version": 66 + }, + { + "createDate": "2022-10-17T13:39:25.319+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "itemsCount": 0, + "listId": "85552_ANTHONYFILEHASHLIST", + "listType": "CL", + "name": "File Hash List", + "notes": "This is another File hash client list for Oct 17", + "productionActivationStatus": "PENDING_ACTIVATION", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "tags": ["blue"], + "type": "TLS_FINGERPRINT", + "updateDate": "2023-06-05T06:56:19.004+00:00", + "updatedBy": "ccare2", + "version": 343 + } + ] + } + `, + expectedPath: uri, + expectedResponse: &GetClientListsResponse{ + Content: []ListContent{ + { + CreateDate: "2023-06-06T15:58:39.225+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 1, + ListID: "91596_AUDITLOGSTESTLIST", + ListType: "CL", + Name: "AUDIT LOGS - TEST LIST", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"green"}, + Type: "IP", + UpdateDate: "2023-06-06T15:58:39.225+00:00", + UpdatedBy: "ccare2", + Version: 1, + }, + { + CreateDate: "2022-11-10T14:42:04.857+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 2, + ListID: "85988_ANTHONYGEOLISTOPEN", + ListType: "CL", + Name: "AnthonyGeoListOPEN", + Notes: "This is another Geo client list for Nov 11", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{}, + Type: "GEO", + UpdateDate: "2023-05-11T15:30:10.224+00:00", + UpdatedBy: "ccare2", + Version: 66, + }, + { + CreateDate: "2022-10-17T13:39:25.319+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 0, + ListID: "85552_ANTHONYFILEHASHLIST", + ListType: "CL", + Name: "File Hash List", + Notes: "This is another File hash client list for Oct 17", + ProductionActivationStatus: "PENDING_ACTIVATION", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"blue"}, + Type: "TLS_FINGERPRINT", + UpdateDate: "2023-06-05T06:56:19.004+00:00", + UpdatedBy: "ccare2", + Version: 343, + }, + }, + }, + }, + "500 internal server error": { + params: GetClientListsRequest{}, + headers: http.Header{}, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists", + StatusCode: http.StatusInternalServerError, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetClientLists( + session.ContextWithOptions( + context.Background(), + session.WithContextHeaders(test.headers), + ), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/clientlists/clientlists.go b/pkg/clientlists/clientlists.go new file mode 100644 index 00000000..5af01ff2 --- /dev/null +++ b/pkg/clientlists/clientlists.go @@ -0,0 +1,44 @@ +// Package clientlists provides access to Akamai Client Lists APIs +// +// See: https://techdocs.akamai.com/client-lists/reference/api +package clientlists + +import ( + "errors" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" +) + +var ( + // ErrStructValidation is returned when given struct validation failed + ErrStructValidation = errors.New("struct validation") +) + +type ( + // ClientLists is the clientlists api interface + ClientLists interface { + Lists + } + + clientlists struct { + session.Session + } + + // Option defines a clientlists option + Option func(*clientlists) + + // ClientFunc is a clientlists client new method, this can be used for mocking + ClientFunc func(sess session.Session, opts ...Option) ClientLists +) + +// Client returns a new clientlists Client instance with the specified controller +func Client(sess session.Session, opts ...Option) ClientLists { + p := &clientlists{ + Session: sess, + } + + for _, opt := range opts { + opt(p) + } + return p +} diff --git a/pkg/clientlists/clientlists_test.go b/pkg/clientlists/clientlists_test.go new file mode 100644 index 00000000..e6576215 --- /dev/null +++ b/pkg/clientlists/clientlists_test.go @@ -0,0 +1,87 @@ +package clientlists + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegrid" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/require" + "github.com/tj/assert" +) + +// compactJSON converts a JSON-encoded byte slice to a compact form (so our JSON fixtures can be readable) +func compactJSON(encoded []byte) string { + buf := bytes.Buffer{} + if err := json.Compact(&buf, encoded); err != nil { + panic(fmt.Sprintf("%s: %s", err, string(encoded))) + } + + return buf.String() +} + +// loadFixtureBytes returns the entire contents of the given file as a byte slice +func loadFixtureBytes(path string) []byte { + contents, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + return contents +} + +func mockAPIClient(t *testing.T, mockServer *httptest.Server) ClientLists { + serverURL, err := url.Parse(mockServer.URL) + require.NoError(t, err) + certPool := x509.NewCertPool() + certPool.AddCert(mockServer.Certificate()) + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, + }, + } + s, err := session.New(session.WithClient(httpClient), session.WithSigner(&edgegrid.Config{Host: serverURL.Host})) + assert.NoError(t, err) + return Client(s) +} + +func opt() Option { + return func(*clientlists) {} +} + +func TestClient(t *testing.T) { + sess, err := session.New() + require.NoError(t, err) + tests := map[string]struct { + options []Option + expected *clientlists + }{ + "no options provided, return default": { + options: nil, + expected: &clientlists{ + Session: sess, + }, + }, + "dummy option": { + options: []Option{opt()}, + expected: &clientlists{ + Session: sess, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := Client(sess, test.options...) + assert.Equal(t, res, test.expected) + }) + } +} diff --git a/pkg/clientlists/errors.go b/pkg/clientlists/errors.go new file mode 100644 index 00000000..2a355d1f --- /dev/null +++ b/pkg/clientlists/errors.go @@ -0,0 +1,70 @@ +package clientlists + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +type ( + // Error is a Client Lists error interface + Error struct { + Type string `json:"type"` + Title string `json:"title"` + Detail string `json:"detail"` + Instance string `json:"instance,omitempty"` + BehaviorName string `json:"behaviorName,omitempty"` + ErrorLocation string `json:"errorLocation,omitempty"` + StatusCode int `json:"-"` + } +) + +// Error parses an error from the response +func (p *clientlists) Error(r *http.Response) error { + var e Error + + var body []byte + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + p.Log(r.Request.Context()).Errorf("reading error response body: %s", err) + e.StatusCode = r.StatusCode + e.Title = "Failed to read error body" + e.Detail = err.Error() + return &e + } + + if err := json.Unmarshal(body, &e); err != nil { + p.Log(r.Request.Context()).Errorf("could not unmarshal API error: %s", err) + e.Title = "Failed to unmarshal error body" + e.Detail = err.Error() + } + + e.StatusCode = r.StatusCode + + return &e +} + +func (e *Error) Error() string { + return fmt.Sprintf("Title: %s; Type: %s; Detail: %s", e.Title, e.Type, e.Detail) +} + +// Is handles error comparisons +func (e *Error) Is(target error) bool { + var t *Error + if !errors.As(target, &t) { + return false + } + + if e == t { + return true + } + + if e.StatusCode != t.StatusCode { + return false + } + + return e.Error() == t.Error() +} diff --git a/pkg/clientlists/mocks.go b/pkg/clientlists/mocks.go new file mode 100644 index 00000000..22123f9a --- /dev/null +++ b/pkg/clientlists/mocks.go @@ -0,0 +1,25 @@ +package clientlists + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +// Mock is ClientList API Mock +type Mock struct { + mock.Mock +} + +var _ ClientLists = &Mock{} + +// GetClientLists return list of client lists +func (p *Mock) GetClientLists(ctx context.Context, params GetClientListsRequest) (*GetClientListsResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetClientListsResponse), args.Error(1) +} From 1bb9aec488d09b59c9e480193a3295434c7a3934 Mon Sep 17 00:00:00 2001 From: Hamza Bentebbaa Date: Fri, 23 Jun 2023 10:51:31 +0000 Subject: [PATCH 12/17] SECKSD-20453 add client lists filter support Merge in DEVEXP/akamaiopen-edgegrid-golang from feature/SECKSD-20453-add-client-lists-filter-support to feature/sp-clientlists-july-2023 --- CHANGELOG.md | 1 + pkg/clientlists/client_list.go | 99 +++++++++++++++++++++++------ pkg/clientlists/client_list_test.go | 59 +++++++++++++++++ 3 files changed, 139 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ba2160..00efc560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * CLIENTLISTS * [IMPORTANT] Added Client Lists API Support * [GetClientLists](https://techdocs.akamai.com/client-lists/reference/get-lists) + * Support filter by name or type ## 7.1.0 (July 25, 2023) diff --git a/pkg/clientlists/client_list.go b/pkg/clientlists/client_list.go index b5eb665e..1f94902e 100644 --- a/pkg/clientlists/client_list.go +++ b/pkg/clientlists/client_list.go @@ -4,6 +4,10 @@ import ( "context" "fmt" "net/http" + "net/url" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" ) type ( @@ -15,8 +19,13 @@ type ( GetClientLists(ctx context.Context, params GetClientListsRequest) (*GetClientListsResponse, error) } + // ClientListType represents client list type + ClientListType string + // GetClientListsRequest contains request parameters for GetClientLists method GetClientListsRequest struct { + Type []ClientListType + Name string } // GetClientListsResponse contains response parameters from GetClientLists method @@ -26,32 +35,51 @@ type ( // ListContent contains list content ListContent struct { - Name string `json:"name"` - Type string `json:"type"` - Notes string `json:"notes"` - Tags []string `json:"tags"` - ListID string `json:"listId"` - Version int `json:"version"` - ItemsCount int `json:"itemsCount"` - CreateDate string `json:"createDate"` - CreatedBy string `json:"createdBy"` - UpdateDate string `json:"updateDate"` - UpdatedBy string `json:"updatedBy"` - ProductionActivationStatus string `json:"productionActivationStatus"` - StagingActivationStatus string `json:"stagingActivationStatus"` - ListType string `json:"listType"` - Shared bool `json:"shared"` - ReadOnly bool `json:"readOnly"` - Deprecated bool `json:"deprecated"` + Name string `json:"name"` + Type ClientListType `json:"type"` + Notes string `json:"notes"` + Tags []string `json:"tags"` + ListID string `json:"listId"` + Version int `json:"version"` + ItemsCount int `json:"itemsCount"` + CreateDate string `json:"createDate"` + CreatedBy string `json:"createdBy"` + UpdateDate string `json:"updateDate"` + UpdatedBy string `json:"updatedBy"` + ProductionActivationStatus string `json:"productionActivationStatus"` + StagingActivationStatus string `json:"stagingActivationStatus"` + ListType string `json:"listType"` + Shared bool `json:"shared"` + ReadOnly bool `json:"readOnly"` + Deprecated bool `json:"deprecated"` } ) -func (p *clientlists) GetClientLists(ctx context.Context, _ GetClientListsRequest) (*GetClientListsResponse, error) { +func (p *clientlists) GetClientLists(ctx context.Context, params GetClientListsRequest) (*GetClientListsResponse, error) { logger := p.Log(ctx) logger.Debug("GetClientLists") - uri := "/client-list/v1/lists" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri, err := url.Parse("/client-list/v1/lists") + if err != nil { + return nil, fmt.Errorf("Error parsing URL: %s", err.Error()) + } + + q := uri.Query() + if params.Name != "" { + q.Add("name", params.Name) + } + if params.Type != nil { + for _, v := range params.Type { + q.Add("type", string(v)) + } + } + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create getClientLists request: %s", err.Error()) } @@ -69,3 +97,34 @@ func (p *clientlists) GetClientLists(ctx context.Context, _ GetClientListsReques return &rval, nil } + +func (v GetClientListsRequest) validate() error { + listTypes := getValidListTypesAsInterface() + return edgegriderr.ParseValidationErrors(validation.Errors{ + "Type": validation.Validate(v.Type, validation.Each(validation.In(listTypes...).Error( + fmt.Sprintf("Invalid 'type' value(s) provided. Valid values are: %s", listTypes)))), + }) +} + +const ( + // IP for ip type list type + IP ClientListType = "IP" + // GEO for geo/countries list type + GEO ClientListType = "GEO" + // ASN for AS Number list type + ASN ClientListType = "ASN" + // TLSFingerprint for TLS Fingerprint list type + TLSFingerprint ClientListType = "TLS_FINGERPRINT" + // FileHash for file hash type list + FileHash ClientListType = "FILE_HASH" +) + +func getValidListTypesAsInterface() []interface{} { + return []interface{}{ + IP, + GEO, + ASN, + TLSFingerprint, + FileHash, + } +} diff --git a/pkg/clientlists/client_list_test.go b/pkg/clientlists/client_list_test.go index edc9ad37..91abafd7 100644 --- a/pkg/clientlists/client_list_test.go +++ b/pkg/clientlists/client_list_test.go @@ -3,6 +3,7 @@ package clientlists import ( "context" "errors" + "fmt" "net/http" "net/http/httptest" "testing" @@ -157,6 +158,64 @@ func TestClientList_GetClientLists(t *testing.T) { }, }, }, + "200 OK - Lists filtered by name and type": { + params: GetClientListsRequest{ + Name: "list name", + Type: []ClientListType{IP, GEO}, + }, + headers: http.Header{ + "Content-Type": []string{"application/json"}, + }, + responseStatus: http.StatusOK, + responseBody: ` + { + "content": [ + { + "createDate": "2023-06-06T15:58:39.225+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "itemsCount": 1, + "listId": "91596_AUDITLOGSTESTLIST", + "listType": "CL", + "name": "AUDIT LOGS - TEST LIST", + "productionActivationStatus": "INACTIVE", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "tags": ["green"], + "type": "IP", + "updateDate": "2023-06-06T15:58:39.225+00:00", + "updatedBy": "ccare2", + "version": 1 + } + ] + } + `, + expectedPath: fmt.Sprintf(uri+"?name=%s&type=%s&type=%s", "list+name", "IP", "GEO"), + expectedResponse: &GetClientListsResponse{ + Content: []ListContent{ + { + CreateDate: "2023-06-06T15:58:39.225+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 1, + ListID: "91596_AUDITLOGSTESTLIST", + ListType: "CL", + Name: "AUDIT LOGS - TEST LIST", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"green"}, + Type: "IP", + UpdateDate: "2023-06-06T15:58:39.225+00:00", + UpdatedBy: "ccare2", + Version: 1, + }, + }, + }, + }, "500 internal server error": { params: GetClientListsRequest{}, headers: http.Header{}, From 7fd846f9681382989f63259d7056b83861272dd5 Mon Sep 17 00:00:00 2001 From: Hamza Bentebbaa Date: Fri, 14 Jul 2023 13:37:33 +0000 Subject: [PATCH 13/17] SECKSD-20408 Add Client Lists management apis Merge in DEVEXP/akamaiopen-edgegrid-golang from feature/SECKSD-20408-add-clientlists-management-apis to feature/sp-clientlists-july-2023 --- CHANGELOG.md | 5 + pkg/clientlists/client_list.go | 348 ++++++++++- pkg/clientlists/client_list_test.go | 909 +++++++++++++++++++++++++--- pkg/clientlists/mocks.go | 47 ++ 4 files changed, 1221 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00efc560..e9961ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ * [IMPORTANT] Added Client Lists API Support * [GetClientLists](https://techdocs.akamai.com/client-lists/reference/get-lists) * Support filter by name or type + * [GetClientList](https://techdocs.akamai.com/client-lists/reference/get-list) + * [UpdateClientList](https://techdocs.akamai.com/client-lists/reference/put-update-list) + * [UpdateClientListItems](https://techdocs.akamai.com/client-lists/reference/post-update-items) + * [CreateClientList](https://techdocs.akamai.com/client-lists/reference/post-create-list) + * [DeleteClientList](https://techdocs.akamai.com/client-lists/reference/delete-list) ## 7.1.0 (July 25, 2023) diff --git a/pkg/clientlists/client_list.go b/pkg/clientlists/client_list.go index 1f94902e..bd0cae99 100644 --- a/pkg/clientlists/client_list.go +++ b/pkg/clientlists/client_list.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -17,6 +18,31 @@ type ( // // See: https://techdocs.akamai.com/client-lists/reference/get-lists GetClientLists(ctx context.Context, params GetClientListsRequest) (*GetClientListsResponse, error) + + // GetClientList retrieves client list with specific list id + // + // See: https://techdocs.akamai.com/client-lists/reference/get-list + GetClientList(ctx context.Context, params GetClientListRequest) (*GetClientListResponse, error) + + // CreateClientList creates a new client list + // + // See: https://techdocs.akamai.com/client-lists/reference/post-create-list + CreateClientList(ctx context.Context, params CreateClientListRequest) (*CreateClientListResponse, error) + + // UpdateClientList updates existing client list + // + // See: https://techdocs.akamai.com/client-lists/reference/put-update-list + UpdateClientList(ctx context.Context, params UpdateClientListRequest) (*UpdateClientListResponse, error) + + // UpdateClientListItems updates items/entries of an existing client lists + // + // See: https://techdocs.akamai.com/client-lists/reference/post-update-items + UpdateClientListItems(ctx context.Context, params UpdateClientListItemsRequest) (*UpdateClientListItemsResponse, error) + + // DeleteClientList removes a client list + // + // See: https://techdocs.akamai.com/client-lists/reference/delete-list + DeleteClientList(ctx context.Context, params DeleteClientListRequest) error } // ClientListType represents client list type @@ -24,13 +50,26 @@ type ( // GetClientListsRequest contains request parameters for GetClientLists method GetClientListsRequest struct { - Type []ClientListType - Name string + Type []ClientListType + Name string + Search string + IncludeItems bool + IncludeDeprecated bool + IncludeNetworkList bool + Page *int + PageSize *int + Sort []string } // GetClientListsResponse contains response parameters from GetClientLists method GetClientListsResponse struct { - Content []ListContent + Content []ClientList + } + + // ClientList contains list content and items + ClientList struct { + ListContent + Items []ListItemContent } // ListContent contains list content @@ -40,19 +79,118 @@ type ( Notes string `json:"notes"` Tags []string `json:"tags"` ListID string `json:"listId"` - Version int `json:"version"` - ItemsCount int `json:"itemsCount"` + Version int64 `json:"version"` + ItemsCount int64 `json:"itemsCount"` CreateDate string `json:"createDate"` CreatedBy string `json:"createdBy"` UpdateDate string `json:"updateDate"` UpdatedBy string `json:"updatedBy"` ProductionActivationStatus string `json:"productionActivationStatus"` StagingActivationStatus string `json:"stagingActivationStatus"` + ProductionActiveVersion int64 `json:"productionActiveVersion"` + StagingActiveVersion int64 `json:"stagingActiveVersion"` ListType string `json:"listType"` Shared bool `json:"shared"` ReadOnly bool `json:"readOnly"` Deprecated bool `json:"deprecated"` } + + // ListItemContent contains client list item information + ListItemContent struct { + Value string `json:"value"` + Tags []string `json:"tags"` + Description string `json:"description"` + ExpirationDate string `json:"expirationDate"` + CreateDate string `json:"createDate"` + CreatedBy string `json:"createdBy"` + CreatedVersion int64 `json:"createdVersion"` + ProductionStatus string `json:"productionStatus"` + StagingStatus string `json:"stagingStatus"` + Type ClientListType `json:"type"` + UpdateDate string `json:"updateDate"` + UpdatedBy string `json:"updatedBy"` + } + + // ListItemPayload contains item's editable fields to use as update/create/delete payload + ListItemPayload struct { + Value string `json:"value"` + Tags []string `json:"tags"` + Description string `json:"description"` + ExpirationDate string `json:"expirationDate"` + } + + // GetClientListRequest contains request params for GetClientList method + GetClientListRequest struct { + ListID string + IncludeItems bool + } + + // GetClientListResponse contains response from GetClientList method + GetClientListResponse struct { + ListContent + ContractID string `json:"contractId"` + GroupName string `json:"groupName"` + Items []ListItemContent `json:"items"` + } + + // CreateClientListRequest contains request params for CreateClientList method + CreateClientListRequest struct { + ContractID string `json:"contractId"` + GroupID int64 `json:"groupId"` + Name string `json:"name"` + Type ClientListType `json:"type"` + Notes string `json:"notes"` + Tags []string `json:"tags"` + Items []ListItemPayload `json:"items"` + } + + // CreateClientListResponse contains response from CreateClientList method + CreateClientListResponse GetClientListResponse + + // UpdateClientListRequest contains request params for UpdateClientList method + UpdateClientListRequest struct { + UpdateClientList + ListID string + } + + // UpdateClientList contains the body of client list update request + UpdateClientList struct { + Name string `json:"name"` + Notes string `json:"notes"` + Tags []string `json:"tags"` + } + + // UpdateClientListResponse contains response from UpdateClientList method + UpdateClientListResponse struct { + ListContent + ContractID string `json:"contractId"` + GroupName string `json:"groupName"` + } + + // UpdateClientListItemsRequest contains request params for UpdateClientListItems method + UpdateClientListItemsRequest struct { + UpdateClientListItems + ListID string + } + + // UpdateClientListItems contains the body of client list items update request + UpdateClientListItems struct { + Append []ListItemPayload `json:"append"` + Update []ListItemPayload `json:"update"` + Delete []ListItemPayload `json:"delete"` + } + + // UpdateClientListItemsResponse contains response from UpdateClientListItems method + UpdateClientListItemsResponse struct { + Appended []ListItemContent `json:"appended"` + Updated []ListItemContent `json:"updated"` + Deleted []ListItemContent `json:"deleted"` + } + + // DeleteClientListRequest contains request params for DeleteClientList method + DeleteClientListRequest struct { + ListID string + } ) func (p *clientlists) GetClientLists(ctx context.Context, params GetClientListsRequest) (*GetClientListsResponse, error) { @@ -77,6 +215,29 @@ func (p *clientlists) GetClientLists(ctx context.Context, params GetClientListsR q.Add("type", string(v)) } } + if params.Search != "" { + q.Add("search", params.Search) + } + if params.IncludeItems { + q.Add("includeItems", strconv.FormatBool(params.IncludeItems)) + } + if params.IncludeDeprecated { + q.Add("includeDeprecated", strconv.FormatBool(params.IncludeDeprecated)) + } + if params.IncludeNetworkList { + q.Add("includeNetworkList", strconv.FormatBool(params.IncludeNetworkList)) + } + if params.Page != nil { + q.Add("page", fmt.Sprintf("%d", *params.Page)) + } + if params.PageSize != nil { + q.Add("pageSize", fmt.Sprintf("%d", *params.PageSize)) + } + if params.Sort != nil { + for _, v := range params.Sort { + q.Add("sort", string(v)) + } + } uri.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) @@ -98,6 +259,183 @@ func (p *clientlists) GetClientLists(ctx context.Context, params GetClientListsR return &rval, nil } +func (p *clientlists) GetClientList(ctx context.Context, params GetClientListRequest) (*GetClientListResponse, error) { + logger := p.Log(ctx) + logger.Debug("GetClientList") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri, err := url.Parse(fmt.Sprintf("/client-list/v1/lists/%s", params.ListID)) + if err != nil { + return nil, fmt.Errorf("failed to parse url: %w", err) + } + + q := uri.Query() + if params.IncludeItems { + q.Add("includeItems", strconv.FormatBool(params.IncludeItems)) + } + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create getClientList request: %s", err.Error()) + } + + var rval GetClientListResponse + resp, err := p.Exec(req, &rval) + if err != nil { + return nil, fmt.Errorf("getClientList request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} + +func (p *clientlists) UpdateClientList(ctx context.Context, params UpdateClientListRequest) (*UpdateClientListResponse, error) { + logger := p.Log(ctx) + logger.Debug("UpdateClientList") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/client-list/v1/lists/%s", params.ListID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create updateClientList request: %s", err.Error()) + } + + var rval UpdateClientListResponse + resp, err := p.Exec(req, &rval, ¶ms.UpdateClientList) + if err != nil { + return nil, fmt.Errorf("updateClientList request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} + +func (p *clientlists) UpdateClientListItems(ctx context.Context, params UpdateClientListItemsRequest) (*UpdateClientListItemsResponse, error) { + logger := p.Log(ctx) + logger.Debug("UpdateClientListItems") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/client-list/v1/lists/%s/items", params.ListID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateClientListItems request: %s", err.Error()) + } + + var rval UpdateClientListItemsResponse + resp, err := p.Exec(req, &rval, ¶ms.UpdateClientListItems) + if err != nil { + return nil, fmt.Errorf("UpdateClientListItems request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} + +func (p *clientlists) CreateClientList(ctx context.Context, params CreateClientListRequest) (*CreateClientListResponse, error) { + logger := p.Log(ctx) + logger.Debug("CreateClientList") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/client-list/v1/lists", nil) + if err != nil { + return nil, fmt.Errorf("failed to create createClientList request: %s", err.Error()) + } + + var rval CreateClientListResponse + resp, err := p.Exec(req, &rval, ¶ms) + if err != nil { + return nil, fmt.Errorf("createClientList request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusCreated { + return nil, p.Error(resp) + } + + return &rval, nil +} + +func (p *clientlists) DeleteClientList(ctx context.Context, params DeleteClientListRequest) error { + logger := p.Log(ctx) + logger.Debug("DeleteClientList") + + if err := params.validate(); err != nil { + return fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("%s/%s", "/client-list/v1/lists", params.ListID) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("failed to create deleteClientList request: %s", err.Error()) + } + + resp, err := p.Exec(req, nil) + if err != nil { + return fmt.Errorf("deleteClientList request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusNoContent { + return p.Error(resp) + } + + return nil +} + +func (v GetClientListRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ListID": validation.Validate(v.ListID, validation.Required), + }) +} + +func (v UpdateClientListRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ListID": validation.Validate(v.ListID, validation.Required), + "Name": validation.Validate(v.ListID, validation.Required), + }) +} + +func (v UpdateClientListItemsRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ListID": validation.Validate(v.ListID, validation.Required), + }) +} + +func (v CreateClientListRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "Name": validation.Validate(v.Name, validation.Required), + "Type": validation.Validate(v.Type, validation.Required), + }) +} + +func (v DeleteClientListRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ListID": validation.Validate(v.ListID, validation.Required), + }) +} + func (v GetClientListsRequest) validate() error { listTypes := getValidListTypesAsInterface() return edgegriderr.ParseValidationErrors(validation.Errors{ diff --git a/pkg/clientlists/client_list_test.go b/pkg/clientlists/client_list_test.go index 91abafd7..f9b0aebf 100644 --- a/pkg/clientlists/client_list_test.go +++ b/pkg/clientlists/client_list_test.go @@ -4,16 +4,18 @@ import ( "context" "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/tools" "github.com/stretchr/testify/require" "github.com/tj/assert" ) -func TestClientList_GetClientLists(t *testing.T) { +func TestGetClientLists(t *testing.T) { uri := "/client-list/v1/lists" tests := map[string]struct { @@ -23,13 +25,9 @@ func TestClientList_GetClientLists(t *testing.T) { expectedPath string expectedResponse *GetClientListsResponse withError error - headers http.Header }{ "200 OK": { - params: GetClientListsRequest{}, - headers: http.Header{ - "Content-Type": []string{"application/json"}, - }, + params: GetClientListsRequest{}, responseStatus: http.StatusOK, responseBody: ` { @@ -98,62 +96,68 @@ func TestClientList_GetClientLists(t *testing.T) { `, expectedPath: uri, expectedResponse: &GetClientListsResponse{ - Content: []ListContent{ + Content: []ClientList{ { - CreateDate: "2023-06-06T15:58:39.225+00:00", - CreatedBy: "ccare2", - Deprecated: false, - ItemsCount: 1, - ListID: "91596_AUDITLOGSTESTLIST", - ListType: "CL", - Name: "AUDIT LOGS - TEST LIST", - ProductionActivationStatus: "INACTIVE", - ReadOnly: false, - Shared: false, - StagingActivationStatus: "INACTIVE", - Tags: []string{"green"}, - Type: "IP", - UpdateDate: "2023-06-06T15:58:39.225+00:00", - UpdatedBy: "ccare2", - Version: 1, + ListContent: ListContent{ + CreateDate: "2023-06-06T15:58:39.225+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 1, + ListID: "91596_AUDITLOGSTESTLIST", + ListType: "CL", + Name: "AUDIT LOGS - TEST LIST", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"green"}, + Type: "IP", + UpdateDate: "2023-06-06T15:58:39.225+00:00", + UpdatedBy: "ccare2", + Version: 1, + }, }, { - CreateDate: "2022-11-10T14:42:04.857+00:00", - CreatedBy: "ccare2", - Deprecated: false, - ItemsCount: 2, - ListID: "85988_ANTHONYGEOLISTOPEN", - ListType: "CL", - Name: "AnthonyGeoListOPEN", - Notes: "This is another Geo client list for Nov 11", - ProductionActivationStatus: "INACTIVE", - ReadOnly: false, - Shared: false, - StagingActivationStatus: "INACTIVE", - Tags: []string{}, - Type: "GEO", - UpdateDate: "2023-05-11T15:30:10.224+00:00", - UpdatedBy: "ccare2", - Version: 66, + ListContent: ListContent{ + CreateDate: "2022-11-10T14:42:04.857+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 2, + ListID: "85988_ANTHONYGEOLISTOPEN", + ListType: "CL", + Name: "AnthonyGeoListOPEN", + Notes: "This is another Geo client list for Nov 11", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{}, + Type: "GEO", + UpdateDate: "2023-05-11T15:30:10.224+00:00", + UpdatedBy: "ccare2", + Version: 66, + }, }, { - CreateDate: "2022-10-17T13:39:25.319+00:00", - CreatedBy: "ccare2", - Deprecated: false, - ItemsCount: 0, - ListID: "85552_ANTHONYFILEHASHLIST", - ListType: "CL", - Name: "File Hash List", - Notes: "This is another File hash client list for Oct 17", - ProductionActivationStatus: "PENDING_ACTIVATION", - ReadOnly: false, - Shared: false, - StagingActivationStatus: "INACTIVE", - Tags: []string{"blue"}, - Type: "TLS_FINGERPRINT", - UpdateDate: "2023-06-05T06:56:19.004+00:00", - UpdatedBy: "ccare2", - Version: 343, + ListContent: ListContent{ + CreateDate: "2022-10-17T13:39:25.319+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 0, + ListID: "85552_ANTHONYFILEHASHLIST", + ListType: "CL", + Name: "File Hash List", + Notes: "This is another File hash client list for Oct 17", + ProductionActivationStatus: "PENDING_ACTIVATION", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"blue"}, + Type: "TLS_FINGERPRINT", + UpdateDate: "2023-06-05T06:56:19.004+00:00", + UpdatedBy: "ccare2", + Version: 343, + }, }, }, }, @@ -163,9 +167,6 @@ func TestClientList_GetClientLists(t *testing.T) { Name: "list name", Type: []ClientListType{IP, GEO}, }, - headers: http.Header{ - "Content-Type": []string{"application/json"}, - }, responseStatus: http.StatusOK, responseBody: ` { @@ -194,38 +195,105 @@ func TestClientList_GetClientLists(t *testing.T) { `, expectedPath: fmt.Sprintf(uri+"?name=%s&type=%s&type=%s", "list+name", "IP", "GEO"), expectedResponse: &GetClientListsResponse{ - Content: []ListContent{ + Content: []ClientList{ { - CreateDate: "2023-06-06T15:58:39.225+00:00", - CreatedBy: "ccare2", - Deprecated: false, - ItemsCount: 1, - ListID: "91596_AUDITLOGSTESTLIST", - ListType: "CL", - Name: "AUDIT LOGS - TEST LIST", - ProductionActivationStatus: "INACTIVE", - ReadOnly: false, - Shared: false, - StagingActivationStatus: "INACTIVE", - Tags: []string{"green"}, - Type: "IP", - UpdateDate: "2023-06-06T15:58:39.225+00:00", - UpdatedBy: "ccare2", - Version: 1, + ListContent: ListContent{ + CreateDate: "2023-06-06T15:58:39.225+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 1, + ListID: "91596_AUDITLOGSTESTLIST", + ListType: "CL", + Name: "AUDIT LOGS - TEST LIST", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"green"}, + Type: "IP", + UpdateDate: "2023-06-06T15:58:39.225+00:00", + UpdatedBy: "ccare2", + Version: 1, + }, + }, + }, + }, + }, + "200 OK - Lists filtered by search and query params: includeItems, includeDeprecated, includeNetworkList, page, pageSize, sort": { + params: GetClientListsRequest{ + Search: "search term", + IncludeItems: true, + IncludeDeprecated: true, + IncludeNetworkList: true, + Page: tools.IntPtr(0), + PageSize: tools.IntPtr(2), + Sort: []string{"updatedBy:desc", "value:desc"}, + }, + responseStatus: http.StatusOK, + responseBody: ` + { + "content": [ + { + "createDate": "2023-06-06T15:58:39.225+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "itemsCount": 1, + "listId": "91596_AUDITLOGSTESTLIST", + "listType": "CL", + "name": "AUDIT LOGS - TEST LIST", + "productionActivationStatus": "INACTIVE", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "tags": ["green"], + "type": "IP", + "updateDate": "2023-06-06T15:58:39.225+00:00", + "updatedBy": "ccare2", + "version": 1, + "items": [] + } + ] + }`, + expectedPath: fmt.Sprintf( + uri+"?includeDeprecated=%s&includeItems=%s&includeNetworkList=%s&page=%d&pageSize=%d&search=%s&sort=%s&sort=%s", + "true", "true", "true", 0, 2, "search+term", "updatedBy%3Adesc", "value%3Adesc", + ), + expectedResponse: &GetClientListsResponse{ + Content: []ClientList{ + { + ListContent: ListContent{ + CreateDate: "2023-06-06T15:58:39.225+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 1, + ListID: "91596_AUDITLOGSTESTLIST", + ListType: "CL", + Name: "AUDIT LOGS - TEST LIST", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + Tags: []string{"green"}, + Type: "IP", + UpdateDate: "2023-06-06T15:58:39.225+00:00", + UpdatedBy: "ccare2", + Version: 1, + }, + Items: []ListItemContent{}, }, }, }, }, "500 internal server error": { params: GetClientListsRequest{}, - headers: http.Header{}, responseStatus: http.StatusInternalServerError, responseBody: ` { - "type": "internal_error", - "title": "Internal Server Error", - "detail": "Error fetching client lists", - "status": 500 + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 }`, expectedPath: uri, withError: &Error{ @@ -248,9 +316,180 @@ func TestClientList_GetClientLists(t *testing.T) { })) client := mockAPIClient(t, mockServer) result, err := client.GetClientLists( + context.Background(), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestGetClientList(t *testing.T) { + uri := "/client-list/v1/lists/12_AB?includeItems=true" + + tests := map[string]struct { + params GetClientListRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetClientListResponse + withError error + }{ + "200 OK": { + params: GetClientListRequest{ + ListID: "12_AB", + IncludeItems: true, + }, + responseStatus: http.StatusOK, + responseBody: `{ + "createDate": "2023-06-06T15:58:39.225+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "itemsCount": 1, + "listId": "12_AB", + "listType": "CL", + "name": "AUDIT LOGS - TEST LIST", + "productionActivationStatus": "INACTIVE", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "productionActiveVersion": 2, + "stagingActiveVersion": 2, + "tags": ["green"], + "type": "IP", + "updateDate": "2023-06-06T15:58:39.225+00:00", + "updatedBy": "ccare2", + "version": 1, + "items": [ + { + "createDate": "2022-07-12T20:14:29.189+00:00", + "createdBy": "ccare2", + "createdVersion": 9, + "productionStatus": "INACTIVE", + "stagingStatus": "PENDING_ACTIVATION", + "tags": [], + "type": "IP", + "updateDate": "2022-07-12T20:14:29.189+00:00", + "updatedBy": "ccare2", + "value": "7d0:1:0::0/64" + }, + { + "createDate": "2022-07-12T20:14:29.189+00:00", + "createdBy": "ccare2", + "createdVersion": 9, + "description": "Item with description, tags, expiration date", + "expirationDate": "2030-12-31T12:40:00.000+00:00", + "productionStatus": "INACTIVE", + "stagingStatus": "PENDING_ACTIVATION", + "tags": [ + "red", + "green", + "blue" + ], + "type": "IP", + "updateDate": "2022-07-12T20:14:29.189+00:00", + "updatedBy": "ccare2", + "value": "7d0:1:1::0/64" + } + ] + }`, + expectedPath: uri, + expectedResponse: &GetClientListResponse{ + ListContent: ListContent{ + CreateDate: "2023-06-06T15:58:39.225+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 1, + ListID: "12_AB", + ListType: "CL", + Name: "AUDIT LOGS - TEST LIST", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + ProductionActiveVersion: 2, + StagingActiveVersion: 2, + Tags: []string{"green"}, + Type: "IP", + UpdateDate: "2023-06-06T15:58:39.225+00:00", + UpdatedBy: "ccare2", + Version: 1, + }, + Items: []ListItemContent{ + { + CreateDate: "2022-07-12T20:14:29.189+00:00", + CreatedBy: "ccare2", + CreatedVersion: 9, + ProductionStatus: "INACTIVE", + StagingStatus: "PENDING_ACTIVATION", + Tags: []string{}, + Type: "IP", + UpdateDate: "2022-07-12T20:14:29.189+00:00", + UpdatedBy: "ccare2", + Value: "7d0:1:0::0/64", + }, + { + CreateDate: "2022-07-12T20:14:29.189+00:00", + CreatedBy: "ccare2", + CreatedVersion: 9, + ProductionStatus: "INACTIVE", + StagingStatus: "PENDING_ACTIVATION", + Tags: []string{"red", "green", "blue"}, + Description: "Item with description, tags, expiration date", + ExpirationDate: "2030-12-31T12:40:00.000+00:00", + Type: "IP", + UpdateDate: "2022-07-12T20:14:29.189+00:00", + UpdatedBy: "ccare2", + Value: "7d0:1:1::0/64", + }, + }, + }, + }, + "500 internal server error": { + params: GetClientListRequest{ + ListID: "12_AB", + IncludeItems: true, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: GetClientListRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetClientList( session.ContextWithOptions( context.Background(), - session.WithContextHeaders(test.headers), ), test.params) if test.withError != nil { @@ -262,3 +501,507 @@ func TestClientList_GetClientLists(t *testing.T) { }) } } + +func TestUpdateClientList(t *testing.T) { + uri := "/client-list/v1/lists/12_12" + request := UpdateClientListRequest{ + UpdateClientList: UpdateClientList{ + Name: "Some New Name", + Tags: []string{"red"}, + Notes: "Updating list notes", + }, + ListID: "12_12", + } + result := UpdateClientListResponse{ + ContractID: "M-2CF0QRI", + GroupName: "Kona QA16-M-2CF0QRI", + ListContent: ListContent{ + CreateDate: "2023-04-03T15:50:34.074+00:00", + CreatedBy: "ccare2", + Deprecated: false, + ItemsCount: 51, + ListID: "12_12", + ListType: "CL", + Name: "Some New Name", + Tags: []string{"red"}, + Notes: "Updating list notes", + ProductionActivationStatus: "INACTIVE", + ReadOnly: false, + Shared: false, + StagingActivationStatus: "INACTIVE", + ProductionActiveVersion: 2, + StagingActiveVersion: 2, + Type: "IP", + UpdateDate: "2023-06-15T20:28:09.047+00:00", + UpdatedBy: "ccare2", + Version: 75, + }, + } + + tests := map[string]struct { + params UpdateClientListRequest + expectedRequestBody string + responseStatus int + responseBody string + expectedPath string + expectedResponse *UpdateClientListResponse + withError error + }{ + "200 OK": { + params: request, + expectedRequestBody: `{"name":"Some New Name","notes":"Updating list notes","tags":["red"]}`, + responseStatus: http.StatusOK, + responseBody: `{ + "contractId": "M-2CF0QRI", + "createDate": "2023-04-03T15:50:34.074+00:00", + "createdBy": "ccare2", + "deprecated": false, + "filePrefix": "CL", + "groupName": "Kona QA16-M-2CF0QRI", + "itemsCount": 51, + "listId": "12_12", + "listType": "CL", + "name": "Some New Name", + "tags": [ "red"], + "notes": "Updating list notes", + "productionActivationStatus": "INACTIVE", + "readOnly": false, + "shared": false, + "stagingActivationStatus": "INACTIVE", + "productionActiveVersion": 2, + "stagingActiveVersion": 2, + "type": "IP", + "updateDate": "2023-06-15T20:28:09.047+00:00", + "updatedBy": "ccare2", + "version": 75 + }`, + expectedPath: uri, + expectedResponse: &result, + }, + "500 internal server error": { + params: request, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: UpdateClientListRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + + if len(test.expectedRequestBody) > 0 { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateClientList( + context.Background(), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} +func TestUpdateClientListItems(t *testing.T) { + uri := "/client-list/v1/lists/12_12/items" + request := UpdateClientListItemsRequest{ + ListID: "12_12", + UpdateClientListItems: UpdateClientListItems{ + Append: []ListItemPayload{ + { + Description: "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s...", + ExpirationDate: "2026-12-26T01:32:08.375+00:00", + Value: "1.1.1.72", + }, + }, + Update: []ListItemPayload{ + { + Description: "remove exp date and tags", + ExpirationDate: "", + Tags: []string{"t"}, + Value: "1.1.1.45", + }, + { + ExpirationDate: "2028-11-26T17:32:08.375+00:00", + Value: "1.1.1.33", + }, + }, + Delete: []ListItemPayload{ + { + Value: "1.1.1.38", + }, + }, + }, + } + result := UpdateClientListItemsResponse{ + Appended: []ListItemContent{ + { + Description: "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley", + ExpirationDate: "2026-12-26T01:32:08.375+00:00", + Tags: []string{"new tag"}, + Value: "1.1.1.75", + CreateDate: "2023-06-15T20:46:30.780+00:00", + CreatedBy: "ccare2", + CreatedVersion: 76, + ProductionStatus: "INACTIVE", + StagingStatus: "INACTIVE", + Type: "IP", + UpdateDate: "2023-06-15T20:46:30.780+00:00", + UpdatedBy: "ccare2", + }, + }, + Deleted: []ListItemContent{ + { + Value: "1.1.1.39", + }, + }, + Updated: []ListItemContent{ + { + Description: "remove exp date and tags", + Tags: []string{"t1"}, + Value: "1.1.1.45", + CreateDate: "2023-04-28T19:34:00.906+00:00", + CreatedBy: "ccare2", + CreatedVersion: 54, + ProductionStatus: "INACTIVE", + StagingStatus: "INACTIVE", + Type: "IP", + UpdateDate: "2023-06-15T20:46:30.765+00:00", + UpdatedBy: "ccare2", + }, + }, + } + + tests := map[string]struct { + params UpdateClientListItemsRequest + expectedRequestBody string + responseStatus int + responseBody string + expectedPath string + expectedResponse *UpdateClientListItemsResponse + withError error + }{ + "200 OK": { + params: request, + expectedRequestBody: `{"append":[{"value":"1.1.1.72","tags":null,"description":"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s...","expirationDate":"2026-12-26T01:32:08.375+00:00"}],"update":[{"value":"1.1.1.45","tags":["t"],"description":"remove exp date and tags","expirationDate":""},{"value":"1.1.1.33","tags":null,"description":"","expirationDate":"2028-11-26T17:32:08.375+00:00"}],"delete":[{"value":"1.1.1.38","tags":null,"description":"","expirationDate":""}]}`, + responseStatus: http.StatusOK, + responseBody: `{ + "appended": [ + { + "createDate": "2023-06-15T20:46:30.780+00:00", + "createdBy": "ccare2", + "createdVersion": 76, + "description": "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley", + "expirationDate": "2026-12-26T01:32:08.375+00:00", + "productionStatus": "INACTIVE", + "stagingStatus": "INACTIVE", + "tags": [ + "new tag" + ], + "type": "IP", + "updateDate": "2023-06-15T20:46:30.780+00:00", + "updatedBy": "ccare2", + "value": "1.1.1.75" + } + ], + "deleted": [ + { + "value": "1.1.1.39" + } + ], + "updated": [ + { + "createDate": "2023-04-28T19:34:00.906+00:00", + "createdBy": "ccare2", + "createdVersion": 54, + "description": "remove exp date and tags", + "productionStatus": "INACTIVE", + "stagingStatus": "INACTIVE", + "tags": [ + "t1" + ], + "type": "IP", + "updateDate": "2023-06-15T20:46:30.765+00:00", + "updatedBy": "ccare2", + "value": "1.1.1.45" + } + ] + }`, + expectedPath: uri, + expectedResponse: &result, + }, + "500 internal server error": { + params: request, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: UpdateClientListItemsRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + + if len(test.expectedRequestBody) > 0 { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateClientListItems( + context.Background(), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestCreateClientLists(t *testing.T) { + uri := "/client-list/v1/lists" + request := CreateClientListRequest{ + Name: "TEST LIST", + Type: "IP", + Notes: "Some notes", + Tags: []string{"red", "green"}, + ContractID: "M-2CF0QRI", + GroupID: 112524, + Items: []ListItemPayload{ + { + Value: "1.1.1.1", + Description: "some description", + Tags: []string{}, + ExpirationDate: "2026-12-26T01:32:08.375+00:00", + }, + }, + } + result := CreateClientListResponse{ + ListContent: ListContent{ + ListID: "123_ABC", + Name: "TEST LIST", + Type: "IP", + Notes: "Some notes", + Tags: []string{"red", "green"}, + }, + ContractID: "M-2CF0QRI", + GroupName: "Group A", + Items: []ListItemContent{ + { + Value: "1.1.1.1", + Description: "", + Tags: []string{}, + ExpirationDate: "2026-12-26T01:32:08.375+00:00", + }, + }, + } + + tests := map[string]struct { + params CreateClientListRequest + expectedRequestBody string + responseStatus int + responseBody string + expectedPath string + expectedResponse *CreateClientListResponse + withError error + }{ + "201 Created": { + params: request, + expectedRequestBody: `{"contractId":"M-2CF0QRI","groupId":112524,"name":"TEST LIST","type":"IP","notes":"Some notes","tags":["red","green"],"items":[{"value":"1.1.1.1","tags":[],"description":"some description","expirationDate":"2026-12-26T01:32:08.375+00:00"}]}`, + responseStatus: http.StatusCreated, + responseBody: `{ + "listId": "123_ABC", + "name": "TEST LIST", + "type": "IP", + "notes": "Some notes", + "tags": [ + "red", + "green" + ], + "contractId": "M-2CF0QRI", + "groupName": "Group A", + "items": [ + { + "value": "1.1.1.1", + "description": "", + "tags": [], + "expirationDate": "2026-12-26T01:32:08.375+00:00" + } + ] + } + `, + expectedPath: uri, + expectedResponse: &result, + }, + "500 internal server error": { + params: request, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: CreateClientListRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + + if len(test.expectedRequestBody) > 0 { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreateClientList( + context.Background(), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestDeleteClientLists(t *testing.T) { + uri := "/client-list/v1/lists/12_AB" + request := DeleteClientListRequest{ + ListID: "12_AB", + } + + tests := map[string]struct { + params DeleteClientListRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *Error + withError error + }{ + "204 NoContent": { + params: request, + responseBody: "", + responseStatus: http.StatusNoContent, + expectedPath: uri, + expectedResponse: nil, + }, + "500 internal server error": { + params: request, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: DeleteClientListRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodDelete, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + err := client.DeleteClientList( + context.Background(), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/pkg/clientlists/mocks.go b/pkg/clientlists/mocks.go index 22123f9a..413f0f4b 100644 --- a/pkg/clientlists/mocks.go +++ b/pkg/clientlists/mocks.go @@ -1,3 +1,5 @@ +//revive:disable:exported + package clientlists import ( @@ -23,3 +25,48 @@ func (p *Mock) GetClientLists(ctx context.Context, params GetClientListsRequest) return args.Get(0).(*GetClientListsResponse), args.Error(1) } + +func (p *Mock) GetClientList(ctx context.Context, params GetClientListRequest) (*GetClientListResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetClientListResponse), args.Error(1) +} + +func (p *Mock) CreateClientList(ctx context.Context, params CreateClientListRequest) (*CreateClientListResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*CreateClientListResponse), args.Error(1) +} + +func (p *Mock) UpdateClientList(ctx context.Context, params UpdateClientListRequest) (*UpdateClientListResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*UpdateClientListResponse), args.Error(1) +} + +func (p *Mock) UpdateClientListItems(ctx context.Context, params UpdateClientListItemsRequest) (*UpdateClientListItemsResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*UpdateClientListItemsResponse), args.Error(1) +} + +func (p *Mock) DeleteClientList(ctx context.Context, params DeleteClientListRequest) error { + args := p.Called(ctx, params) + return args.Error(0) +} From d0f047c8f8d8a583fd89eb3b5239786a56983d9e Mon Sep 17 00:00:00 2001 From: Hamza Bentebbaa Date: Tue, 25 Jul 2023 12:22:49 +0000 Subject: [PATCH 14/17] SECKSD-20409 Add Client Lists activation apis support Merge in DEVEXP/akamaiopen-edgegrid-golang from feature/SECKSD-20409-client-lists-activation-apis to feature/sp-clientlists-july-2023 --- CHANGELOG.md | 6 +- pkg/clientlists/client_list_activation.go | 238 +++++++++++++ .../client_list_activation_test.go | 321 ++++++++++++++++++ pkg/clientlists/clientlists.go | 1 + pkg/clientlists/mocks.go | 30 ++ 5 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 pkg/clientlists/client_list_activation.go create mode 100644 pkg/clientlists/client_list_activation_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e9961ea5..f00e10ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ * [UpdateClientListItems](https://techdocs.akamai.com/client-lists/reference/post-update-items) * [CreateClientList](https://techdocs.akamai.com/client-lists/reference/post-create-list) * [DeleteClientList](https://techdocs.akamai.com/client-lists/reference/delete-list) + * [GetActivation](https://techdocs.akamai.com/client-lists/reference/get-retrieve-activation-status) + * [GetActivationStatus](https://techdocs.akamai.com/client-lists/reference/get-activation-status) + * [CreateActivation](https://techdocs.akamai.com/client-lists/reference/post-activate-list) + ## 7.1.0 (July 25, 2023) @@ -63,7 +67,7 @@ * DataStream * Updated `connectors` details in DataStream 2 API v2. * Updated `GetProperties` and `GetDatasetFields` methods in DataStream 2 API v2. - * Updated `CreateStream`, `GetStream`, `UpdateStream`, `DeleteStream` and `ListStreams` methods in DataStream 2 API v2. + * Updated `CreateStream`, `GetStream`, `UpdateStream`, `DeleteStream` and `ListStreams` methods in DataStream 2 API v2. * Updated `Activate`, `Deactivate`, `ActivationHistory` and `Stream` details in DataStream 2 API v2 and also changed their corresponding response objects. ### FEATURES/ENHANCEMENTS: diff --git a/pkg/clientlists/client_list_activation.go b/pkg/clientlists/client_list_activation.go new file mode 100644 index 00000000..2b9c12ab --- /dev/null +++ b/pkg/clientlists/client_list_activation.go @@ -0,0 +1,238 @@ +package clientlists + +import ( + "context" + "fmt" + "net/http" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // Activation interface to support activating client lists. + Activation interface { + // GetActivation retrieves details of a specified activation ID. + // + // See: https://techdocs.akamai.com/client-lists/reference/get-retrieve-activation-status + GetActivation(ctx context.Context, params GetActivationRequest) (*GetActivationResponse, error) + + // GetActivationStatus retrieves activation status for a client list in a network environment. + // + // See: https://techdocs.akamai.com/client-lists/reference/get-activation-status + GetActivationStatus(ctx context.Context, params GetActivationStatusRequest) (*GetActivationStatusResponse, error) + + // CreateActivation activates a client list + // + // See: https://techdocs.akamai.com/client-lists/reference/post-activate-list + CreateActivation(ctx context.Context, params CreateActivationRequest) (*CreateActivationResponse, error) + } + + // ActivationParams contains activation general parameters + ActivationParams struct { + Action ActivationAction `json:"action"` + Comments string `json:"comments"` + Network ActivationNetwork `json:"network"` + NotificationRecipients []string `json:"notificationRecipients"` + SiebelTicketID string `json:"siebelTicketId"` + } + + // GetActivationRequest contains activation request param + GetActivationRequest struct { + ActivationID int64 + } + + // GetActivationResponse contains activation details + GetActivationResponse struct { + ActivationID int64 `json:"activationId"` + CreateDate string `json:"createDate"` + CreatedBy string `json:"createdBy"` + Environment ActivationNetwork `json:"environment"` + Fast bool `json:"fast"` + InitialActivation bool `json:"initialActivation"` + Status ActivationStatus `json:"status"` + ClientList ListInfo `json:"clientList"` + ActivationParams + } + + // ListInfo contains Client List details + ListInfo struct { + ListID string `json:"listId"` + Version int64 `json:"version"` + } + + // CreateActivationRequest contains activation request parameters for CreateActivation method + CreateActivationRequest struct { + ListID string + ActivationParams + } + + // CreateActivationResponse contains activation response + CreateActivationResponse GetActivationStatusResponse + + // GetActivationStatusRequest contains request params for GetActivationStatus + GetActivationStatusRequest struct { + ListID string + Network ActivationNetwork + } + + // GetActivationStatusResponse contains activation status response + GetActivationStatusResponse struct { + Action ActivationAction `json:"action"` + ActivationID int64 `json:"activationId"` + ActivationStatus ActivationStatus `json:"activationStatus"` + Comments string `json:"comments"` + CreateDate string `json:"createDate"` + CreatedBy string `json:"createdBy"` + ListID string `json:"listId"` + Network ActivationNetwork `json:"network"` + NotificationRecipients []string `json:"notificationRecipients"` + SiebelTicketID string `json:"siebelTicketId"` + Version int64 `json:"version"` + } + + // ActivationNetwork is a type for network field + ActivationNetwork string + + // ActivationStatus is a type for activationStatus field + ActivationStatus string + + // ActivationAction is a type for action field + ActivationAction string +) + +const ( + // Staging activation network value STAGING + Staging ActivationNetwork = "STAGING" + // Production activation network value PRODUCTION + Production ActivationNetwork = "PRODUCTION" + + // Inactive activation status value INACTIVE + Inactive ActivationStatus = "INACTIVE" + // PendingActivation activation status value PENDING_ACTIVATION + PendingActivation ActivationStatus = "PENDING_ACTIVATION" + // Active activation status value ACTIVE + Active ActivationStatus = "ACTIVE" + // Modified activation status value MODIFIED + Modified ActivationStatus = "MODIFIED" + // PendingDeactivation activation status value PENDING_DEACTIVATION + PendingDeactivation ActivationStatus = "PENDING_DEACTIVATION" + // Failed activation status value FAILED + Failed ActivationStatus = "FAILED" + + // Activate action value ACTIVATE + Activate ActivationAction = "ACTIVATE" +) + +func (v GetActivationRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ActivationID": validation.Validate(v.ActivationID, validation.Required), + }) +} + +func (v GetActivationStatusRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ListID": validation.Validate(v.ListID, validation.Required), + "Network": validation.Validate(v.Network, + validation.Required, + validation.In(Staging, Production), + ), + }) +} + +func (v CreateActivationRequest) validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ListID": validation.Validate(v.ListID, validation.Required), + "Network": validation.Validate(v.Network, + validation.Required, + validation.In(Staging, Production), + ), + }) +} + +func (p *clientlists) CreateActivation(ctx context.Context, params CreateActivationRequest) (*CreateActivationResponse, error) { + logger := p.Log(ctx) + logger.Debug("Create Activation") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/client-list/v1/lists/%s/activations", params.ListID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return nil, fmt.Errorf("create 'create activation' request failed: %s", err.Error()) + } + + var rval CreateActivationResponse + + resp, err := p.Exec(req, &rval, params.ActivationParams) + if err != nil { + return nil, fmt.Errorf("create activation request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} + +func (p *clientlists) GetActivationStatus(ctx context.Context, params GetActivationStatusRequest) (*GetActivationStatusResponse, error) { + logger := p.Log(ctx) + logger.Debug("Get Activation Status") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/client-list/v1/lists/%s/environments/%s/status", params.ListID, params.Network) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("create get activation status request failed: %s", err.Error()) + } + + var rval GetActivationStatusResponse + + resp, err := p.Exec(req, &rval, params) + if err != nil { + return nil, fmt.Errorf("get activation status request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} + +func (p *clientlists) GetActivation(ctx context.Context, params GetActivationRequest) (*GetActivationResponse, error) { + logger := p.Log(ctx) + logger.Debug("Get Activation") + + if err := params.validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/client-list/v1/activations/%d", params.ActivationID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("create get activation request failed: %s", err.Error()) + } + + var rval GetActivationResponse + + resp, err := p.Exec(req, &rval, params) + if err != nil { + return nil, fmt.Errorf("get activation request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusOK { + return nil, p.Error(resp) + } + + return &rval, nil +} diff --git a/pkg/clientlists/client_list_activation_test.go b/pkg/clientlists/client_list_activation_test.go new file mode 100644 index 00000000..986e8253 --- /dev/null +++ b/pkg/clientlists/client_list_activation_test.go @@ -0,0 +1,321 @@ +package clientlists + +import ( + "context" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/require" + "github.com/tj/assert" +) + +func TestCreateActivation(t *testing.T) { + uri := "/client-list/v1/lists/1234_NORTHAMERICAGEOALLOWLIST/activations" + + tests := map[string]struct { + params CreateActivationRequest + responseStatus int + expectedRequestBody string + responseBody string + expectedPath string + expectedResponse *CreateActivationResponse + withError error + }{ + "200 OK": { + params: CreateActivationRequest{ + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + ActivationParams: ActivationParams{ + Action: Activate, + Network: Production, + Comments: "Activation of GEO allowlist list", + SiebelTicketID: "12_B", + NotificationRecipients: []string{"a@a.com", "c@c.com"}, + }, + }, + expectedRequestBody: `{"action":"ACTIVATE","comments":"Activation of GEO allowlist list","network":"PRODUCTION","notificationRecipients":["a@a.com","c@c.com"],"siebelTicketId":"12_B"}`, + responseStatus: http.StatusOK, + responseBody: `{ + "action": "ACTIVATE", + "activationStatus": "PENDING_ACTIVATION", + "listId": "1234_NORTHAMERICAGEOALLOWLIST", + "network": "PRODUCTION", + "notificationRecipients": [], + "version": 1, + "activationId": 12, + "createDate": "2023-04-05T18:46:56.365Z", + "createdBy": "jdoe", + "network": "PRODUCTION", + "comments": "Activation of GEO allowlist list", + "siebelTicketId": "12_AB" + }`, + expectedPath: uri, + expectedResponse: &CreateActivationResponse{ + Action: "ACTIVATE", + ActivationID: 12, + ActivationStatus: PendingActivation, + CreateDate: "2023-04-05T18:46:56.365Z", + CreatedBy: "jdoe", + Comments: "Activation of GEO allowlist list", + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + Network: Production, + NotificationRecipients: []string{}, + SiebelTicketID: "12_AB", + Version: 1, + }, + }, + "500 internal server error": { + params: CreateActivationRequest{ + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + ActivationParams: ActivationParams{ + Network: Production, + }, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating client lists activation", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error creating client lists activation", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: CreateActivationRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + + if len(test.expectedRequestBody) > 0 { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreateActivation( + session.ContextWithOptions( + context.Background(), + ), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestGetActivation(t *testing.T) { + uri := "/client-list/v1/activations/12" + + tests := map[string]struct { + params GetActivationRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetActivationResponse + withError error + }{ + "200 OK": { + params: GetActivationRequest{ + ActivationID: 12, + }, + responseStatus: http.StatusOK, + responseBody: `{ + "activationId": 12, + "clientList": { + "listId": "1234_NORTHAMERICAGEOALLOWLIST", + "version": 1 + }, + "createDate": "2023-04-05T18:46:56.365Z", + "createdBy": "jdoe", + "environment": "PRODUCTION", + "fast": true, + "initialActivation": false, + "status": "PENDING_ACTIVATION" + }`, + expectedPath: uri, + expectedResponse: &GetActivationResponse{ + ActivationID: 12, + ClientList: ListInfo{ + Version: 1, + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + }, + CreateDate: "2023-04-05T18:46:56.365Z", + CreatedBy: "jdoe", + Environment: Production, + Fast: true, + InitialActivation: false, + Status: "PENDING_ACTIVATION", + }, + }, + "500 internal server error": { + params: GetActivationRequest{ + ActivationID: 12, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists activation", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists activation", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: GetActivationRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetActivation( + session.ContextWithOptions( + context.Background(), + ), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestGetActivationStatus(t *testing.T) { + uri := "/client-list/v1/lists/1234_NORTHAMERICAGEOALLOWLIST/environments/PRODUCTION/status" + + tests := map[string]struct { + params GetActivationStatusRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetActivationStatusResponse + withError error + }{ + "200 OK": { + params: GetActivationStatusRequest{ + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + Network: Production, + }, + responseStatus: http.StatusOK, + responseBody: `{ + "action": "ACTIVATE", + "activationStatus": "PENDING_ACTIVATION", + "listId": "1234_NORTHAMERICAGEOALLOWLIST", + "network": "PRODUCTION", + "notificationRecipients": [], + "version": 1, + "activationId": 12, + "createDate": "2023-04-05T18:46:56.365Z", + "createdBy": "jdoe", + "network": "PRODUCTION", + "comments": "Activation of GEO allowlist list", + "siebelTicketId": "12_AB" + }`, + expectedPath: uri, + expectedResponse: &GetActivationStatusResponse{ + Action: "ACTIVATE", + ActivationID: 12, + ActivationStatus: PendingActivation, + CreateDate: "2023-04-05T18:46:56.365Z", + CreatedBy: "jdoe", + Comments: "Activation of GEO allowlist list", + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + Network: Production, + NotificationRecipients: []string{}, + SiebelTicketID: "12_AB", + Version: 1, + }, + }, + "500 internal server error": { + params: GetActivationStatusRequest{ + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + Network: Production, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching client lists activation", + "status": 500 + }`, + expectedPath: uri, + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching client lists activation", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error": { + params: GetActivationStatusRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetActivationStatus( + session.ContextWithOptions( + context.Background(), + ), + test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/clientlists/clientlists.go b/pkg/clientlists/clientlists.go index 5af01ff2..1ea40582 100644 --- a/pkg/clientlists/clientlists.go +++ b/pkg/clientlists/clientlists.go @@ -17,6 +17,7 @@ var ( type ( // ClientLists is the clientlists api interface ClientLists interface { + Activation Lists } diff --git a/pkg/clientlists/mocks.go b/pkg/clientlists/mocks.go index 413f0f4b..1f2ec675 100644 --- a/pkg/clientlists/mocks.go +++ b/pkg/clientlists/mocks.go @@ -70,3 +70,33 @@ func (p *Mock) DeleteClientList(ctx context.Context, params DeleteClientListRequ args := p.Called(ctx, params) return args.Error(0) } + +func (p *Mock) GetActivation(ctx context.Context, params GetActivationRequest) (*GetActivationResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetActivationResponse), args.Error(1) +} + +func (p *Mock) GetActivationStatus(ctx context.Context, params GetActivationStatusRequest) (*GetActivationStatusResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetActivationStatusResponse), args.Error(1) +} + +func (p *Mock) CreateActivation(ctx context.Context, params CreateActivationRequest) (*CreateActivationResponse, error) { + args := p.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*CreateActivationResponse), args.Error(1) +} From ae2b2ff8cae6b7182f954066b4bd1ea366701112 Mon Sep 17 00:00:00 2001 From: Hamza Bentebbaa Date: Fri, 28 Jul 2023 13:59:30 +0000 Subject: [PATCH 15/17] SECKSD-20409 Update request response Merge in DEVEXP/akamaiopen-edgegrid-golang from feature/SECKSD-20409-update-request-response to feature/sp-clientlists-august-2023 --- pkg/clientlists/client_list_activation.go | 22 ++++------- .../client_list_activation_test.go | 39 +++++++++++-------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/pkg/clientlists/client_list_activation.go b/pkg/clientlists/client_list_activation.go index 2b9c12ab..90511773 100644 --- a/pkg/clientlists/client_list_activation.go +++ b/pkg/clientlists/client_list_activation.go @@ -44,23 +44,17 @@ type ( // GetActivationResponse contains activation details GetActivationResponse struct { - ActivationID int64 `json:"activationId"` - CreateDate string `json:"createDate"` - CreatedBy string `json:"createdBy"` - Environment ActivationNetwork `json:"environment"` - Fast bool `json:"fast"` - InitialActivation bool `json:"initialActivation"` - Status ActivationStatus `json:"status"` - ClientList ListInfo `json:"clientList"` + ActivationID int64 `json:"activationId"` + CreateDate string `json:"createDate"` + CreatedBy string `json:"createdBy"` + Fast bool `json:"fast"` + InitialActivation bool `json:"initialActivation"` + ActivationStatus ActivationStatus `json:"activationStatus"` + ListID string `json:"listId"` + Version int64 `json:"version"` ActivationParams } - // ListInfo contains Client List details - ListInfo struct { - ListID string `json:"listId"` - Version int64 `json:"version"` - } - // CreateActivationRequest contains activation request parameters for CreateActivation method CreateActivationRequest struct { ListID string diff --git a/pkg/clientlists/client_list_activation_test.go b/pkg/clientlists/client_list_activation_test.go index 986e8253..5d852594 100644 --- a/pkg/clientlists/client_list_activation_test.go +++ b/pkg/clientlists/client_list_activation_test.go @@ -43,7 +43,7 @@ func TestCreateActivation(t *testing.T) { "activationStatus": "PENDING_ACTIVATION", "listId": "1234_NORTHAMERICAGEOALLOWLIST", "network": "PRODUCTION", - "notificationRecipients": [], + "notificationRecipients": ["aa@dd.com"], "version": 1, "activationId": 12, "createDate": "2023-04-05T18:46:56.365Z", @@ -62,7 +62,7 @@ func TestCreateActivation(t *testing.T) { Comments: "Activation of GEO allowlist list", ListID: "1234_NORTHAMERICAGEOALLOWLIST", Network: Production, - NotificationRecipients: []string{}, + NotificationRecipients: []string{"aa@dd.com"}, SiebelTicketID: "12_AB", Version: 1, }, @@ -144,31 +144,38 @@ func TestGetActivation(t *testing.T) { }, responseStatus: http.StatusOK, responseBody: `{ + "action": "ACTIVATE", "activationId": 12, - "clientList": { - "listId": "1234_NORTHAMERICAGEOALLOWLIST", - "version": 1 - }, + "activationStatus": "PENDING_ACTIVATION", + "comments": "latest activation", "createDate": "2023-04-05T18:46:56.365Z", "createdBy": "jdoe", - "environment": "PRODUCTION", "fast": true, - "initialActivation": false, - "status": "PENDING_ACTIVATION" + "listId": "1234_NORTHAMERICAGEOALLOWLIST", + "network": "PRODUCTION", + "notificationRecipients": [ + "qw@ff.com" + ], + "siebelTicketId": "q", + "version": 1 }`, expectedPath: uri, expectedResponse: &GetActivationResponse{ - ActivationID: 12, - ClientList: ListInfo{ - Version: 1, - ListID: "1234_NORTHAMERICAGEOALLOWLIST", - }, + ActivationID: 12, + ListID: "1234_NORTHAMERICAGEOALLOWLIST", + Version: 1, CreateDate: "2023-04-05T18:46:56.365Z", CreatedBy: "jdoe", - Environment: Production, Fast: true, InitialActivation: false, - Status: "PENDING_ACTIVATION", + ActivationStatus: "PENDING_ACTIVATION", + ActivationParams: ActivationParams{ + Action: Activate, + NotificationRecipients: []string{"qw@ff.com"}, + Comments: "latest activation", + Network: Production, + SiebelTicketID: "q", + }, }, }, "500 internal server error": { From 45d54256181bdb03182b13d8ab01b411b9a68816 Mon Sep 17 00:00:00 2001 From: Amith Halavarthy Basavanagouda Date: Mon, 7 Aug 2023 10:25:34 +0000 Subject: [PATCH 16/17] BOTMAN-11848 Added api support for custom client sequence - read and update Merge in DEVEXP/akamaiopen-edgegrid-golang from feature/BOTMAN-11848 to feature/sp-security-august-2023 --- CHANGELOG.md | 5 +- pkg/appsec/export_configuration.go | 1 + .../ExportConfiguration.json | 4 + pkg/botman/botman.go | 1 + pkg/botman/custom_client_sequence.go | 121 ++++++++++ pkg/botman/custom_client_sequence_test.go | 217 ++++++++++++++++++ pkg/botman/mocks.go | 16 ++ pkg/botman/validation_response.go | 17 ++ 8 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 pkg/botman/custom_client_sequence.go create mode 100644 pkg/botman/custom_client_sequence_test.go create mode 100644 pkg/botman/validation_response.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f00e10ef..dbe058ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ #### FEATURES/ENHANCEMENTS: +* APPSEC + * Added Bot Management API Support + * Custom Client Sequence - read and update + * [IMPORTANT] Added CloudWrapper API support * Capacities * [ListCapacities](https://techdocs.akamai.com/cloud-wrapper/reference/get-capacity-inventory) @@ -35,7 +39,6 @@ * [GetActivationStatus](https://techdocs.akamai.com/client-lists/reference/get-activation-status) * [CreateActivation](https://techdocs.akamai.com/client-lists/reference/post-activate-list) - ## 7.1.0 (July 25, 2023) ### FEATURES/ENHANCEMENTS: diff --git a/pkg/appsec/export_configuration.go b/pkg/appsec/export_configuration.go index 7f9cd089..34eb121e 100644 --- a/pkg/appsec/export_configuration.go +++ b/pkg/appsec/export_configuration.go @@ -246,6 +246,7 @@ type ( CustomDefinedBots []map[string]interface{} `json:"customDefinedBots,omitempty"` CustomBotCategorySequence []string `json:"customBotCategorySequence,omitempty"` CustomClients []map[string]interface{} `json:"customClients,omitempty"` + CustomClientSequence []string `json:"customClientSequence,omitempty"` ResponseActions *ResponseActions `json:"responseActions,omitempty"` AdvancedSettings *AdvancedSettings `json:"advancedSettings,omitempty"` } diff --git a/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json b/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json index ab2817a2..1a4c4368 100644 --- a/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json +++ b/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json @@ -5150,6 +5150,10 @@ } } ], + "customClientSequence": [ + "a7fe489d-0354-43bd-b81c-8cabbe850cdd", + "60374346-2d1d-444d-91c1-90373e3f804a" + ], "customDefinedBots": [ { "botId": "50789280-ba99-4f8f-b4c6-ad9c1c69569a", diff --git a/pkg/botman/botman.go b/pkg/botman/botman.go index 4b5310d7..679eb0a0 100644 --- a/pkg/botman/botman.go +++ b/pkg/botman/botman.go @@ -34,6 +34,7 @@ type ( CustomBotCategoryAction CustomBotCategorySequence CustomClient + CustomClientSequence CustomDefinedBot CustomDenyAction JavascriptInjection diff --git a/pkg/botman/custom_client_sequence.go b/pkg/botman/custom_client_sequence.go new file mode 100644 index 00000000..299b6de2 --- /dev/null +++ b/pkg/botman/custom_client_sequence.go @@ -0,0 +1,121 @@ +package botman + +import ( + "context" + "fmt" + "net/http" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // The CustomClientSequence interface supports retrieving and updating the custom client sequence for a configuration + CustomClientSequence interface { + // GetCustomClientSequence is used to retrieve the custom client sequence for a config version + // See https://techdocs.akamai.com/bot-manager/reference/get-custom-client-sequence + GetCustomClientSequence(ctx context.Context, params GetCustomClientSequenceRequest) (*CustomClientSequenceResponse, error) + + // UpdateCustomClientSequence is used to update the existing custom client sequence for a config version + // See https://techdocs.akamai.com/bot-manager/reference/put-custom-client-sequence + UpdateCustomClientSequence(ctx context.Context, params UpdateCustomClientSequenceRequest) (*CustomClientSequenceResponse, error) + } + + // GetCustomClientSequenceRequest is used to retrieve custom client sequence + GetCustomClientSequenceRequest struct { + ConfigID int64 + Version int64 + } + + // UpdateCustomClientSequenceRequest is used to modify custom client sequence + UpdateCustomClientSequenceRequest struct { + ConfigID int64 `json:"-"` + Version int64 `json:"-"` + Sequence []string `json:"sequence"` + } + + // CustomClientSequenceResponse is used to represent custom client sequence + CustomClientSequenceResponse struct { + Sequence []string `json:"sequence"` + Validation ValidationResponse `json:"validation"` + } +) + +// Validate validates a GetCustomClientSequenceRequest. +func (v GetCustomClientSequenceRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + }) +} + +// Validate validates an UpdateCustomClientSequenceRequest. +func (v UpdateCustomClientSequenceRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "Sequence": validation.Validate(v.Sequence, validation.Required), + }) +} + +func (b *botman) GetCustomClientSequence(ctx context.Context, params GetCustomClientSequenceRequest) (*CustomClientSequenceResponse, error) { + logger := b.Log(ctx) + logger.Debug("GetCustomClientSequence") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/custom-client-sequence", + params.ConfigID, + params.Version) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetCustomClientSequence request: %w", err) + } + + var result CustomClientSequenceResponse + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetCustomClientSequence request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return &result, nil +} + +func (b *botman) UpdateCustomClientSequence(ctx context.Context, params UpdateCustomClientSequenceRequest) (*CustomClientSequenceResponse, error) { + logger := b.Log(ctx) + logger.Debug("UpdateCustomClientSequence") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/custom-client-sequence", + params.ConfigID, + params.Version) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateCustomClientSequence request: %w", err) + } + + var result CustomClientSequenceResponse + resp, err := b.Exec(req, &result, params) + if err != nil { + return nil, fmt.Errorf("UpdateCustomClientSequence request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return &result, nil +} diff --git a/pkg/botman/custom_client_sequence_test.go b/pkg/botman/custom_client_sequence_test.go new file mode 100644 index 00000000..21b11158 --- /dev/null +++ b/pkg/botman/custom_client_sequence_test.go @@ -0,0 +1,217 @@ +package botman + +import ( + "context" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test Get CustomClientSequence +func TestBotman_GetCustomClientSequence(t *testing.T) { + tests := map[string]struct { + params GetCustomClientSequenceRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *CustomClientSequenceResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetCustomClientSequenceRequest{ + ConfigID: 43253, + Version: 15, + }, + responseStatus: http.StatusOK, + responseBody: `{"sequence":["cc9c3f89-e179-4892-89cf-d5e623ba9dc7","d79285df-e399-43e8-bb0f-c0d980a88e4f","afa309b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-client-sequence", + expectedResponse: &CustomClientSequenceResponse{ + Sequence: []string{"cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + }, + "500 internal server error": { + params: GetCustomClientSequenceRequest{ + ConfigID: 43253, + Version: 15, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-client-sequence", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching data", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: GetCustomClientSequenceRequest{ + Version: 15, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "ConfigID") + }, + }, + "Missing Version": { + params: GetCustomClientSequenceRequest{ + ConfigID: 43253, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "Version") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetCustomClientSequence(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +// Test Update CustomClientSequence. +func TestBotman_UpdateCustomClientSequence(t *testing.T) { + tests := map[string]struct { + params UpdateCustomClientSequenceRequest + expectedRequestBody string + responseStatus int + responseBody string + expectedPath string + expectedResponse *CustomClientSequenceResponse + withError func(*testing.T, error) + }{ + "200 Success": { + params: UpdateCustomClientSequenceRequest{ + ConfigID: 43253, + Version: 15, + Sequence: []string{"cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + expectedRequestBody: `{"sequence":["cc9c3f89-e179-4892-89cf-d5e623ba9dc7","d79285df-e399-43e8-bb0f-c0d980a88e4f","afa309b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + responseStatus: http.StatusOK, + responseBody: `{"sequence":["cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + expectedResponse: &CustomClientSequenceResponse{ + Sequence: []string{"cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-client-sequence", + }, + "500 internal server error": { + params: UpdateCustomClientSequenceRequest{ + ConfigID: 43253, + Version: 15, + Sequence: []string{"cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error updating data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-client-sequence", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error updating data", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: UpdateCustomClientSequenceRequest{ + Version: 15, + Sequence: []string{"cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "ConfigID") + }, + }, + "Missing Version": { + params: UpdateCustomClientSequenceRequest{ + ConfigID: 43253, + Sequence: []string{"cc9c3f89-e179-4892-89cf-d5e623ba9dc7", "d79285df-e399-43e8-bb0f-c0d980a88e4f", "afa309b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "Version") + }, + }, + "Missing Sequence": { + params: UpdateCustomClientSequenceRequest{ + ConfigID: 43253, + Version: 15, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "Sequence") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(test.responseStatus) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + + if len(test.expectedRequestBody) > 0 { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, test.expectedRequestBody, string(body)) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateCustomClientSequence( + session.ContextWithOptions( + context.Background()), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/botman/mocks.go b/pkg/botman/mocks.go index f17f9239..4181b8e0 100644 --- a/pkg/botman/mocks.go +++ b/pkg/botman/mocks.go @@ -548,3 +548,19 @@ func (p *Mock) GetBotDetectionList(ctx context.Context, params GetBotDetectionLi } return args.Get(0).(*GetBotDetectionListResponse), nil } + +func (p *Mock) GetCustomClientSequence(ctx context.Context, params GetCustomClientSequenceRequest) (*CustomClientSequenceResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*CustomClientSequenceResponse), nil +} + +func (p *Mock) UpdateCustomClientSequence(ctx context.Context, params UpdateCustomClientSequenceRequest) (*CustomClientSequenceResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*CustomClientSequenceResponse), nil +} diff --git a/pkg/botman/validation_response.go b/pkg/botman/validation_response.go new file mode 100644 index 00000000..36bd245b --- /dev/null +++ b/pkg/botman/validation_response.go @@ -0,0 +1,17 @@ +package botman + +type ( + // ValidationResponse is used to represent validation member in the botman api response + ValidationResponse struct { + Errors []ValidationDetail `json:"errors"` + Notices []ValidationDetail `json:"notices"` + Warnings []ValidationDetail `json:"warnings"` + } + + // ValidationDetail is used to represent validation details + ValidationDetail struct { + Title string `json:"title"` + Type string `json:"type"` + Detail string `json:"detail"` + } +) From cd6b24230d4eab620ab47746d6f349eca6a65f23 Mon Sep 17 00:00:00 2001 From: Dawid Dzhafarov Date: Fri, 18 Aug 2023 11:00:51 +0000 Subject: [PATCH 17/17] DXE-2965 Add changelog entry for 7.2.0 release --- CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe058ca..21cc6e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,9 @@ # EDGEGRID GOLANG RELEASE NOTES -## X.X.X (XX xx, 202x) [CloudWrapper changelog] +## 7.2.0 (August 22, 2023) #### FEATURES/ENHANCEMENTS: -* APPSEC - * Added Bot Management API Support - * Custom Client Sequence - read and update - * [IMPORTANT] Added CloudWrapper API support * Capacities * [ListCapacities](https://techdocs.akamai.com/cloud-wrapper/reference/get-capacity-inventory) @@ -26,8 +22,8 @@ * [ListProperties](https://techdocs.akamai.com/cloud-wrapper/reference/get-properties) * [ListOrigins](https://techdocs.akamai.com/cloud-wrapper/reference/get-origins) -* CLIENTLISTS - * [IMPORTANT] Added Client Lists API Support +* [IMPORTANT] Added Client Lists API Support + * ClientLists * [GetClientLists](https://techdocs.akamai.com/client-lists/reference/get-lists) * Support filter by name or type * [GetClientList](https://techdocs.akamai.com/client-lists/reference/get-list) @@ -35,10 +31,15 @@ * [UpdateClientListItems](https://techdocs.akamai.com/client-lists/reference/post-update-items) * [CreateClientList](https://techdocs.akamai.com/client-lists/reference/post-create-list) * [DeleteClientList](https://techdocs.akamai.com/client-lists/reference/delete-list) + * Activations * [GetActivation](https://techdocs.akamai.com/client-lists/reference/get-retrieve-activation-status) * [GetActivationStatus](https://techdocs.akamai.com/client-lists/reference/get-activation-status) * [CreateActivation](https://techdocs.akamai.com/client-lists/reference/post-activate-list) +* APPSEC + * Added Bot Management API Support + * Custom Client Sequence - read and update + ## 7.1.0 (July 25, 2023) ### FEATURES/ENHANCEMENTS: