diff --git a/CHANGELOG.md b/CHANGELOG.md index ca34b324..43232925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # EDGEGRID GOLANG RELEASE NOTES +## 8.3.0 (July 09, 2024) + +#### FEATURES/ENHANCEMENTS: + +* General + * Added `To` utility function in the `ptr` package that helps with creating value pointers + +* BOTMAN + * Added Content Protection APIs + * [CreateContentProtectionRule](https://techdocs.akamai.com/content-protector/reference/post-content-protection-rule) + * [GetContentProtectionRuleList](https://techdocs.akamai.com/content-protector/reference/get-content-protection-rules) + * [GetContentProtectionRule](https://techdocs.akamai.com/content-protector/reference/get-content-protection-rule) + * [UpdateContentProtectionRule](https://techdocs.akamai.com/content-protector/reference/put-content-protection-rule) + * [RemoveContentProtectionRule](https://techdocs.akamai.com/content-protector/reference/delete-content-protection-rule) + * [GetContentProtectionRuleSequence](https://techdocs.akamai.com/content-protector/reference/get-content-protection-rule-sequence) + * [UpdateContentProtectionRuleSequence](https://techdocs.akamai.com/content-protector/reference/put-content-protection-rule-sequence) + * [GetContentProtectionJavaScriptInjectionRuleList](https://techdocs.akamai.com/content-protector/reference/get-content-protection-javascript-injection-rules) + * [GetContentProtectionJavaScriptInjectionRule](https://techdocs.akamai.com/content-protector/reference/get-content-protection-javascript-injection-rule) + * [CreateContentProtectionJavaScriptInjectionRule](https://techdocs.akamai.com/content-protector/reference/post-content-protection-javascript-injection-rule) + * [UpdateContentProtectionJavaScriptInjectionRule](https://techdocs.akamai.com/content-protector/reference/put-content-protection-javascript-injection-rule) + * [RemoveContentProtectionJavaScriptInjectionRule](https://techdocs.akamai.com/content-protector/reference/delete-content-protection-javascript-injection-rule) + +* Added Cloud Access Manager API support + * Access Keys + * [GetAccessKeyStatus](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-create-request) + * [CreateAccessKey](https://techdocs.akamai.com/cloud-access-mgr/reference/post-access-key) + * [GetAccessKey](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key) + * [ListAccessKeys](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-keys) + * [UpdateAccessKey](https://techdocs.akamai.com/cloud-access-mgr/reference/put-access-key) + * [DeleteAccessKey](https://techdocs.akamai.com/cloud-access-mgr/reference/delete-access-key) + * Access Key Versions + * [GetAccessKeyVersionStatus](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-version-create-request) + * [GetAccessKeyVersion](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-version) + * [CreateAccessKeyVersion](https://techdocs.akamai.com/cloud-access-mgr/reference/post-access-key-version) + * [ListAccessKeyVersions](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-versions) + * [DeleteAccessKeyVersion](https://techdocs.akamai.com/cloud-access-mgr/reference/delete-access-key-version) + * Properties using Access Key + * [LookupProperties](https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-version-properties) + * [GetAsyncPropertiesLookupID](https://techdocs.akamai.com/cloud-access-mgr/reference/get-async-version-property-lookup) + * [PerformAsyncPropertiesLookup](https://techdocs.akamai.com/cloud-access-mgr/reference/get-property-lookup) + +* DNS + * Added [GetZonesDNSSecStatus](https://techdocs.akamai.com/edge-dns/reference/post-zones-dns-sec-status) method returning the current DNSSEC status for one or more zones + +#### Deprecations + +* Deprecated the following functions in the `tools` package. Use `ptr.To` instead. + * `BoolPtr` + * `IntPtr` + * `Int64Ptr` + * `Float32Ptr` + * `Float64Ptr` + * `StringPtr` + ## 8.2.0 (May 21, 2024) #### FEATURES/ENHANCEMENTS: diff --git a/go.mod b/go.mod index a4b26851..26495052 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/akamai/AkamaiOPEN-edgegrid-golang/v8 go 1.21 require ( + github.com/akamai/AkamaiOPEN-edgegrid-golang/v7 v7.6.1 github.com/apex/log v1.9.0 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/google/uuid v1.1.1 @@ -19,12 +20,10 @@ require ( github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/objx v0.5.0 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f829348e..c527ac51 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/akamai/AkamaiOPEN-edgegrid-golang/v7 v7.6.1 h1:KrYkNvCKBGPs/upjgJCojZnnmt5XdEPWS4L2zRQm7+o= +github.com/akamai/AkamaiOPEN-edgegrid-golang/v7 v7.6.1/go.mod h1:gajRk0oNRQj4bHUc2SGAvAp/gPestSpuvK4QXU1QtPA= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= @@ -9,7 +11,6 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06 github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -53,7 +54,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= @@ -88,7 +88,6 @@ go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6m golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= @@ -101,7 +100,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/internal/test/test.go b/internal/test/test.go new file mode 100644 index 00000000..7c8210fb --- /dev/null +++ b/internal/test/test.go @@ -0,0 +1,17 @@ +// Package test contains utility code used in tests +package test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// NewTimeFromString returns a time value parsed from a string +// in the RFC3339Nano format +func NewTimeFromString(t *testing.T, s string) time.Time { + parsedTime, err := time.Parse(time.RFC3339Nano, s) + require.NoError(t, err) + return parsedTime +} diff --git a/pkg/botman/botman.go b/pkg/botman/botman.go index 0cde2b6e..c5a43329 100644 --- a/pkg/botman/botman.go +++ b/pkg/botman/botman.go @@ -30,6 +30,9 @@ type ( ChallengeInterceptionRules ClientSideSecurity ConditionalAction + ContentProtectionJavaScriptInjectionRule + ContentProtectionRule + ContentProtectionRuleSequence CustomBotCategory CustomBotCategoryAction CustomBotCategoryItemSequence diff --git a/pkg/botman/content_protection_javascript_injection_rule.go b/pkg/botman/content_protection_javascript_injection_rule.go new file mode 100644 index 00000000..96428d31 --- /dev/null +++ b/pkg/botman/content_protection_javascript_injection_rule.go @@ -0,0 +1,302 @@ +package botman + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // The ContentProtectionJavaScriptInjectionRule interface supports creating, retrieving, modifying and removing content protection JavaScript injection rule + // for a policy. + ContentProtectionJavaScriptInjectionRule interface { + // GetContentProtectionJavaScriptInjectionRuleList https://techdocs.akamai.com/content-protector/reference/get-content-protection-javascript-injection-rules + GetContentProtectionJavaScriptInjectionRuleList(ctx context.Context, params GetContentProtectionJavaScriptInjectionRuleListRequest) (*GetContentProtectionJavaScriptInjectionRuleListResponse, error) + + // GetContentProtectionJavaScriptInjectionRule https://techdocs.akamai.com/content-protector/reference/get-content-protection-javascript-injection-rule + GetContentProtectionJavaScriptInjectionRule(ctx context.Context, params GetContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) + + // CreateContentProtectionJavaScriptInjectionRule https://techdocs.akamai.com/content-protector/reference/post-content-protection-javascript-injection-rule + CreateContentProtectionJavaScriptInjectionRule(ctx context.Context, params CreateContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) + + // UpdateContentProtectionJavaScriptInjectionRule https://techdocs.akamai.com/content-protector/reference/put-content-protection-javascript-injection-rule + UpdateContentProtectionJavaScriptInjectionRule(ctx context.Context, params UpdateContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) + + // RemoveContentProtectionJavaScriptInjectionRule https://techdocs.akamai.com/content-protector/reference/delete-content-protection-javascript-injection-rule + RemoveContentProtectionJavaScriptInjectionRule(ctx context.Context, params RemoveContentProtectionJavaScriptInjectionRuleRequest) error + } + + // GetContentProtectionJavaScriptInjectionRuleListRequest is used to retrieve the content protection JavaScript injection rules for a policy. + GetContentProtectionJavaScriptInjectionRuleListRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionJavaScriptInjectionRuleID string + } + + // GetContentProtectionJavaScriptInjectionRuleListResponse is used to retrieve the content protection JavaScript injection rules for a policy. + GetContentProtectionJavaScriptInjectionRuleListResponse struct { + ContentProtectionJavaScriptInjectionRules []map[string]interface{} `json:"contentProtectionJavaScriptInjectionRules"` + } + + // GetContentProtectionJavaScriptInjectionRuleRequest is used to retrieve a specific content protection JavaScript injection rule. + GetContentProtectionJavaScriptInjectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionJavaScriptInjectionRuleID string + } + + // CreateContentProtectionJavaScriptInjectionRuleRequest is used to create a new content protection JavaScript injection rule for a specific policy. + CreateContentProtectionJavaScriptInjectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + JsonPayload json.RawMessage + } + + // UpdateContentProtectionJavaScriptInjectionRuleRequest is used to update details for a content protection JavaScript injection rule. + UpdateContentProtectionJavaScriptInjectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionJavaScriptInjectionRuleID string + JsonPayload json.RawMessage + } + + // RemoveContentProtectionJavaScriptInjectionRuleRequest is used to remove an existing content protection JavaScript injection rule + RemoveContentProtectionJavaScriptInjectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionJavaScriptInjectionRuleID string + } +) + +// Validate validates a GetContentProtectionJavaScriptInjectionRuleRequest. +func (v GetContentProtectionJavaScriptInjectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionJavaScriptInjectionRuleID": validation.Validate(v.ContentProtectionJavaScriptInjectionRuleID, validation.Required), + }.Filter() +} + +// Validate validates a GetContentProtectionJavaScriptInjectionRuleListRequest. +func (v GetContentProtectionJavaScriptInjectionRuleListRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + }.Filter() +} + +// Validate validates a CreateContentProtectionJavaScriptInjectionRuleRequest. +func (v CreateContentProtectionJavaScriptInjectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "JsonPayload": validation.Validate(v.JsonPayload, validation.Required), + }.Filter() +} + +// Validate validates an UpdateContentProtectionJavaScriptInjectionRuleRequest. +func (v UpdateContentProtectionJavaScriptInjectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionJavaScriptInjectionRuleID": validation.Validate(v.ContentProtectionJavaScriptInjectionRuleID, validation.Required), + "JsonPayload": validation.Validate(v.JsonPayload, validation.Required), + }.Filter() +} + +// Validate validates a RemoveContentProtectionJavaScriptInjectionRuleRequest. +func (v RemoveContentProtectionJavaScriptInjectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionJavaScriptInjectionRuleID": validation.Validate(v.ContentProtectionJavaScriptInjectionRuleID, validation.Required), + }.Filter() +} + +func (b *botman) GetContentProtectionJavaScriptInjectionRule(ctx context.Context, params GetContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("GetContentProtectionJavaScriptInjectionRule") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-javascript-injection-rules/%s", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + params.ContentProtectionJavaScriptInjectionRuleID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetContentProtectionJavaScriptInjectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetContentProtectionJavaScriptInjectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) GetContentProtectionJavaScriptInjectionRuleList(ctx context.Context, params GetContentProtectionJavaScriptInjectionRuleListRequest) (*GetContentProtectionJavaScriptInjectionRuleListResponse, error) { + logger := b.Log(ctx) + logger.Debug("GetContentProtectionJavaScriptInjectionRuleList") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-javascript-injection-rules", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetContentProtectionJavaScriptInjectionRuleList request: %w", err) + } + + var result GetContentProtectionJavaScriptInjectionRuleListResponse + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetContentProtectionJavaScriptInjectionRuleList request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + var filteredResult GetContentProtectionJavaScriptInjectionRuleListResponse + if params.ContentProtectionJavaScriptInjectionRuleID != "" { + for _, val := range result.ContentProtectionJavaScriptInjectionRules { + if val["contentProtectionJavaScriptInjectionRuleId"].(string) == params.ContentProtectionJavaScriptInjectionRuleID { + filteredResult.ContentProtectionJavaScriptInjectionRules = append(filteredResult.ContentProtectionJavaScriptInjectionRules, val) + } + } + } else { + filteredResult = result + } + return &filteredResult, nil +} + +func (b *botman) UpdateContentProtectionJavaScriptInjectionRule(ctx context.Context, params UpdateContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("UpdateContentProtectionJavaScriptInjectionRule") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + putURL := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-javascript-injection-rules/%s", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + params.ContentProtectionJavaScriptInjectionRuleID, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateContentProtectionJavaScriptInjectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result, params.JsonPayload) + if err != nil { + return nil, fmt.Errorf("UpdateContentProtectionJavaScriptInjectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) CreateContentProtectionJavaScriptInjectionRule(ctx context.Context, params CreateContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("CreateContentProtectionJavaScriptInjectionRule") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-javascript-injection-rules", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create CreateContentProtectionJavaScriptInjectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result, params.JsonPayload) + if err != nil { + return nil, fmt.Errorf("CreateContentProtectionJavaScriptInjectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusCreated { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) RemoveContentProtectionJavaScriptInjectionRule(ctx context.Context, params RemoveContentProtectionJavaScriptInjectionRuleRequest) error { + logger := b.Log(ctx) + logger.Debug("RemoveContentProtectionJavaScriptInjectionRule") + + if err := params.Validate(); err != nil { + return fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-javascript-injection-rules/%s", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + params.ContentProtectionJavaScriptInjectionRuleID) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("failed to create RemoveContentProtectionJavaScriptInjectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result) + if err != nil { + return fmt.Errorf("RemoveContentProtectionJavaScriptInjectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusNoContent { + return b.Error(resp) + } + + return nil +} diff --git a/pkg/botman/content_protection_javascript_injection_rule_test.go b/pkg/botman/content_protection_javascript_injection_rule_test.go new file mode 100644 index 00000000..3a6fdc7f --- /dev/null +++ b/pkg/botman/content_protection_javascript_injection_rule_test.go @@ -0,0 +1,647 @@ +package botman + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test Get ContentProtectionJavaScriptInjectionRule List +func TestBotman_GetContentProtectionJavaScriptInjectionRuleList(t *testing.T) { + + tests := map[string]struct { + params GetContentProtectionJavaScriptInjectionRuleListRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetContentProtectionJavaScriptInjectionRuleListResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetContentProtectionJavaScriptInjectionRuleListRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "contentProtectionJavaScriptInjectionRules": [ + {"contentProtectionJavaScriptInjectionRuleId":"fake3eaa-d334-466d-857e-33308ce416be", "testKey":"testValue1"}, + {"contentProtectionJavaScriptInjectionRuleId":"fakead64-7459-4c1d-9bad-672600150127", "testKey":"testValue2"}, + {"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}, + {"contentProtectionJavaScriptInjectionRuleId":"fake4ea3-e3cb-4fc0-b0e0-fa3658aebd7b", "testKey":"testValue4"}, + {"contentProtectionJavaScriptInjectionRuleId":"faked85a-a07f-485a-bbac-24c60658a1b8", "testKey":"testValue5"} + ] +}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules", + expectedResponse: &GetContentProtectionJavaScriptInjectionRuleListResponse{ + ContentProtectionJavaScriptInjectionRules: []map[string]interface{}{ + {"contentProtectionJavaScriptInjectionRuleId": "fake3eaa-d334-466d-857e-33308ce416be", "testKey": "testValue1"}, + {"contentProtectionJavaScriptInjectionRuleId": "fakead64-7459-4c1d-9bad-672600150127", "testKey": "testValue2"}, + {"contentProtectionJavaScriptInjectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + {"contentProtectionJavaScriptInjectionRuleId": "fake4ea3-e3cb-4fc0-b0e0-fa3658aebd7b", "testKey": "testValue4"}, + {"contentProtectionJavaScriptInjectionRuleId": "faked85a-a07f-485a-bbac-24c60658a1b8", "testKey": "testValue5"}, + }, + }, + }, + "200 OK One Record": { + params: GetContentProtectionJavaScriptInjectionRuleListRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "contentProtectionJavaScriptInjectionRules":[ + {"contentProtectionJavaScriptInjectionRuleId":"fake3eaa-d334-466d-857e-33308ce416be", "testKey":"testValue1"}, + {"contentProtectionJavaScriptInjectionRuleId":"fakead64-7459-4c1d-9bad-672600150127", "testKey":"testValue2"}, + {"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}, + {"contentProtectionJavaScriptInjectionRuleId":"fake4ea3-e3cb-4fc0-b0e0-fa3658aebd7b", "testKey":"testValue4"}, + {"contentProtectionJavaScriptInjectionRuleId":"faked85a-a07f-485a-bbac-24c60658a1b8", "testKey":"testValue5"} + ] +}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules", + expectedResponse: &GetContentProtectionJavaScriptInjectionRuleListResponse{ + ContentProtectionJavaScriptInjectionRules: []map[string]interface{}{ + {"contentProtectionJavaScriptInjectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + }, + }, + }, + "500 internal server error": { + params: GetContentProtectionJavaScriptInjectionRuleListRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data", + "status": 500 +}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules", + 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: GetContentProtectionJavaScriptInjectionRuleListRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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: GetContentProtectionJavaScriptInjectionRuleListRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + }, + 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 SecurityPolicyID": { + params: GetContentProtectionJavaScriptInjectionRuleListRequest{ + 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(), "SecurityPolicyID") + }, + }, + } + + 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.GetContentProtectionJavaScriptInjectionRuleList( + 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) + }) + } +} + +// Test Get ContentProtectionJavaScriptInjectionRule +func TestBotman_GetContentProtectionJavaScriptInjectionRule(t *testing.T) { + tests := map[string]struct { + params GetContentProtectionJavaScriptInjectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusOK, + responseBody: `{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + expectedResponse: map[string]interface{}{"contentProtectionJavaScriptInjectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + }, + "500 internal server error": { + params: GetContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + 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: GetContentProtectionJavaScriptInjectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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: GetContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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 SecurityPolicyID": { + params: GetContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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(), "SecurityPolicyID") + }, + }, + "Missing ContentProtectionJavaScriptInjectionRuleID": { + params: GetContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + 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(), "ContentProtectionJavaScriptInjectionRuleID") + }, + }, + } + + 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.GetContentProtectionJavaScriptInjectionRule(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +// Test Create ContentProtectionJavaScriptInjectionRule +func TestBotman_CreateContentProtectionJavaScriptInjectionRule(t *testing.T) { + + tests := map[string]struct { + params CreateContentProtectionJavaScriptInjectionRuleRequest + prop *CreateContentProtectionJavaScriptInjectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "201 Created": { + params: CreateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + responseStatus: http.StatusCreated, + responseBody: `{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`, + expectedResponse: map[string]interface{}{"contentProtectionJavaScriptInjectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules", + }, + "500 internal server error": { + params: CreateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-javascript-injection-rules", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error creating data", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: CreateContentProtectionJavaScriptInjectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + 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: CreateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + 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 JsonPayload": { + params: CreateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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(), "JsonPayload") + }, + }, + } + + 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) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreateContentProtectionJavaScriptInjectionRule(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) + }) + } +} + +// Test Update ContentProtectionJavaScriptInjectionRule +func TestBotman_UpdateContentProtectionJavaScriptInjectionRule(t *testing.T) { + tests := map[string]struct { + params UpdateContentProtectionJavaScriptInjectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 Success": { + params: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + responseStatus: http.StatusOK, + responseBody: `{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`, + expectedResponse: map[string]interface{}{"contentProtectionJavaScriptInjectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-javascript-injection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + "500 internal server error": { + params: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating zone" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-javascript-injection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error creating zone", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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 SecurityPolicyID": { + params: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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(), "SecurityPolicyID") + }, + }, + "Missing JsonPayload": { + params: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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(), "JsonPayload") + }, + }, + "Missing ContentProtectionJavaScriptInjectionRuleID": { + params: UpdateContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"contentProtectionJavaScriptInjectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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(), "ContentProtectionJavaScriptInjectionRuleID") + }, + }, + } + + 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.Path) + 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) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateContentProtectionJavaScriptInjectionRule(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) + }) + } +} + +// Test Remove ContentProtectionJavaScriptInjectionRule +func TestBotman_RemoveContentProtectionJavaScriptInjectionRule(t *testing.T) { + tests := map[string]struct { + params RemoveContentProtectionJavaScriptInjectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 Success": { + params: RemoveContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusNoContent, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-javascript-injection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + "500 internal server error": { + params: RemoveContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error deleting match target" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-javascript-injection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error deleting match target", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: RemoveContentProtectionJavaScriptInjectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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: RemoveContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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 SecurityPolicyID": { + params: RemoveContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionJavaScriptInjectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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(), "SecurityPolicyID") + }, + }, + "Missing ContentProtectionJavaScriptInjectionRuleID": { + params: RemoveContentProtectionJavaScriptInjectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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(), "ContentProtectionJavaScriptInjectionRuleID") + }, + }, + } + + 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.Path) + assert.Equal(t, http.MethodDelete, r.Method) + w.WriteHeader(test.responseStatus) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + err := client.RemoveContentProtectionJavaScriptInjectionRule(session.ContextWithOptions(context.Background()), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/pkg/botman/content_protection_rule.go b/pkg/botman/content_protection_rule.go new file mode 100644 index 00000000..21a177d0 --- /dev/null +++ b/pkg/botman/content_protection_rule.go @@ -0,0 +1,302 @@ +package botman + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // The ContentProtectionRule interface supports creating, retrieving, modifying and removing content protection rule + // for a policy. + ContentProtectionRule interface { + // GetContentProtectionRuleList https://techdocs.akamai.com/content-protector/reference/get-content-protection-rules + GetContentProtectionRuleList(ctx context.Context, params GetContentProtectionRuleListRequest) (*GetContentProtectionRuleListResponse, error) + + // GetContentProtectionRule https://techdocs.akamai.com/content-protector/reference/get-content-protection-rule + GetContentProtectionRule(ctx context.Context, params GetContentProtectionRuleRequest) (map[string]interface{}, error) + + // CreateContentProtectionRule https://techdocs.akamai.com/content-protector/reference/post-content-protection-rule + CreateContentProtectionRule(ctx context.Context, params CreateContentProtectionRuleRequest) (map[string]interface{}, error) + + // UpdateContentProtectionRule https://techdocs.akamai.com/content-protector/reference/put-content-protection-rule + UpdateContentProtectionRule(ctx context.Context, params UpdateContentProtectionRuleRequest) (map[string]interface{}, error) + + // RemoveContentProtectionRule https://techdocs.akamai.com/content-protector/reference/delete-content-protection-rule + RemoveContentProtectionRule(ctx context.Context, params RemoveContentProtectionRuleRequest) error + } + + // GetContentProtectionRuleListRequest is used to retrieve the content protection rules for a policy. + GetContentProtectionRuleListRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionRuleID string + } + + // GetContentProtectionRuleListResponse is used to retrieve the content protection rules for a policy. + GetContentProtectionRuleListResponse struct { + ContentProtectionRules []map[string]interface{} `json:"contentProtectionRules"` + } + + // GetContentProtectionRuleRequest is used to retrieve a specific content protection rule. + GetContentProtectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionRuleID string + } + + // CreateContentProtectionRuleRequest is used to create a new content protection rule for a specific policy. + CreateContentProtectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + JsonPayload json.RawMessage + } + + // UpdateContentProtectionRuleRequest is used to update details for a content protection rule. + UpdateContentProtectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionRuleID string + JsonPayload json.RawMessage + } + + // RemoveContentProtectionRuleRequest is used to remove an existing content protection rule + RemoveContentProtectionRuleRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionRuleID string + } +) + +// Validate validates a GetContentProtectionRuleRequest. +func (v GetContentProtectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionRuleID": validation.Validate(v.ContentProtectionRuleID, validation.Required), + }.Filter() +} + +// Validate validates a GetContentProtectionRuleListRequest. +func (v GetContentProtectionRuleListRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + }.Filter() +} + +// Validate validates a CreateContentProtectionRuleRequest. +func (v CreateContentProtectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "JsonPayload": validation.Validate(v.JsonPayload, validation.Required), + }.Filter() +} + +// Validate validates an UpdateContentProtectionRuleRequest. +func (v UpdateContentProtectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionRuleID": validation.Validate(v.ContentProtectionRuleID, validation.Required), + "JsonPayload": validation.Validate(v.JsonPayload, validation.Required), + }.Filter() +} + +// Validate validates a RemoveContentProtectionRuleRequest. +func (v RemoveContentProtectionRuleRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionRuleID": validation.Validate(v.ContentProtectionRuleID, validation.Required), + }.Filter() +} + +func (b *botman) GetContentProtectionRule(ctx context.Context, params GetContentProtectionRuleRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("GetContentProtectionRule") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rules/%s", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + params.ContentProtectionRuleID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetContentProtectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetContentProtectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) GetContentProtectionRuleList(ctx context.Context, params GetContentProtectionRuleListRequest) (*GetContentProtectionRuleListResponse, error) { + logger := b.Log(ctx) + logger.Debug("GetContentProtectionRuleList") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rules", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetContentProtectionRuleList request: %w", err) + } + + var result GetContentProtectionRuleListResponse + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetContentProtectionRuleList request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + var filteredResult GetContentProtectionRuleListResponse + if params.ContentProtectionRuleID != "" { + for _, val := range result.ContentProtectionRules { + if val["contentProtectionRuleId"].(string) == params.ContentProtectionRuleID { + filteredResult.ContentProtectionRules = append(filteredResult.ContentProtectionRules, val) + } + } + } else { + filteredResult = result + } + return &filteredResult, nil +} + +func (b *botman) UpdateContentProtectionRule(ctx context.Context, params UpdateContentProtectionRuleRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("UpdateContentProtectionRule") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + putURL := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rules/%s", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + params.ContentProtectionRuleID, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateContentProtectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result, params.JsonPayload) + if err != nil { + return nil, fmt.Errorf("UpdateContentProtectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) CreateContentProtectionRule(ctx context.Context, params CreateContentProtectionRuleRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("CreateContentProtectionRule") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rules", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create CreateContentProtectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result, params.JsonPayload) + if err != nil { + return nil, fmt.Errorf("CreateContentProtectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusCreated { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) RemoveContentProtectionRule(ctx context.Context, params RemoveContentProtectionRuleRequest) error { + logger := b.Log(ctx) + logger.Debug("RemoveContentProtectionRule") + + if err := params.Validate(); err != nil { + return fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf("/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rules/%s", + params.ConfigID, + params.Version, + params.SecurityPolicyID, + params.ContentProtectionRuleID) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("failed to create RemoveContentProtectionRule request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result) + if err != nil { + return fmt.Errorf("RemoveContentProtectionRule request failed: %w", err) + } + + if resp.StatusCode != http.StatusNoContent { + return b.Error(resp) + } + + return nil +} diff --git a/pkg/botman/content_protection_rule_sequence.go b/pkg/botman/content_protection_rule_sequence.go new file mode 100644 index 00000000..47f09811 --- /dev/null +++ b/pkg/botman/content_protection_rule_sequence.go @@ -0,0 +1,129 @@ +package botman + +import ( + "context" + "fmt" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // The ContentProtectionRuleSequence interface supports retrieving and updating content protection rule sequence + ContentProtectionRuleSequence interface { + // GetContentProtectionRuleSequence https://techdocs.akamai.com/content-protector/reference/get-content-protection-rule-sequence + GetContentProtectionRuleSequence(ctx context.Context, params GetContentProtectionRuleSequenceRequest) (*GetContentProtectionRuleSequenceResponse, error) + + // UpdateContentProtectionRuleSequence https://techdocs.akamai.com/content-protector/reference/put-content-protection-rule-sequence + UpdateContentProtectionRuleSequence(ctx context.Context, params UpdateContentProtectionRuleSequenceRequest) (*UpdateContentProtectionRuleSequenceResponse, error) + } + + // GetContentProtectionRuleSequenceRequest is used to retrieve content protection rule sequence + GetContentProtectionRuleSequenceRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + } + + // GetContentProtectionRuleSequenceResponse contains the sequence of content protection rule + GetContentProtectionRuleSequenceResponse ContentProtectionRuleUUIDSequence + + // UpdateContentProtectionRuleSequenceResponse contains the sequence of content protection rule + UpdateContentProtectionRuleSequenceResponse ContentProtectionRuleUUIDSequence + + // UpdateContentProtectionRuleSequenceRequest is used to update content protection rule sequence + UpdateContentProtectionRuleSequenceRequest struct { + ConfigID int64 + Version int64 + SecurityPolicyID string + ContentProtectionRuleSequence ContentProtectionRuleUUIDSequence + } + + // ContentProtectionRuleUUIDSequence is a sequence of UUIDs. + ContentProtectionRuleUUIDSequence struct { + ContentProtectionRuleSequence []string `json:"contentProtectionRuleSequence"` + } +) + +// Validate validates a GetContentProtectionRuleSequenceRequest. +func (v GetContentProtectionRuleSequenceRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + }.Filter() +} + +// Validate validates an UpdateContentProtectionRuleSequenceRequest. +func (v UpdateContentProtectionRuleSequenceRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "SecurityPolicyID": validation.Validate(v.SecurityPolicyID, validation.Required), + "ContentProtectionRuleSequence": validation.Validate(v.ContentProtectionRuleSequence.ContentProtectionRuleSequence, validation.Required), + }.Filter() +} + +func (b *botman) GetContentProtectionRuleSequence(ctx context.Context, params GetContentProtectionRuleSequenceRequest) (*GetContentProtectionRuleSequenceResponse, error) { + logger := b.Log(ctx) + logger.Debug("GetContentProtectionRuleSequence") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rule-sequence", + params.ConfigID, + params.Version, + params.SecurityPolicyID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetContentProtectionRuleSequence request: %w", err) + } + + var result GetContentProtectionRuleSequenceResponse + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetContentProtectionRuleSequence request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return &result, nil +} + +func (b *botman) UpdateContentProtectionRuleSequence(ctx context.Context, params UpdateContentProtectionRuleSequenceRequest) (*UpdateContentProtectionRuleSequenceResponse, error) { + logger := b.Log(ctx) + logger.Debug("UpdateContentProtectionRuleSequence") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + putURL := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/security-policies/%s/content-protection-rule-sequence", + params.ConfigID, + params.Version, + params.SecurityPolicyID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateContentProtectionRuleSequence request: %w", err) + } + + var result UpdateContentProtectionRuleSequenceResponse + resp, err := b.Exec(req, &result, params.ContentProtectionRuleSequence) + if err != nil { + return nil, fmt.Errorf("UpdateContentProtectionRuleSequence request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return &result, nil +} diff --git a/pkg/botman/content_protection_rule_sequence_test.go b/pkg/botman/content_protection_rule_sequence_test.go new file mode 100644 index 00000000..92f7c114 --- /dev/null +++ b/pkg/botman/content_protection_rule_sequence_test.go @@ -0,0 +1,240 @@ +package botman + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test Get ContentProtectionRuleSequence +func TestBotman_GetContentProtectionRuleSequence(t *testing.T) { + tests := map[string]struct { + params GetContentProtectionRuleSequenceRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetContentProtectionRuleSequenceResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + responseStatus: http.StatusOK, + responseBody: `{"contentProtectionRuleSequence":["fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rule-sequence", + expectedResponse: &GetContentProtectionRuleSequenceResponse{ + ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + }, + "500 internal server error": { + params: GetContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rule-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: GetContentProtectionRuleSequenceRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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: GetContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + }, + 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 SecurityPolicyID": { + params: GetContentProtectionRuleSequenceRequest{ + 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(), "SecurityPolicyID") + }, + }, + } + + 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.GetContentProtectionRuleSequence(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 ContentProtectionRuleSequence. +func TestBotman_UpdateContentProtectionRuleSequence(t *testing.T) { + tests := map[string]struct { + params UpdateContentProtectionRuleSequenceRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *UpdateContentProtectionRuleSequenceResponse + withError func(*testing.T, error) + }{ + "200 Success": { + params: UpdateContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleSequence: ContentProtectionRuleUUIDSequence{ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"}}, + }, + responseStatus: http.StatusOK, + responseBody: `{"contentProtectionRuleSequence":["fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + expectedResponse: &UpdateContentProtectionRuleSequenceResponse{ + ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rule-sequence", + }, + "500 internal server error": { + params: UpdateContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleSequence: ContentProtectionRuleUUIDSequence{ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-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/security-policies/AAAA_81230/content-protection-rule-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: UpdateContentProtectionRuleSequenceRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleSequence: ContentProtectionRuleUUIDSequence{ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-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: UpdateContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleSequence: ContentProtectionRuleUUIDSequence{ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-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 SecurityPolicyID": { + params: UpdateContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionRuleSequence: ContentProtectionRuleUUIDSequence{ContentProtectionRuleSequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-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(), "SecurityPolicyID") + }, + }, + "Missing ContentProtectionRuleSequence": { + params: UpdateContentProtectionRuleSequenceRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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(), "ContentProtectionRuleSequence") + }, + }, + } + + 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) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateContentProtectionRuleSequence( + 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/content_protection_rule_test.go b/pkg/botman/content_protection_rule_test.go new file mode 100644 index 00000000..c7ecaf97 --- /dev/null +++ b/pkg/botman/content_protection_rule_test.go @@ -0,0 +1,647 @@ +package botman + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test Get ContentProtectionRule List +func TestBotman_GetContentProtectionRuleList(t *testing.T) { + + tests := map[string]struct { + params GetContentProtectionRuleListRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetContentProtectionRuleListResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetContentProtectionRuleListRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "contentProtectionRules": [ + {"contentProtectionRuleId":"fake3eaa-d334-466d-857e-33308ce416be", "testKey":"testValue1"}, + {"contentProtectionRuleId":"fakead64-7459-4c1d-9bad-672600150127", "testKey":"testValue2"}, + {"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}, + {"contentProtectionRuleId":"fake4ea3-e3cb-4fc0-b0e0-fa3658aebd7b", "testKey":"testValue4"}, + {"contentProtectionRuleId":"faked85a-a07f-485a-bbac-24c60658a1b8", "testKey":"testValue5"} + ] +}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules", + expectedResponse: &GetContentProtectionRuleListResponse{ + ContentProtectionRules: []map[string]interface{}{ + {"contentProtectionRuleId": "fake3eaa-d334-466d-857e-33308ce416be", "testKey": "testValue1"}, + {"contentProtectionRuleId": "fakead64-7459-4c1d-9bad-672600150127", "testKey": "testValue2"}, + {"contentProtectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + {"contentProtectionRuleId": "fake4ea3-e3cb-4fc0-b0e0-fa3658aebd7b", "testKey": "testValue4"}, + {"contentProtectionRuleId": "faked85a-a07f-485a-bbac-24c60658a1b8", "testKey": "testValue5"}, + }, + }, + }, + "200 OK One Record": { + params: GetContentProtectionRuleListRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "contentProtectionRules":[ + {"contentProtectionRuleId":"fake3eaa-d334-466d-857e-33308ce416be", "testKey":"testValue1"}, + {"contentProtectionRuleId":"fakead64-7459-4c1d-9bad-672600150127", "testKey":"testValue2"}, + {"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}, + {"contentProtectionRuleId":"fake4ea3-e3cb-4fc0-b0e0-fa3658aebd7b", "testKey":"testValue4"}, + {"contentProtectionRuleId":"faked85a-a07f-485a-bbac-24c60658a1b8", "testKey":"testValue5"} + ] +}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules", + expectedResponse: &GetContentProtectionRuleListResponse{ + ContentProtectionRules: []map[string]interface{}{ + {"contentProtectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + }, + }, + }, + "500 internal server error": { + params: GetContentProtectionRuleListRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data", + "status": 500 +}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules", + 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: GetContentProtectionRuleListRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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: GetContentProtectionRuleListRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + }, + 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 SecurityPolicyID": { + params: GetContentProtectionRuleListRequest{ + 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(), "SecurityPolicyID") + }, + }, + } + + 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.GetContentProtectionRuleList( + 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) + }) + } +} + +// Test Get ContentProtectionRule +func TestBotman_GetContentProtectionRule(t *testing.T) { + tests := map[string]struct { + params GetContentProtectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusOK, + responseBody: `{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + expectedResponse: map[string]interface{}{"contentProtectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + }, + "500 internal server error": { + params: GetContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + 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: GetContentProtectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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: GetContentProtectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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 SecurityPolicyID": { + params: GetContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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(), "SecurityPolicyID") + }, + }, + "Missing ContentProtectionRuleID": { + params: GetContentProtectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + 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(), "ContentProtectionRuleID") + }, + }, + } + + 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.GetContentProtectionRule(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +// Test Create ContentProtectionRule +func TestBotman_CreateContentProtectionRule(t *testing.T) { + + tests := map[string]struct { + params CreateContentProtectionRuleRequest + prop *CreateContentProtectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "201 Created": { + params: CreateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + responseStatus: http.StatusCreated, + responseBody: `{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`, + expectedResponse: map[string]interface{}{"contentProtectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules", + }, + "500 internal server error": { + params: CreateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/content-protection-rules", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error creating data", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: CreateContentProtectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + 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: CreateContentProtectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + 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 JsonPayload": { + params: CreateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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(), "JsonPayload") + }, + }, + } + + 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) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreateContentProtectionRule(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) + }) + } +} + +// Test Update ContentProtectionRule +func TestBotman_UpdateContentProtectionRule(t *testing.T) { + tests := map[string]struct { + params UpdateContentProtectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 Success": { + params: UpdateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + responseStatus: http.StatusOK, + responseBody: `{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`, + expectedResponse: map[string]interface{}{"contentProtectionRuleId": "fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey": "testValue3"}, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + "500 internal server error": { + params: UpdateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating zone" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error creating zone", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: UpdateContentProtectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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: UpdateContentProtectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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 SecurityPolicyID": { + params: UpdateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + JsonPayload: json.RawMessage(`{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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(), "SecurityPolicyID") + }, + }, + "Missing JsonPayload": { + params: UpdateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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(), "JsonPayload") + }, + }, + "Missing ContentProtectionRuleID": { + params: UpdateContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + JsonPayload: json.RawMessage(`{"contentProtectionRuleId":"fake3f89-e179-4892-89cf-d5e623ba9dc7", "testKey":"testValue3"}`), + }, + 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(), "ContentProtectionRuleID") + }, + }, + } + + 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.Path) + 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) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateContentProtectionRule(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) + }) + } +} + +// Test Remove ContentProtectionRule +func TestBotman_RemoveContentProtectionRule(t *testing.T) { + tests := map[string]struct { + params RemoveContentProtectionRuleRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 Success": { + params: RemoveContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusNoContent, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + "500 internal server error": { + params: RemoveContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 10, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error deleting match target" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/10/security-policies/AAAA_81230/content-protection-rules/fake3f89-e179-4892-89cf-d5e623ba9dc7", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error deleting match target", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: RemoveContentProtectionRuleRequest{ + Version: 15, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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: RemoveContentProtectionRuleRequest{ + ConfigID: 43253, + SecurityPolicyID: "AAAA_81230", + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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 SecurityPolicyID": { + params: RemoveContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + ContentProtectionRuleID: "fake3f89-e179-4892-89cf-d5e623ba9dc7", + }, + 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(), "SecurityPolicyID") + }, + }, + "Missing ContentProtectionRuleID": { + params: RemoveContentProtectionRuleRequest{ + ConfigID: 43253, + Version: 15, + SecurityPolicyID: "AAAA_81230", + }, + 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(), "ContentProtectionRuleID") + }, + }, + } + + 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.Path) + assert.Equal(t, http.MethodDelete, r.Method) + w.WriteHeader(test.responseStatus) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + err := client.RemoveContentProtectionRule(session.ContextWithOptions(context.Background()), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/pkg/botman/mocks.go b/pkg/botman/mocks.go index b9e973b3..5b4dbcd7 100644 --- a/pkg/botman/mocks.go +++ b/pkg/botman/mocks.go @@ -596,3 +596,93 @@ func (p *Mock) UpdateCustomBotCategoryItemSequence(ctx context.Context, params U } return args.Get(0).(*UpdateCustomBotCategoryItemSequenceResponse), nil } + +func (p *Mock) GetContentProtectionRuleList(ctx context.Context, params GetContentProtectionRuleListRequest) (*GetContentProtectionRuleListResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*GetContentProtectionRuleListResponse), nil +} + +func (p *Mock) GetContentProtectionRule(ctx context.Context, params GetContentProtectionRuleRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) CreateContentProtectionRule(ctx context.Context, params CreateContentProtectionRuleRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) UpdateContentProtectionRule(ctx context.Context, params UpdateContentProtectionRuleRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) RemoveContentProtectionRule(ctx context.Context, params RemoveContentProtectionRuleRequest) error { + args := p.Called(ctx, params) + return args.Error(0) +} + +func (p *Mock) GetContentProtectionRuleSequence(ctx context.Context, params GetContentProtectionRuleSequenceRequest) (*GetContentProtectionRuleSequenceResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*GetContentProtectionRuleSequenceResponse), nil +} + +func (p *Mock) UpdateContentProtectionRuleSequence(ctx context.Context, params UpdateContentProtectionRuleSequenceRequest) (*UpdateContentProtectionRuleSequenceResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*UpdateContentProtectionRuleSequenceResponse), nil +} + +func (p *Mock) GetContentProtectionJavaScriptInjectionRuleList(ctx context.Context, params GetContentProtectionJavaScriptInjectionRuleListRequest) (*GetContentProtectionJavaScriptInjectionRuleListResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*GetContentProtectionJavaScriptInjectionRuleListResponse), nil +} + +func (p *Mock) GetContentProtectionJavaScriptInjectionRule(ctx context.Context, params GetContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) CreateContentProtectionJavaScriptInjectionRule(ctx context.Context, params CreateContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) UpdateContentProtectionJavaScriptInjectionRule(ctx context.Context, params UpdateContentProtectionJavaScriptInjectionRuleRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) RemoveContentProtectionJavaScriptInjectionRule(ctx context.Context, params RemoveContentProtectionJavaScriptInjectionRuleRequest) error { + args := p.Called(ctx, params) + return args.Error(0) +} diff --git a/pkg/cloudaccess/access_key.go b/pkg/cloudaccess/access_key.go new file mode 100644 index 00000000..73dd6145 --- /dev/null +++ b/pkg/cloudaccess/access_key.go @@ -0,0 +1,342 @@ +package cloudaccess + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // GetAccessKeyStatusResponse contains response from GetAccessKeyStatus + GetAccessKeyStatusResponse struct { + AccessKey *KeyLink `json:"accessKey"` + AccessKeyVersion *KeyVersion `json:"accessKeyVersion"` + ProcessingStatus ProcessingType `json:"processingStatus"` + Request *RequestInformation `json:"request"` + RequestDate time.Time `json:"requestDate"` + RequestID int64 `json:"requestId"` + RequestedBy string `json:"requestedBy"` + } + + // GetAccessKeyStatusRequest holds parameters for GetAccessKeyStatus + GetAccessKeyStatusRequest struct { + RequestID int64 + } + + // CreateAccessKeyRequest holds request body for CreateAccessKey + CreateAccessKeyRequest struct { + AccessKeyName string `json:"accessKeyName"` + AuthenticationMethod string `json:"authenticationMethod"` + ContractID string `json:"contractId"` + Credentials Credentials `json:"credentials"` + GroupID int64 `json:"groupId"` + NetworkConfiguration SecureNetwork `json:"networkConfiguration"` + } + + // Credentials holds information used to sign API requests + Credentials struct { + CloudAccessKeyID string `json:"cloudAccessKeyId"` + CloudSecretAccessKey string `json:"cloudSecretAccessKey"` + } + + // CreateAccessKeyResponse contains response from CreateAccessKey + CreateAccessKeyResponse struct { + RequestID int64 `json:"requestId,omitempty"` + RetryAfter int64 `json:"retryAfter,omitempty"` + Location string + } + + // AccessKeyRequest holds parameters for GetAccessKey, UpdateAccessKey, DeleteAccessKey + AccessKeyRequest struct { + AccessKeyUID int64 + } + + // AccessKeyResponse contains response from ListAccessKeys + AccessKeyResponse struct { + AccessKeyUID int64 `json:"accessKeyUid"` + AccessKeyName string `json:"accessKeyName"` + AuthenticationMethod string `json:"authenticationMethod"` + NetworkConfiguration *SecureNetwork `json:"networkConfiguration"` + LatestVersion int64 `json:"latestVersion"` + Groups []Group `json:"groups"` + CreatedBy string `json:"createdBy"` + CreatedTime time.Time `json:"createdTime"` + } + + // GetAccessKeyResponse contains response from GetAccessKey + GetAccessKeyResponse AccessKeyResponse + + // Group is an object to which the access key is assigned + Group struct { + ContractIDs []string `json:"contractIds"` + GroupID int64 `json:"groupId"` + GroupName *string `json:"groupName"` + } + + // ListAccessKeysRequest holds parameters for GetAccessKeys + ListAccessKeysRequest struct { + VersionGUID string + } + + // ListAccessKeysResponse contains array of GetAccessKeyResponse + ListAccessKeysResponse struct { + AccessKeys []AccessKeyResponse `json:"accessKeys"` + } + + // UpdateAccessKeyRequest holds request body for UpdateAccessKey + UpdateAccessKeyRequest struct { + AccessKeyName string `json:"accessKeyName,omitempty"` + } + + // UpdateAccessKeyResponse contains response from UpdateAccessKey + UpdateAccessKeyResponse struct { + AccessKeyUID int64 `json:"accessKeyUid"` + AccessKeyName string `json:"accessKeyName"` + } +) + +// Validate validates GetAccessKeyStatusRequest +func (r GetAccessKeyStatusRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "RequestID": validation.Validate(r.RequestID, validation.Required), + }) +} + +// Validate validates CreateAccessKeyRequest +func (r CreateAccessKeyRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyName": validation.Validate(r.AccessKeyName, validation.Required), + "AuthenticationMethod": validation.Validate(r.AuthenticationMethod, validation.Required), + "ContractID": validation.Validate(r.ContractID, validation.Required), + "CloudAccessKeyID": validation.Validate(r.Credentials.CloudAccessKeyID, validation.Required), + "CloudSecretAccessKey": validation.Validate(r.Credentials.CloudSecretAccessKey, validation.Required), + "GroupID": validation.Validate(r.GroupID, validation.Required), + "SecurityNetwork": validation.Validate(r.NetworkConfiguration.SecurityNetwork, validation.Required), + }) +} + +// Validate validates AccessKeyRequest +func (r AccessKeyRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + }) +} + +// Validate validates UpdateAccessKeyRequest +func (r UpdateAccessKeyRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyName": validation.Validate(r.AccessKeyName, validation.Required, validation.Length(1, 50)), + }) +} + +var ( + // ErrGetAccessKeyStatus is returned when GetAccessKeyStatus fails + ErrGetAccessKeyStatus = errors.New("get the status of an access key") + + // ErrCreateAccessKey is returned when CreateAccessKey fails + ErrCreateAccessKey = errors.New("create an access key") + + // ErrGetAccessKey is returned when GetAccessKey fails + ErrGetAccessKey = errors.New("get an access key") + + // ErrUpdateAccessKey is returned when UpdateAccessKey fails + ErrUpdateAccessKey = errors.New("update an access key") + + // ErrDeleteAccessKey is returned when DeleteAccessKey fails + ErrDeleteAccessKey = errors.New("delete an access key") +) + +func (c *cloudaccess) GetAccessKeyStatus(ctx context.Context, params GetAccessKeyStatusRequest) (*GetAccessKeyStatusResponse, error) { + logger := c.Log(ctx) + logger.Debug("GetAccessKeyStatus") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetAccessKeyStatus, ErrStructValidation, err) + } + + getURL := fmt.Sprintf("/cam/v1/access-key-create-requests/%d", params.RequestID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetAccessKeyStatus request: %w", err) + } + + var result GetAccessKeyStatusResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetAccessKeyStatus request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, c.Error(resp) + } + + return &result, nil +} + +func (c *cloudaccess) CreateAccessKey(ctx context.Context, accessKey CreateAccessKeyRequest) (*CreateAccessKeyResponse, error) { + logger := c.Log(ctx) + logger.Debug("CreateAccessKey") + + if err := accessKey.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrCreateAccessKey, ErrStructValidation, err) + } + + uri, err := url.Parse("/cam/v1/access-keys") + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create CreateAccessKey request: %w", err) + } + + var result CreateAccessKeyResponse + resp, err := c.Exec(req, &result, accessKey) + if err != nil { + return nil, fmt.Errorf("CreateAccessKey request failed: %w", err) + } + + if resp.StatusCode != http.StatusAccepted { + return nil, c.Error(resp) + } + + result.Location = resp.Header.Get("Location") + + return &result, nil +} + +func (c *cloudaccess) GetAccessKey(ctx context.Context, params AccessKeyRequest) (*GetAccessKeyResponse, error) { + logger := c.Log(ctx) + logger.Debug("GetAccessKey") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetAccessKey, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cam/v1/access-keys/%d", params.AccessKeyUID)) + if err != nil { + return nil, fmt.Errorf("failed to parse url: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetAccessKey request: %w", err) + } + + var result GetAccessKeyResponse + resp, err := c.Exec(req, &result) + if err != nil { + + return nil, fmt.Errorf("GetAccessKey request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, c.Error(resp) + } + + return &result, nil +} + +func (c *cloudaccess) ListAccessKeys(ctx context.Context, params ListAccessKeysRequest) (*ListAccessKeysResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListAccessKeys") + + uri, err := url.Parse("/cam/v1/access-keys") + if err != nil { + return nil, fmt.Errorf("failed to parse url: %w", err) + } + q := uri.Query() + if params.VersionGUID != "" { + q.Add("versionGuid", params.VersionGUID) + } + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create ListAccessKeys request: %w", err) + } + + var result ListAccessKeysResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("ListAccessKeys request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, c.Error(resp) + } + + return &result, nil +} + +func (c *cloudaccess) DeleteAccessKey(ctx context.Context, params AccessKeyRequest) error { + logger := c.Log(ctx) + logger.Debug("DeleteAccessKey") + + if err := params.Validate(); err != nil { + return fmt.Errorf("%s: %w: %s", ErrDeleteAccessKey, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cam/v1/access-keys/%d", params.AccessKeyUID)) + if err != nil { + return fmt.Errorf("failed to parse url: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri.String(), nil) + if err != nil { + return fmt.Errorf("failed to create DeleteAccessKey request: %s", err.Error()) + } + + resp, err := c.Exec(req, nil) + if err != nil { + return fmt.Errorf("DeleteAccessKey request failed: %s", err.Error()) + } + + if resp.StatusCode != http.StatusNoContent { + return c.Error(resp) + } + + return nil +} + +func (c *cloudaccess) UpdateAccessKey(ctx context.Context, accessKey UpdateAccessKeyRequest, params AccessKeyRequest) (*UpdateAccessKeyResponse, error) { + logger := c.Log(ctx) + logger.Debug("UpdateAccessKey") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrUpdateAccessKey, ErrStructValidation, err) + } + if err := accessKey.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrUpdateAccessKey, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cam/v1/access-keys/%d", params.AccessKeyUID)) + if err != nil { + return nil, fmt.Errorf("failed to parse url: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateAccessKey request: %w", err) + } + + var result UpdateAccessKeyResponse + resp, err := c.Exec(req, &result, accessKey) + if err != nil { + return nil, fmt.Errorf("UpdateAccessKey request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, c.Error(resp) + } + + return &result, nil + +} diff --git a/pkg/cloudaccess/access_key_test.go b/pkg/cloudaccess/access_key_test.go new file mode 100644 index 00000000..9ea15167 --- /dev/null +++ b/pkg/cloudaccess/access_key_test.go @@ -0,0 +1,568 @@ +package cloudaccess + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAccessKeyStatus(t *testing.T) { + + var result GetAccessKeyStatusResponse + var resultMinimal GetAccessKeyStatusResponse + + respData, err := loadTestData("AccessKeyStatus/GetAccessKeyStatus.resp.json") + if err != nil { + t.Fatal(err) + } + if err := json.NewDecoder(bytes.NewBuffer(respData)).Decode(&result); err != nil { + t.Fatal(err) + } + + respDataMinimal, err := loadTestData("AccessKeyStatus/GetAccessKeyStatus.resp.json") + if err != nil { + t.Fatal(err) + } + if err := json.NewDecoder(bytes.NewBuffer(respDataMinimal)).Decode(&resultMinimal); err != nil { + t.Fatal(err) + } + + tests := map[string]struct { + params GetAccessKeyStatusRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetAccessKeyStatusResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetAccessKeyStatusRequest{ + RequestID: 1, + }, + responseStatus: http.StatusOK, + responseBody: string(respData), + expectedPath: "/cam/v1/access-key-create-requests/1", + expectedResponse: &result, + }, + "200 OK - minimal": { + params: GetAccessKeyStatusRequest{ + RequestID: 1, + }, + responseStatus: http.StatusOK, + responseBody: string(respDataMinimal), + expectedPath: "/cam/v1/access-key-create-requests/1", + expectedResponse: &resultMinimal, + }, + "missing required params - validation error": { + params: GetAccessKeyStatusRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get the status of an access key: struct validation: RequestID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: GetAccessKeyStatusRequest{ + RequestID: 123, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + expectedPath: "/cam/v1/access-key-create-requests/123", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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.GetAccessKeyStatus(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestCreateAccessKey(t *testing.T) { + + var req CreateAccessKeyRequest + reqData, err := loadTestData("AccessKey/CreateAccessKey.req.json") + if err != nil { + t.Fatal(err) + } + + if err := json.NewDecoder(bytes.NewBuffer(reqData)).Decode(&req); err != nil { + t.Fatal(err) + } + + tests := map[string]struct { + accessKey CreateAccessKeyRequest + expectedPath string + responseStatus int + responseBody string + expectedResponse *CreateAccessKeyResponse + responseHeaders map[string]string + withError func(*testing.T, error) + }{ + "202 Accepted": { + accessKey: req, + expectedPath: "/cam/v1/access-keys", + responseStatus: http.StatusAccepted, + responseBody: ` + { + "requestId": 195, + "retryAfter": 4 + + }`, + expectedResponse: &CreateAccessKeyResponse{ + RequestID: 195, + RetryAfter: 4, + Location: "https://abc.com", + }, + responseHeaders: map[string]string{ + "Location": "https://abc.com", + }, + }, + "missing required request body - validation error": { + accessKey: CreateAccessKeyRequest{ + Credentials: Credentials{}, + NetworkConfiguration: SecureNetwork{}, + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create an access key: struct validation: AccessKeyName: cannot be blank\nAuthenticationMethod: cannot be blank\nCloudAccessKeyID: cannot be blank\nCloudSecretAccessKey: cannot be blank\nContractID: cannot be blank\nGroupID: cannot be blank\nSecurityNetwork: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + accessKey: req, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + expectedPath: "/cam/v1/access-keys", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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) + if len(test.responseHeaders) > 0 { + for header, value := range test.responseHeaders { + w.Header().Set(header, value) + } + } + + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreateAccessKey(context.Background(), test.accessKey) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestGetAccessKey(t *testing.T) { + + var result GetAccessKeyResponse + + respData, err := loadTestData("AccessKey/GetAccessKey.resp.json") + if err != nil { + t.Fatal(err) + } + + if err := json.NewDecoder(bytes.NewBuffer(respData)).Decode(&result); err != nil { + t.Fatal(err) + } + + tests := map[string]struct { + params AccessKeyRequest + expectedPath string + responseStatus int + responseBody string + expectedResponse *GetAccessKeyResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + expectedPath: "/cam/v1/access-keys/1", + responseStatus: http.StatusOK, + responseBody: string(respData), + expectedResponse: &result, + }, + "missing required params - validation error": { + params: AccessKeyRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get an access key: struct validation: AccessKeyUID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + expectedPath: "/cam/v1/access-keys/1", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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.GetAccessKey(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestListAccessKey(t *testing.T) { + + var result ListAccessKeysResponse + + respData, err := loadTestData("AccessKey/ListAccessKey.resp.json") + if err != nil { + t.Fatal(err) + } + + if err := json.NewDecoder(bytes.NewBuffer(respData)).Decode(&result); err != nil { + t.Fatal(err) + } + + tests := map[string]struct { + params ListAccessKeysRequest + expectedPath string + responseStatus int + responseBody string + expectedResponse *ListAccessKeysResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: ListAccessKeysRequest{ + VersionGUID: "1", + }, + expectedPath: "/cam/v1/access-keys?versionGuid=1", + responseStatus: http.StatusOK, + responseBody: string(respData), + expectedResponse: &result, + }, + "500 internal server error": { + params: ListAccessKeysRequest{ + VersionGUID: "1", + }, + expectedPath: "/cam/v1/access-keys?versionGuid=1", + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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.ListAccessKeys(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestDeleteAccessKey(t *testing.T) { + + tests := map[string]struct { + params AccessKeyRequest + expectedPath string + responseStatus int + responseBody string + withError func(*testing.T, error) + }{ + "204 No Content": { + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + expectedPath: "/cam/v1/access-keys/1", + responseStatus: http.StatusNoContent, + }, + "missing required params - validation error": { + params: AccessKeyRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "delete an access key: struct validation: AccessKeyUID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + expectedPath: "/cam/v1/access-keys/1", + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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.DeleteAccessKey(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestUpdateAccessKey(t *testing.T) { + tests := map[string]struct { + accessKey UpdateAccessKeyRequest + params AccessKeyRequest + expectedPath string + responseStatus int + responseBody string + expectedResponse *UpdateAccessKeyResponse + withError func(*testing.T, error) + }{ + "201 OK": { + accessKey: UpdateAccessKeyRequest{ + AccessKeyName: "key2", + }, + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + expectedPath: "/cam/v1/access-keys/1", + responseStatus: http.StatusOK, + responseBody: ` + { + "accessKeyName": "key2", + "AccessKeyUID": 1 + + }`, + expectedResponse: &UpdateAccessKeyResponse{ + AccessKeyUID: 1, + AccessKeyName: "key2", + }, + }, + "missing required params - validation error": { + params: AccessKeyRequest{}, + accessKey: UpdateAccessKeyRequest{ + AccessKeyName: "key2", + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "update an access key: struct validation: AccessKeyUID: cannot be blank", err.Error()) + }, + }, + "missing required request body - validation error": { + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + accessKey: UpdateAccessKeyRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "update an access key: struct validation: AccessKeyName: cannot be blank", err.Error()) + }, + }, + "max length - validation error": { + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + accessKey: UpdateAccessKeyRequest{ + AccessKeyName: "asdfghjkloasdfghjkloasdfghjkloasdfghjkloasdfghjkloasdfghjkloasdfghjkloasdfghjklo", + }, + withError: func(t *testing.T, err error) { + assert.Equal(t, "update an access key: struct validation: AccessKeyName: the length must be between 1 and 50", err.Error()) + }, + }, + "500 internal server error": { + accessKey: UpdateAccessKeyRequest{ + AccessKeyName: "key2", + }, + params: AccessKeyRequest{ + AccessKeyUID: 1, + }, + expectedPath: "/cam/v1/access-keys/1", + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 + }`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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) + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateAccessKey(context.Background(), test.accessKey, test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func loadTestData(name string) ([]byte, error) { + data, err := ioutil.ReadFile(fmt.Sprintf("./testdata/%s", name)) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/pkg/cloudaccess/access_key_version.go b/pkg/cloudaccess/access_key_version.go new file mode 100644 index 00000000..d68b0e16 --- /dev/null +++ b/pkg/cloudaccess/access_key_version.go @@ -0,0 +1,294 @@ +package cloudaccess + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // GetAccessKeyVersionStatusResponse contains response from GetAccessKeyVersionStatus + GetAccessKeyVersionStatusResponse struct { + AccessKeyVersion *KeyVersion `json:"accessKeyVersion"` + ProcessingStatus ProcessingType `json:"processingStatus"` + RequestDate time.Time `json:"requestDate"` + RequestedBy string `json:"requestedBy"` + } + + // GetAccessKeyVersionStatusRequest holds parameters for GetAccessKeyVersionStatus + GetAccessKeyVersionStatusRequest struct { + RequestID int64 + } + + // CreateAccessKeyVersionRequest holds parameters for CreateAccessKeyVersion + CreateAccessKeyVersionRequest struct { + AccessKeyUID int64 + BodyParams CreateAccessKeyVersionBodyParams + } + + // CreateAccessKeyVersionBodyParams holds body parameters for CreateAccessKeyVersion + CreateAccessKeyVersionBodyParams struct { + CloudAccessKeyID string `json:"cloudAccessKeyId"` + CloudSecretAccessKey string `json:"cloudSecretAccessKey"` + } + + // CreateAccessKeyVersionResponse contains response from CreateAccessKeyVersion + CreateAccessKeyVersionResponse struct { + RequestID int64 `json:"requestId"` + RetryAfter int64 `json:"retryAfter"` + } + + // GetAccessKeyVersionRequest holds parameters for GetAccessKeyVersion + GetAccessKeyVersionRequest struct { + Version int64 + AccessKeyUID int64 + } + + // GetAccessKeyVersionResponse contains response from GetAccessKeyVersion + GetAccessKeyVersionResponse AccessKeyVersion + + // ListAccessKeyVersionsRequest holds parameters for ListAccessKeyVersion + ListAccessKeyVersionsRequest struct { + AccessKeyUID int64 + } + + // ListAccessKeyVersionsResponse contains response from ListAccessKeyVersions + ListAccessKeyVersionsResponse struct { + AccessKeyVersions []AccessKeyVersion `json:"accessKeyVersions"` + } + + // DeleteAccessKeyVersionRequest hold parameters for DeleteAccessKeyVersion + DeleteAccessKeyVersionRequest struct { + Version int64 + AccessKeyUID int64 + } + + // DeleteAccessKeyVersionResponse contains response from DeleteAccessKeyVersion + DeleteAccessKeyVersionResponse AccessKeyVersion + + // AccessKeyVersion holds information about access key version + AccessKeyVersion struct { + AccessKeyUID int64 `json:"accessKeyUid"` + CloudAccessKeyID *string `json:"cloudAccessKeyId"` + CreatedBy string `json:"createdBy"` + CreatedTime time.Time `json:"createdTime"` + DeploymentStatus DeploymentStatus `json:"deploymentStatus"` + Version int64 `json:"version"` + VersionGUID string `json:"versionGuid"` + } + + // DeploymentStatus represents deployment information + DeploymentStatus string +) + +const ( + // PendingActivation represents pending activation deployment status of access key version + PendingActivation DeploymentStatus = "PENDING_ACTIVATION" + // Active represents activated deployment status of access key version + Active DeploymentStatus = "ACTIVE" + // PendingDeletion represents pending deletion deployment status of access key version + PendingDeletion DeploymentStatus = "PENDING_DELETION" +) + +// Validate validates GetAccessKeyVersionStatusRequest +func (r GetAccessKeyVersionStatusRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "RequestID": validation.Validate(r.RequestID, validation.Required), + }) +} + +// Validate validates CreateAccessKeyVersionRequest +func (r CreateAccessKeyVersionRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + "BodyParams": validation.Validate(r.BodyParams, validation.Required), + }) +} + +// Validate validates CreateAccessKeyVersionBodyParams +func (r CreateAccessKeyVersionBodyParams) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "CloudAccessKeyID": validation.Validate(r.CloudAccessKeyID, validation.Required), + "CloudSecretAccessKey": validation.Validate(r.CloudSecretAccessKey, validation.Required), + }) +} + +// Validate validates GetAccessKeyVersionRequest +func (r GetAccessKeyVersionRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + "Version": validation.Validate(r.Version, validation.Required), + }) +} + +// Validate validates ListAccessKeyVersionsRequest +func (r ListAccessKeyVersionsRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + }) +} + +// Validate validates DeleteAccessKeyVersionRequest +func (r DeleteAccessKeyVersionRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + "Version": validation.Validate(r.Version, validation.Required), + }) +} + +var ( + // ErrGetAccessKeyVersionStatus is returned when GetAccessKeyVersionStatus fails + ErrGetAccessKeyVersionStatus = errors.New("get the status of an access key version") + // ErrCreateAccessKeyVersion is returned when CreateAccessKeyVersion fails + ErrCreateAccessKeyVersion = errors.New("create access key version") + // ErrGetAccessKeyVersion is returned when GetAccessKeyVersion fails + ErrGetAccessKeyVersion = errors.New("get access key version") + // ErrListAccessKeyVersions is returned when ListAccessKeyVersions fails + ErrListAccessKeyVersions = errors.New("list access key versions") + // ErrDeleteAccessKeyVersion is returned when DeleteAccessKeyVersion fails + ErrDeleteAccessKeyVersion = errors.New("delete access key version") +) + +func (c *cloudaccess) GetAccessKeyVersionStatus(ctx context.Context, params GetAccessKeyVersionStatusRequest) (*GetAccessKeyVersionStatusResponse, error) { + logger := c.Log(ctx) + logger.Debug("GetAccessKeyStatusVersion") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetAccessKeyVersionStatus, ErrStructValidation, err) + } + + getURL := fmt.Sprintf("/cam/v1/access-key-version-create-requests/%d", params.RequestID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetAccessKeyVersionStatus, err) + } + + var result GetAccessKeyVersionStatusResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %w", ErrGetAccessKeyVersionStatus, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrGetAccessKeyVersionStatus, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudaccess) CreateAccessKeyVersion(ctx context.Context, params CreateAccessKeyVersionRequest) (*CreateAccessKeyVersionResponse, error) { + logger := c.Log(ctx) + logger.Debug("CreateAccessKeyVersion") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrCreateAccessKeyVersion, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/cam/v1/access-keys/%d/versions", params.AccessKeyUID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrCreateAccessKeyVersion, err) + } + + var result CreateAccessKeyVersionResponse + resp, err := c.Exec(req, &result, params.BodyParams) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %w", ErrCreateAccessKeyVersion, err) + } + + if resp.StatusCode != http.StatusAccepted { + return nil, fmt.Errorf("%s: %w", ErrCreateAccessKeyVersion, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudaccess) GetAccessKeyVersion(ctx context.Context, params GetAccessKeyVersionRequest) (*GetAccessKeyVersionResponse, error) { + logger := c.Log(ctx) + logger.Debug("GetAccessKeyVersion") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetAccessKeyVersion, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/cam/v1/access-keys/%d/versions/%d", params.AccessKeyUID, params.Version) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetAccessKeyVersion, err) + } + + var result GetAccessKeyVersionResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %w", ErrGetAccessKeyVersion, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrGetAccessKeyVersion, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudaccess) ListAccessKeyVersions(ctx context.Context, params ListAccessKeyVersionsRequest) (*ListAccessKeyVersionsResponse, error) { + logger := c.Log(ctx) + logger.Debug("ListAccessKeyVersions") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrListAccessKeyVersions, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/cam/v1/access-keys/%d/versions", params.AccessKeyUID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListAccessKeyVersions, err) + } + + var result ListAccessKeyVersionsResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %w", ErrListAccessKeyVersions, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListAccessKeyVersions, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudaccess) DeleteAccessKeyVersion(ctx context.Context, params DeleteAccessKeyVersionRequest) (*DeleteAccessKeyVersionResponse, error) { + logger := c.Log(ctx) + logger.Debug("DeleteAccessKeyVersion") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrDeleteAccessKeyVersion, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/cam/v1/access-keys/%d/versions/%d", params.AccessKeyUID, params.Version) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrDeleteAccessKeyVersion, err) + } + + var result DeleteAccessKeyVersionResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %w", ErrDeleteAccessKeyVersion, err) + } + + if resp.StatusCode != http.StatusAccepted { + return nil, fmt.Errorf("%s: %w", ErrDeleteAccessKeyVersion, c.Error(resp)) + } + + return &result, nil +} diff --git a/pkg/cloudaccess/access_key_version_test.go b/pkg/cloudaccess/access_key_version_test.go new file mode 100644 index 00000000..ea73d3c4 --- /dev/null +++ b/pkg/cloudaccess/access_key_version_test.go @@ -0,0 +1,618 @@ +package cloudaccess + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAccessKeyVersionStatus(t *testing.T) { + tests := map[string]struct { + params GetAccessKeyVersionStatusRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetAccessKeyVersionStatusResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetAccessKeyVersionStatusRequest{ + RequestID: 1, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "accessKeyVersion": { + "accessKeyUid": 123, + "link": "/cam/v1/access-keys/123/versions/2", + "version": 2 + }, + "processingStatus": "IN_PROGRESS", + "requestDate": "2021-02-26T14:54:38.622074Z", + "requestedBy": "user" +}`, + expectedPath: "/cam/v1/access-key-version-create-requests/1", + expectedResponse: &GetAccessKeyVersionStatusResponse{ + ProcessingStatus: ProcessingInProgress, + RequestDate: time.Date(2021, 2, 26, 14, 54, 38, 622074000, time.UTC), + RequestedBy: "user", + AccessKeyVersion: &KeyVersion{ + AccessKeyUID: 123, + Link: "/cam/v1/access-keys/123/versions/2", + Version: 2, + }, + }, + }, + "200 OK - minimal": { + params: GetAccessKeyVersionStatusRequest{ + RequestID: 1, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "accessKeyVersion": null, + "processingStatus": "IN_PROGRESS", + "requestDate": "2021-02-26T14:54:38.622074Z", + "requestedBy": "user" +}`, + expectedPath: "/cam/v1/access-key-version-create-requests/1", + expectedResponse: &GetAccessKeyVersionStatusResponse{ + ProcessingStatus: ProcessingInProgress, + RequestDate: time.Date(2021, 2, 26, 14, 54, 38, 622074000, time.UTC), + RequestedBy: "user", + }, + }, + "missing required params - validation error": { + params: GetAccessKeyVersionStatusRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get the status of an access key version: struct validation: RequestID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: GetAccessKeyVersionStatusRequest{ + RequestID: 123, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + expectedPath: "/cam/v1/access-key-version-create-requests/123", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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.GetAccessKeyVersionStatus(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestCreateAccessKeyVersion(t *testing.T) { + tests := map[string]struct { + params CreateAccessKeyVersionRequest + responseStatus int + responseBody string + expectedPath string + expectedRequestBody string + expectedResponse *CreateAccessKeyVersionResponse + withError func(*testing.T, error) + }{ + "202 ACCEPTED": { + params: CreateAccessKeyVersionRequest{ + AccessKeyUID: 1, + BodyParams: CreateAccessKeyVersionBodyParams{ + CloudAccessKeyID: "key-1", + CloudSecretAccessKey: "secret-1", + }, + }, + responseStatus: http.StatusAccepted, + responseBody: ` +{ + "requestId": 111, + "retryAfter": 6 +}`, + expectedPath: "/cam/v1/access-keys/1/versions", + expectedRequestBody: ` +{ + "cloudAccessKeyId": "key-1", + "cloudSecretAccessKey": "secret-1" +} +`, + expectedResponse: &CreateAccessKeyVersionResponse{ + RequestID: 111, + RetryAfter: 6, + }, + }, + "missing required params - validation error": { + params: CreateAccessKeyVersionRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "create access key version: struct validation: AccessKeyUID: cannot be blank\nBodyParams: CloudAccessKeyID: cannot be blank\nCloudSecretAccessKey: cannot be blank", err.Error()) + }, + }, + "404 error": { + params: CreateAccessKeyVersionRequest{ + AccessKeyUID: 1, + BodyParams: CreateAccessKeyVersionBodyParams{ + CloudAccessKeyID: "key-1", + CloudSecretAccessKey: "secret-1", + }, + }, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "accessKeyUid": 1, + "detail": "Access key with accessKeyUid '1' does not exist.", + "instance": "c111eff1-22ec-4d4e-99c9-55efb5d55f55", + "status": 404, + "title": "Domain Error", + "type": "/cam/error-types/access-key-does-not-exist" +}`, + expectedPath: "/cam/v1/access-keys/1/versions", + expectedRequestBody: ` +{ + "cloudAccessKeyId": "key-1", + "cloudSecretAccessKey": "secret-1" +} +`, + withError: func(t *testing.T, err error) { + want := &Error{ + AccessKeyUID: 1, + Type: "/cam/error-types/access-key-does-not-exist", + Title: "Domain Error", + Detail: "Access key with accessKeyUid '1' does not exist.", + Instance: "c111eff1-22ec-4d4e-99c9-55efb5d55f55", + Status: http.StatusNotFound, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "409 error": { + params: CreateAccessKeyVersionRequest{ + AccessKeyUID: 1, + BodyParams: CreateAccessKeyVersionBodyParams{ + CloudAccessKeyID: "key-1", + CloudSecretAccessKey: "secret-1", + }, + }, + responseStatus: http.StatusConflict, + responseBody: ` +{ + "accessKeyName": "Sales-s3", + "detail": "Access key with name 'Sales-s3' already exists.", + "instance": "109443e6-f347-43f1-922c-fa0fd480973f", + "status": 409, + "title": "Domain Error", + "type": "/cam/error-types/access-key-already-exists" +}`, + expectedPath: "/cam/v1/access-keys/1/versions", + expectedRequestBody: ` +{ + "cloudAccessKeyId": "key-1", + "cloudSecretAccessKey": "secret-1" +} +`, + withError: func(t *testing.T, err error) { + want := &Error{ + AccessKeyName: "Sales-s3", + Type: "/cam/error-types/access-key-already-exists", + Title: "Domain Error", + Detail: "Access key with name 'Sales-s3' already exists.", + Instance: "109443e6-f347-43f1-922c-fa0fd480973f", + Status: http.StatusConflict, + } + 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.CreateAccessKeyVersion(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestGetAccessKeyVersion(t *testing.T) { + tests := map[string]struct { + params GetAccessKeyVersionRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetAccessKeyVersionResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetAccessKeyVersionRequest{ + AccessKeyUID: 12345, + Version: 1, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "accessKeyUid": 12345, + "cloudAccessKeyId": null, + "createdBy": "testUser", + "createdTime": "2021-02-26T13:34:37.916873Z", + "deploymentStatus": "ACTIVE", + "version": 1, + "versionGuid": "aaaa-bbbb-1111" +}`, + expectedPath: "/cam/v1/access-keys/12345/versions/1", + expectedResponse: &GetAccessKeyVersionResponse{ + AccessKeyUID: 12345, + CreatedBy: "testUser", + CreatedTime: *newTimeFromString(t, "2021-02-26T13:34:37.916873Z"), + DeploymentStatus: Active, + Version: 1, + VersionGUID: "aaaa-bbbb-1111", + }, + }, + "missing required params - validation error": { + params: GetAccessKeyVersionRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get access key version: struct validation: AccessKeyUID: cannot be blank\nVersion: cannot be blank", err.Error()) + }, + }, + "404 error": { + params: GetAccessKeyVersionRequest{ + AccessKeyUID: 1, + Version: 1, + }, + responseStatus: http.StatusNotFound, + responseBody: ` + { + "accessKeyUid": 1, + "detail": "Access key with accessKeyUid '1' does not exist.", + "instance": "c111eff1-22ec-4d4e-99c9-55efb5d55f55", + "status": 404, + "title": "Domain Error", + "type": "/cam/error-types/access-key-does-not-exist" + }`, + expectedPath: "/cam/v1/access-keys/1/versions/1", + withError: func(t *testing.T, err error) { + want := &Error{ + AccessKeyUID: 1, + Type: "/cam/error-types/access-key-does-not-exist", + Title: "Domain Error", + Detail: "Access key with accessKeyUid '1' does not exist.", + Instance: "c111eff1-22ec-4d4e-99c9-55efb5d55f55", + Status: http.StatusNotFound, + } + 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.GetAccessKeyVersion(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestListAccessKeyVersions(t *testing.T) { + tests := map[string]struct { + params ListAccessKeyVersionsRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *ListAccessKeyVersionsResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: ListAccessKeyVersionsRequest{ + AccessKeyUID: 2, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "accessKeyVersions": [ + { + "accessKeyUid": 2, + "cloudAccessKeyId": null, + "createdBy": "testUser2", + "createdTime": "2021-02-26T14:48:27.355346Z", + "deploymentStatus": "PENDING_ACTIVATION", + "version": 2, + "versionGuid": "bbbb-2222" + }, + { + "accessKeyUid": 2, + "cloudAccessKeyId": null, + "createdBy": "testUser1", + "createdTime": "2021-02-26T13:34:37.916873Z", + "deploymentStatus": "ACTIVE", + "version": 1, + "versionGuid": "aaaa-1111" + } + ] +}`, + expectedPath: "/cam/v1/access-keys/2/versions", + expectedResponse: &ListAccessKeyVersionsResponse{ + AccessKeyVersions: []AccessKeyVersion{ + { + AccessKeyUID: 2, + CreatedBy: "testUser2", + CreatedTime: *newTimeFromString(t, "2021-02-26T14:48:27.355346Z"), + DeploymentStatus: PendingActivation, + Version: 2, + VersionGUID: "bbbb-2222", + }, + { + AccessKeyUID: 2, + CreatedBy: "testUser1", + CreatedTime: *newTimeFromString(t, "2021-02-26T13:34:37.916873Z"), + DeploymentStatus: Active, + Version: 1, + VersionGUID: "aaaa-1111", + }, + }, + }, + }, + "200 OK - single version": { + params: ListAccessKeyVersionsRequest{ + AccessKeyUID: 2, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "accessKeyVersions": [ + { + "accessKeyUid": 2, + "cloudAccessKeyId": null, + "createdBy": "testUser2", + "createdTime": "2021-02-26T14:48:27.355346Z", + "deploymentStatus": "PENDING_ACTIVATION", + "version": 2, + "versionGuid": "bbbb-2222" + } + ] +}`, + expectedPath: "/cam/v1/access-keys/2/versions", + expectedResponse: &ListAccessKeyVersionsResponse{ + AccessKeyVersions: []AccessKeyVersion{ + { + AccessKeyUID: 2, + CreatedBy: "testUser2", + CreatedTime: *newTimeFromString(t, "2021-02-26T14:48:27.355346Z"), + DeploymentStatus: PendingActivation, + Version: 2, + VersionGUID: "bbbb-2222", + }, + }, + }, + }, + "200 OK - no versions": { + params: ListAccessKeyVersionsRequest{ + AccessKeyUID: 2, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "accessKeyVersions": [] +}`, + expectedPath: "/cam/v1/access-keys/2/versions", + expectedResponse: &ListAccessKeyVersionsResponse{ + AccessKeyVersions: []AccessKeyVersion{}, + }, + }, + "missing required params - validation error": { + params: ListAccessKeyVersionsRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "list access key versions: struct validation: AccessKeyUID: cannot be blank", err.Error()) + }, + }, + "500 internal server error": { + params: ListAccessKeyVersionsRequest{ + AccessKeyUID: 1, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +}`, + expectedPath: "/cam/v1/access-keys/1/versions", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + 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.ListAccessKeyVersions(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestDeleteAccessKeyVersion(t *testing.T) { + tests := map[string]struct { + params DeleteAccessKeyVersionRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *DeleteAccessKeyVersionResponse + withError func(*testing.T, error) + }{ + "202 ACCEPTED": { + params: DeleteAccessKeyVersionRequest{ + AccessKeyUID: 12345, + Version: 1, + }, + responseStatus: http.StatusAccepted, + responseBody: ` +{ + "accessKeyUid": 12345, + "cloudAccessKeyId": null, + "createdBy": "testUser", + "createdTime": "2021-02-26T09:09:53.762230Z", + "deploymentStatus": "PENDING_DELETION", + "version": 1, + "versionGuid": "aaaa-bbbb-1111" +}`, + expectedPath: "/cam/v1/access-keys/12345/versions/1", + expectedResponse: &DeleteAccessKeyVersionResponse{ + AccessKeyUID: 12345, + CreatedBy: "testUser", + CreatedTime: *newTimeFromString(t, "2021-02-26T09:09:53.762230Z"), + DeploymentStatus: PendingDeletion, + Version: 1, + VersionGUID: "aaaa-bbbb-1111", + }, + }, + "missing required params - validation error": { + params: DeleteAccessKeyVersionRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "delete access key version: struct validation: AccessKeyUID: cannot be blank\nVersion: cannot be blank", err.Error()) + }, + }, + "404 error": { + params: DeleteAccessKeyVersionRequest{ + AccessKeyUID: 1, + Version: 1, + }, + responseStatus: http.StatusNotFound, + responseBody: ` + { + "accessKeyUid": 1, + "detail": "Access key with accessKeyUid '1' does not exist.", + "instance": "c111eff1-22ec-4d4e-99c9-55efb5d55f55", + "status": 404, + "title": "Domain Error", + "type": "/cam/error-types/access-key-does-not-exist" + }`, + expectedPath: "/cam/v1/access-keys/1/versions/1", + withError: func(t *testing.T, err error) { + want := &Error{ + AccessKeyUID: 1, + Type: "/cam/error-types/access-key-does-not-exist", + Title: "Domain Error", + Detail: "Access key with accessKeyUid '1' does not exist.", + Instance: "c111eff1-22ec-4d4e-99c9-55efb5d55f55", + Status: http.StatusNotFound, + } + 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) + result, err := client.DeleteAccessKeyVersion(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func newTimeFromString(t *testing.T, s string) *time.Time { + parsedTime, err := time.Parse(time.RFC3339, s) + require.NoError(t, err) + return &parsedTime +} diff --git a/pkg/cloudaccess/cloudaccess.go b/pkg/cloudaccess/cloudaccess.go new file mode 100644 index 00000000..f4d51d5c --- /dev/null +++ b/pkg/cloudaccess/cloudaccess.go @@ -0,0 +1,172 @@ +// Package cloudaccess provides access to the Akamai Cloud Access Manager API +package cloudaccess + +import ( + "context" + "errors" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" +) + +var ( + // ErrStructValidation is returned when given struct validation failed + ErrStructValidation = errors.New("struct validation") +) + +type ( + // CloudAccess is the API interface for Cloud Access Manager + CloudAccess interface { + // GetAccessKeyStatus gets the current status and other details for a request to create a new access key + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-create-request + GetAccessKeyStatus(context.Context, GetAccessKeyStatusRequest) (*GetAccessKeyStatusResponse, error) + + // GetAccessKeyVersionStatus gets the current status and other details for a request to create a new access key version + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-version-create-request + GetAccessKeyVersionStatus(context.Context, GetAccessKeyVersionStatusRequest) (*GetAccessKeyVersionStatusResponse, error) + + // CreateAccessKey creates a new access key + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/post-access-key + CreateAccessKey(context.Context, CreateAccessKeyRequest) (*CreateAccessKeyResponse, error) + + // GetAccessKey returns details for a specific access key + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key + GetAccessKey(context.Context, AccessKeyRequest) (*GetAccessKeyResponse, error) + + // ListAccessKeys returns detailed information about all access keys available to the current user account + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-keys + ListAccessKeys(context.Context, ListAccessKeysRequest) (*ListAccessKeysResponse, error) + + // UpdateAccessKey updates name of an access key + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/put-access-key + UpdateAccessKey(context.Context, UpdateAccessKeyRequest, AccessKeyRequest) (*UpdateAccessKeyResponse, error) + + // DeleteAccessKey deletes an access key + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/delete-access-key + DeleteAccessKey(context.Context, AccessKeyRequest) error + + // CreateAccessKeyVersion rotates an access key to a new version. + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/post-access-key-version + CreateAccessKeyVersion(context.Context, CreateAccessKeyVersionRequest) (*CreateAccessKeyVersionResponse, error) + + // GetAccessKeyVersion returns detailed information for a specific version of an access key. + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-version + GetAccessKeyVersion(context.Context, GetAccessKeyVersionRequest) (*GetAccessKeyVersionResponse, error) + + // ListAccessKeyVersions returns detailed information about all the versions for a specific access key. + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-versions + ListAccessKeyVersions(context.Context, ListAccessKeyVersionsRequest) (*ListAccessKeyVersionsResponse, error) + + // DeleteAccessKeyVersion deletes a specific version of an access key. + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/delete-access-key-version + DeleteAccessKeyVersion(context.Context, DeleteAccessKeyVersionRequest) (*DeleteAccessKeyVersionResponse, error) + + // LookupProperties returns information about all the Property Manager properties that use a specific version of an access key. + // This operation gets the data directly. To avoid any latency problems, use the GetAsyncPropertiesLookupID and PerformAsyncPropertiesLookup + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-access-key-version-properties + LookupProperties(ctx context.Context, params LookupPropertiesRequest) (*LookupPropertiesResponse, error) + + // GetAsyncPropertiesLookupID gets the unique identifier used to perform an PerformAsyncPropertiesLookup + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-async-version-property-lookup + GetAsyncPropertiesLookupID(ctx context.Context, params GetAsyncPropertiesLookupIDRequest) (*GetAsyncPropertiesLookupIDResponse, error) + + // PerformAsyncPropertiesLookup returns in asynchronous way information about all the Property Manager properties that use a specific version of an access key. + // + // See: https://techdocs.akamai.com/cloud-access-mgr/reference/get-property-lookup + PerformAsyncPropertiesLookup(ctx context.Context, params PerformAsyncPropertiesLookupRequest) (*PerformAsyncPropertiesLookupResponse, error) + } + + cloudaccess struct { + session.Session + } + + // Option defines an CloudAccess option + Option func(*cloudaccess) + + // KeyLink contains hypermedia link for the key + KeyLink struct { + AccessKeyUID int64 `json:"accessKeyUid"` + Link string `json:"link"` + } + + // KeyVersion holds details for a version of an access key + KeyVersion struct { + AccessKeyUID int64 `json:"accessKeyUid"` + Link string `json:"link"` + Version int64 `json:"version"` + } + + // RequestInformation contains information about a request to create an access key + RequestInformation struct { + AccessKeyName string `json:"accessKeyName"` + AuthenticationMethod AuthType `json:"authenticationMethod"` + ContractID string `json:"contractId"` + GroupID int64 `json:"groupId"` + NetworkConfiguration *SecureNetwork `json:"networkConfiguration"` + } + + // SecureNetwork contains additional information about network + SecureNetwork struct { + AdditionalCDN *CDNType `json:"additionalCdn,omitempty"` + SecurityNetwork NetworkType `json:"securityNetwork"` + } + + // CDNType is a type of additionalCdn + CDNType string + + // NetworkType is a type of securityNetwork + NetworkType string + + // AuthType is a type of authentication + AuthType string + + // ProcessingType is a type of ProcessingStatus + ProcessingType string +) + +const ( + // ChinaCDN represents CDN value of "CHINA_CDN" + ChinaCDN CDNType = "CHINA_CDN" + // RussiaCDN represents CDN value of "RUSSIA_CDN" + RussiaCDN CDNType = "RUSSIA_CDN" + + // NetworkEnhanced represents Network value of "ENHANCED_TLS" + NetworkEnhanced NetworkType = "ENHANCED_TLS" + // NetworkStandard represents Network value of "STANDARD_TLS" + NetworkStandard NetworkType = "STANDARD_TLS" + + // AuthAWS represents Authentication value of "AWS4_HMAC_SHA256" + AuthAWS AuthType = "AWS4_HMAC_SHA256" + // AuthGOOG represents Authentication value of "GOOG4_HMAC_SHA256" + AuthGOOG AuthType = "GOOG4_HMAC_SHA256" + + // ProcessingInProgress represents ProcessingStatus value of 'IN_PROGRESS' + ProcessingInProgress ProcessingType = "IN_PROGRESS" + // ProcessingFailed represents ProcessingStatus value of 'FAILED' + ProcessingFailed ProcessingType = "FAILED" + // ProcessingDone represents ProcessingStatus value of 'DONE' + ProcessingDone ProcessingType = "DONE" +) + +// Client returns a new cloudaccess Client instance with the specified controller +func Client(sess session.Session, opts ...Option) CloudAccess { + c := &cloudaccess{ + Session: sess, + } + for _, opt := range opts { + opt(c) + } + return c +} diff --git a/pkg/cloudaccess/cloudaccess_test.go b/pkg/cloudaccess/cloudaccess_test.go new file mode 100644 index 00000000..95cf9bc0 --- /dev/null +++ b/pkg/cloudaccess/cloudaccess_test.go @@ -0,0 +1,62 @@ +package cloudaccess + +import ( + "crypto/tls" + "crypto/x509" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegrid" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockAPIClient(t *testing.T, mockServer *httptest.Server) CloudAccess { + 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})) + require.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 *cloudaccess + }{ + "no options provided, return default": { + options: nil, + expected: &cloudaccess{ + Session: sess, + }, + }, + "option provided, overwrite session": { + options: []Option{func(c *cloudaccess) { + c.Session = nil + }}, + expected: &cloudaccess{ + 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/cloudaccess/errors.go b/pkg/cloudaccess/errors.go new file mode 100644 index 00000000..f90f14f8 --- /dev/null +++ b/pkg/cloudaccess/errors.go @@ -0,0 +1,85 @@ +package cloudaccess + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/errs" +) + +type ( + // Error is a cloudaccess error interface + // For details on possible error types, refer to: https://techdocs.akamai.com/cloud-access-mgr/reference/errors + Error struct { + Type string `json:"type"` + Title string `json:"title"` + Detail string `json:"detail"` + Instance string `json:"instance"` + Status int64 `json:"status"` + AccessKeyUID int64 `json:"accessKeyUid,omitempty"` + AccessKeyName string `json:"accessKeyName,omitempty"` + ProblemID string `json:"problemId,omitempty"` + Version int64 `json:"version"` + Errors []ErrorItem `json:"errors,omitempty"` + } + + // ErrorItem is a cloud access error's item + ErrorItem struct { + Detail string `json:"detail"` + Title string `json:"title"` + Type string `json:"type"` + } +) + +// Error parses an error from the response +func (c *cloudaccess) Error(r *http.Response) error { + var e Error + var body []byte + body, err := io.ReadAll(r.Body) + if err != nil { + c.Log(r.Request.Context()).Errorf("reading error response body: %s", err) + e.Status = int64(r.StatusCode) + e.Title = fmt.Sprintf("Failed to read error body") + e.Detail = err.Error() + return &e + } + + if err := json.Unmarshal(body, &e); err != nil { + c.Log(r.Request.Context()).Errorf("could not unmarshal API error: %s", err) + e.Title = "Failed to unmarshal error body. Cloud Access Manager API failed. Check details for more information." + e.Detail = errs.UnescapeContent(string(body)) + } + + e.Status = int64(r.StatusCode) + + return &e +} + +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/cloudaccess/errors_test.go b/pkg/cloudaccess/errors_test.go new file mode 100644 index 00000000..476037e4 --- /dev/null +++ b/pkg/cloudaccess/errors_test.go @@ -0,0 +1,176 @@ +package cloudaccess + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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": "test-instance-123", + "status": 400 +}`), + ), + Request: req, + }, + expected: &Error{ + Type: "bad-request", + Title: "Bad Request", + Instance: "test-instance-123", + Status: http.StatusBadRequest, + }, + }, + "Invalid request 400": { + response: &http.Response{ + Status: "Internal Server Error", + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser(strings.NewReader( + `{ + "type": "invalid-request", + "title": "Invalid Request", + "instance": "test-instance-123", + "status": 400, + "errors": [ + { + "detail": "Constraint violation: accessKeyName must not be blank.", + "title": "Constraint Violation", + "type": "/cam/error-types/constraint-violation" + }, + { + "detail": "Constraint violation: accessKeyName length must be between 1 and 50.", + "title": "Constraint Violation", + "type": "/cam/error-types/constraint-violation" + } + ] +}`), + ), + Request: req, + }, + expected: &Error{ + Type: "invalid-request", + Title: "Invalid Request", + Instance: "test-instance-123", + Status: http.StatusBadRequest, + Errors: []ErrorItem{ + { + Detail: "Constraint violation: accessKeyName must not be blank.", + Title: "Constraint Violation", + Type: "/cam/error-types/constraint-violation", + }, + { + Detail: "Constraint violation: accessKeyName length must be between 1 and 50.", + Title: "Constraint Violation", + Type: "/cam/error-types/constraint-violation", + }, + }, + }, + }, + "access key does not exists 404": { + response: &http.Response{ + Status: "Internal Server Error", + StatusCode: http.StatusNotFound, + Body: ioutil.NopCloser(strings.NewReader( + `{ + "type": "access-key-does-not-exists", + "title": "Domain Error", + "detail": "Access key with accessKeyUID '1' does not exist.", + "instance": "test-instance-123", + "status": 404, + "accessKeyUid": 1 +}`), + ), + Request: req, + }, + expected: &Error{ + Type: "access-key-does-not-exists", + Title: "Domain Error", + Detail: "Access key with accessKeyUID '1' does not exist.", + Instance: "test-instance-123", + Status: http.StatusNotFound, + AccessKeyUID: 1, + }, + }, + "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: "Failed to unmarshal error body. Cloud Access Manager API failed. Check details for more information.", + Detail: "test", + Status: http.StatusInternalServerError, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := Client(sess).(*cloudaccess).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/cloudaccess/mocks.go b/pkg/cloudaccess/mocks.go new file mode 100644 index 00000000..292d484f --- /dev/null +++ b/pkg/cloudaccess/mocks.go @@ -0,0 +1,151 @@ +//revive:disable:exported + +package cloudaccess + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type Mock struct { + mock.Mock +} + +var _ CloudAccess = &Mock{} + +func (m *Mock) GetAccessKeyStatus(ctx context.Context, r GetAccessKeyStatusRequest) (*GetAccessKeyStatusResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetAccessKeyStatusResponse), args.Error(1) +} + +func (m *Mock) GetAccessKeyVersionStatus(ctx context.Context, r GetAccessKeyVersionStatusRequest) (*GetAccessKeyVersionStatusResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetAccessKeyVersionStatusResponse), args.Error(1) +} + +func (m *Mock) LookupProperties(ctx context.Context, r LookupPropertiesRequest) (*LookupPropertiesResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*LookupPropertiesResponse), args.Error(1) +} + +func (m *Mock) GetAsyncPropertiesLookupID(ctx context.Context, r GetAsyncPropertiesLookupIDRequest) (*GetAsyncPropertiesLookupIDResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetAsyncPropertiesLookupIDResponse), args.Error(1) +} + +func (m *Mock) PerformAsyncPropertiesLookup(ctx context.Context, r PerformAsyncPropertiesLookupRequest) (*PerformAsyncPropertiesLookupResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*PerformAsyncPropertiesLookupResponse), args.Error(1) +} + +func (m *Mock) CreateAccessKey(ctx context.Context, r CreateAccessKeyRequest) (*CreateAccessKeyResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*CreateAccessKeyResponse), args.Error(1) +} + +func (m *Mock) GetAccessKey(ctx context.Context, r AccessKeyRequest) (*GetAccessKeyResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetAccessKeyResponse), args.Error(1) +} + +func (m *Mock) ListAccessKeys(ctx context.Context, r ListAccessKeysRequest) (*ListAccessKeysResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListAccessKeysResponse), args.Error(1) +} + +func (m *Mock) UpdateAccessKey(ctx context.Context, request UpdateAccessKeyRequest, param AccessKeyRequest) (*UpdateAccessKeyResponse, error) { + args := m.Called(ctx, request, param) + + if args.Get(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*UpdateAccessKeyResponse), args.Error(1) +} + +func (m *Mock) DeleteAccessKey(ctx context.Context, r AccessKeyRequest) error { + args := m.Called(ctx, r) + + return args.Error(0) +} + +func (m *Mock) GetAccessKeyVersion(ctx context.Context, r GetAccessKeyVersionRequest) (*GetAccessKeyVersionResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetAccessKeyVersionResponse), args.Error(1) +} + +func (m *Mock) CreateAccessKeyVersion(ctx context.Context, r CreateAccessKeyVersionRequest) (*CreateAccessKeyVersionResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*CreateAccessKeyVersionResponse), args.Error(1) +} + +func (m *Mock) ListAccessKeyVersions(ctx context.Context, r ListAccessKeyVersionsRequest) (*ListAccessKeyVersionsResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListAccessKeyVersionsResponse), args.Error(1) +} + +func (m *Mock) DeleteAccessKeyVersion(ctx context.Context, r DeleteAccessKeyVersionRequest) (*DeleteAccessKeyVersionResponse, error) { + args := m.Called(ctx, r) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*DeleteAccessKeyVersionResponse), args.Error(1) +} diff --git a/pkg/cloudaccess/properties.go b/pkg/cloudaccess/properties.go new file mode 100644 index 00000000..20fe9558 --- /dev/null +++ b/pkg/cloudaccess/properties.go @@ -0,0 +1,200 @@ +package cloudaccess + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // LookupPropertiesRequest holds parameters for LookupProperties + LookupPropertiesRequest struct { + AccessKeyUID int64 + Version int64 + } + + // LookupPropertiesResponse contains response for LookupProperties + LookupPropertiesResponse struct { + Properties []Property `json:"properties"` + } + + // Property holds information about property related to given access key + Property struct { + AccessKeyUID int64 `json:"accessKeyUid"` + Version int64 `json:"version"` + PropertyID string `json:"propertyId"` + PropertyName string `json:"propertyName"` + ProductionVersion *int64 `json:"productionVersion"` + StagingVersion *int64 `json:"stagingVersion"` + } + + // GetAsyncPropertiesLookupIDRequest holds parameters for GetAsyncPropertiesLookupID + GetAsyncPropertiesLookupIDRequest struct { + AccessKeyUID int64 + Version int64 + } + + // GetAsyncPropertiesLookupIDResponse contains response for GetAsyncPropertiesLookupID + GetAsyncPropertiesLookupIDResponse struct { + LookupID int64 `json:"lookupId"` + RetryAfter int64 `json:"retryAfter"` + } + + // PerformAsyncPropertiesLookupRequest holds parameters for PerformAsyncPropertiesLookup + PerformAsyncPropertiesLookupRequest struct { + LookupID int64 + } + + // PerformAsyncPropertiesLookupResponse contains response for PerformAsyncPropertiesLookup + PerformAsyncPropertiesLookupResponse struct { + LookupID int64 `json:"lookupId"` + LookupStatus LookupStatus `json:"lookupStatus"` + Properties []Property `json:"properties"` + } + + // LookupStatus represents a lookup status + LookupStatus string +) + +const ( + // LookupComplete represents complete asynchronous property lookup status + LookupComplete LookupStatus = "COMPLETE" + // LookupError represents error asynchronous property lookup status + LookupError LookupStatus = "ERROR" + // LookupInProgress represents in progress asynchronous property lookup status + LookupInProgress LookupStatus = "IN_PROGRESS" + // LookupPending represents pending asynchronous property lookup status + LookupPending LookupStatus = "PENDING" + // LookupSubmitted represents submitted asynchronous property lookup status + LookupSubmitted LookupStatus = "SUBMITTED" +) + +// Validate validates LookupPropertiesRequest +func (r LookupPropertiesRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "Version": validation.Validate(r.Version, validation.Required), + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + }) +} + +// Validate validates GetAsyncPropertiesLookupIDRequest +func (r GetAsyncPropertiesLookupIDRequest) Validate() interface{} { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "Version": validation.Validate(r.Version, validation.Required), + "AccessKeyUID": validation.Validate(r.AccessKeyUID, validation.Required), + }) +} + +// Validate validates PerformAsyncPropertiesLookupRequest +func (r PerformAsyncPropertiesLookupRequest) Validate() interface{} { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "LookupID": validation.Validate(r.LookupID, validation.Required), + }) +} + +var ( + // ErrLookupProperties is returned when LookupProperties fails + ErrLookupProperties = errors.New("lookup properties") + // ErrGetAsyncLookupIDProperties is returned when GetAsyncPropertiesLookupID fails + ErrGetAsyncLookupIDProperties = errors.New("get lookup properties id async") + // ErrPerformAsyncLookupProperties is returned when PerformAsyncPropertiesLookup fails + ErrPerformAsyncLookupProperties = errors.New("perform async lookup properties") +) + +func (c *cloudaccess) LookupProperties(ctx context.Context, params LookupPropertiesRequest) (*LookupPropertiesResponse, error) { + logger := c.Log(ctx) + logger.Debug("LookupProperties") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrLookupProperties, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cam/v1/access-keys/%d/versions/%d/properties", params.AccessKeyUID, params.Version)) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrLookupProperties, err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrLookupProperties, err) + } + + var result LookupPropertiesResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrLookupProperties, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrLookupProperties, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudaccess) GetAsyncPropertiesLookupID(ctx context.Context, params GetAsyncPropertiesLookupIDRequest) (*GetAsyncPropertiesLookupIDResponse, error) { + logger := c.Log(ctx) + logger.Debug("GetAsyncPropertiesLookupID") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetAsyncLookupIDProperties, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cam/v1/access-keys/%d/versions/%d/property-lookup-id", params.AccessKeyUID, params.Version)) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrGetAsyncLookupIDProperties, err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetAsyncLookupIDProperties, err) + } + + var result GetAsyncPropertiesLookupIDResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrGetAsyncLookupIDProperties, err) + } + + if resp.StatusCode != http.StatusAccepted { + return nil, fmt.Errorf("%s: %w", ErrGetAsyncLookupIDProperties, c.Error(resp)) + } + + return &result, nil +} + +func (c *cloudaccess) PerformAsyncPropertiesLookup(ctx context.Context, params PerformAsyncPropertiesLookupRequest) (*PerformAsyncPropertiesLookupResponse, error) { + logger := c.Log(ctx) + logger.Debug("PerformAsyncPropertiesLookup") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrPerformAsyncLookupProperties, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/cam/v1/property-lookups/%d", params.LookupID)) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrPerformAsyncLookupProperties, err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrPerformAsyncLookupProperties, err) + } + + var result PerformAsyncPropertiesLookupResponse + resp, err := c.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrPerformAsyncLookupProperties, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrPerformAsyncLookupProperties, c.Error(resp)) + } + + return &result, nil +} diff --git a/pkg/cloudaccess/properties_test.go b/pkg/cloudaccess/properties_test.go new file mode 100644 index 00000000..c5aa1135 --- /dev/null +++ b/pkg/cloudaccess/properties_test.go @@ -0,0 +1,396 @@ +package cloudaccess + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/tools" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLookupProperties(t *testing.T) { + tests := map[string]struct { + request LookupPropertiesRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *LookupPropertiesResponse + withError func(*testing.T, error) + }{ + "200 OK empty": { + request: LookupPropertiesRequest{AccessKeyUID: 1234, Version: 1}, + responseStatus: http.StatusOK, + responseBody: ` +{ + "properties": [] +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/1/properties", + expectedResponse: &LookupPropertiesResponse{ + Properties: []Property{}, + }, + }, + "200 OK with properties": { + request: LookupPropertiesRequest{AccessKeyUID: 1234, Version: 1}, + responseStatus: http.StatusOK, + responseBody: ` +{ + "properties": [ + { + "accessKeyUid": 1234, + "version": 1, + "propertyId": "prp_5678", + "propertyName": "test-property", + "productionVersion": null, + "stagingVersion": 1 + }, + { + "accessKeyUid": 1234, + "version": 1, + "propertyId": "prp_6789", + "propertyName": "test-property2", + "productionVersion": 1, + "stagingVersion": null + } + ] +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/1/properties", + expectedResponse: &LookupPropertiesResponse{ + Properties: []Property{ + { + AccessKeyUID: 1234, + Version: 1, + PropertyID: "prp_5678", + PropertyName: "test-property", + ProductionVersion: nil, + StagingVersion: tools.Int64Ptr(1), + }, + { + AccessKeyUID: 1234, + Version: 1, + PropertyID: "prp_6789", + PropertyName: "test-property2", + ProductionVersion: tools.Int64Ptr(1), + StagingVersion: nil, + }, + }, + }, + }, + "404 incorrect request": { + request: LookupPropertiesRequest{AccessKeyUID: 1234, Version: 10}, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "/cam/error-types/access-key-version-does-not-exist", + "title": "Domain Error", + "instance": "60126c0d-67f5-473c-bea0-16daa836dc44", + "status": 404, + "detail": "Version '10' for access key '1234' does not exist.", + "problemId": "60126c0d-67f5-473c-bea0-16daa836dc44", + "version": 10, + "accessKeyUID": 1234 +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/10/properties", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cam/error-types/access-key-version-does-not-exist", + Title: "Domain Error", + Detail: "Version '10' for access key '1234' does not exist.", + Instance: "60126c0d-67f5-473c-bea0-16daa836dc44", + Status: 404, + ProblemID: "60126c0d-67f5-473c-bea0-16daa836dc44", + AccessKeyUID: 1234, + Version: 10, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "500 internal server error": { + request: LookupPropertiesRequest{AccessKeyUID: 1234, Version: 1}, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/1/properties", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "validate errors": { + request: LookupPropertiesRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "lookup properties: struct validation: AccessKeyUID: cannot be blank\nVersion: cannot be blank", err.Error()) + }, + }, + } + 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.LookupProperties(context.Background(), test.request) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestGetAsyncPropertiesLookupID(t *testing.T) { + tests := map[string]struct { + request GetAsyncPropertiesLookupIDRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetAsyncPropertiesLookupIDResponse + withError func(*testing.T, error) + }{ + "200 OK": { + request: GetAsyncPropertiesLookupIDRequest{AccessKeyUID: 1234, Version: 1}, + responseStatus: http.StatusAccepted, + responseBody: ` +{ + "lookupId": 4321, + "retryAfter": 10 +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/1/property-lookup-id", + expectedResponse: &GetAsyncPropertiesLookupIDResponse{LookupID: 4321, RetryAfter: 10}, + }, + "404 incorrect request": { + request: GetAsyncPropertiesLookupIDRequest{AccessKeyUID: 1234, Version: 10}, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "/cam/error-types/access-key-version-does-not-exist", + "title": "Domain Error", + "instance": "60126c0d-67f5-473c-bea0-16daa836dc44", + "status": 404, + "detail": "Version '10' for access key '1234' does not exist.", + "problemId": "60126c0d-67f5-473c-bea0-16daa836dc44", + "version": 10, + "accessKeyUID": 1234 +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/10/property-lookup-id", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/cam/error-types/access-key-version-does-not-exist", + Title: "Domain Error", + Detail: "Version '10' for access key '1234' does not exist.", + Instance: "60126c0d-67f5-473c-bea0-16daa836dc44", + Status: 404, + ProblemID: "60126c0d-67f5-473c-bea0-16daa836dc44", + AccessKeyUID: 1234, + Version: 10, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "500 internal server error": { + request: GetAsyncPropertiesLookupIDRequest{AccessKeyUID: 1234, Version: 1}, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 +} +`, + expectedPath: "/cam/v1/access-keys/1234/versions/1/property-lookup-id", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "validate errors": { + request: GetAsyncPropertiesLookupIDRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get lookup properties id async: struct validation: AccessKeyUID: cannot be blank\nVersion: cannot be blank", err.Error()) + }, + }, + } + 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.GetAsyncPropertiesLookupID(context.Background(), test.request) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestPerformAsyncPropertiesLookup(t *testing.T) { + tests := map[string]struct { + request PerformAsyncPropertiesLookupRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *PerformAsyncPropertiesLookupResponse + withError func(*testing.T, error) + }{ + "200 OK empty": { + request: PerformAsyncPropertiesLookupRequest{LookupID: 4321}, + responseStatus: http.StatusOK, + responseBody: ` +{ + "lookupId": 4321, + "lookupStatus": "COMPLETE", + "properties": [] +} +`, + expectedPath: "/cam/v1/property-lookups/4321", + expectedResponse: &PerformAsyncPropertiesLookupResponse{ + LookupID: 4321, + LookupStatus: "COMPLETE", + Properties: []Property{}, + }, + }, + "200 OK with properties": { + request: PerformAsyncPropertiesLookupRequest{LookupID: 4321}, + responseStatus: http.StatusOK, + responseBody: ` +{ + "lookupId": 4321, + "lookupStatus": "COMPLETE", + "properties": [ + { + "accessKeyUid": 1234, + "version": 1, + "propertyId": "prp_5678", + "propertyName": "test-property", + "productionVersion": null, + "stagingVersion": 1 + }, + { + "accessKeyUid": 1234, + "version": 1, + "propertyId": "prp_6789", + "propertyName": "test-property2", + "productionVersion": 1, + "stagingVersion": null + } + ] +} +`, + expectedPath: "/cam/v1/property-lookups/4321", + expectedResponse: &PerformAsyncPropertiesLookupResponse{ + LookupID: 4321, + LookupStatus: LookupComplete, + Properties: []Property{ + { + AccessKeyUID: 1234, + Version: 1, + PropertyID: "prp_5678", + PropertyName: "test-property", + ProductionVersion: nil, + StagingVersion: tools.Int64Ptr(1), + }, + { + AccessKeyUID: 1234, + Version: 1, + PropertyID: "prp_6789", + PropertyName: "test-property2", + ProductionVersion: tools.Int64Ptr(1), + StagingVersion: nil, + }, + }, + }, + }, + "500 internal server error": { + request: PerformAsyncPropertiesLookupRequest{LookupID: 4321}, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal-server-error", + "title": "Internal Server Error", + "detail": "Error processing request", + "instance": "TestInstances", + "status": 500 + } + `, + expectedPath: "/cam/v1/property-lookups/4321", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal-server-error", + Title: "Internal Server Error", + Detail: "Error processing request", + Instance: "TestInstances", + Status: 500, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "validate errors": { + request: PerformAsyncPropertiesLookupRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "perform async lookup properties: struct validation: LookupID: cannot be blank", err.Error()) + }, + }, + } + 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.PerformAsyncPropertiesLookup(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/cloudaccess/testdata/AccessKey/CreateAccessKey.req.json b/pkg/cloudaccess/testdata/AccessKey/CreateAccessKey.req.json new file mode 100644 index 00000000..10f07e4b --- /dev/null +++ b/pkg/cloudaccess/testdata/AccessKey/CreateAccessKey.req.json @@ -0,0 +1,14 @@ +{ + "accessKeyName": "key1", + "authenticationMethod": "AWS4_HMAC_SHA256", + "contractID": "TestContractID", + "credentials": { + "cloudSecretAccessKey": "testKey", + "cloudAccessKeyID": "456" + }, + "groupID": 123, + "networkConfiguration": { + "additionalCDN": "CHINA_CDN", + "securityNetwork": "ENHANCED_TLS" + } +} \ No newline at end of file diff --git a/pkg/cloudaccess/testdata/AccessKey/GetAccessKey.resp.json b/pkg/cloudaccess/testdata/AccessKey/GetAccessKey.resp.json new file mode 100644 index 00000000..31992285 --- /dev/null +++ b/pkg/cloudaccess/testdata/AccessKey/GetAccessKey.resp.json @@ -0,0 +1,19 @@ +{ + "accessKeyName": "key1", + "AccessKeyUid": 1, + "authenticationMethod": "AWS4_HMAC_SHA256", + "createdBy": "user1", + "groups": [ + { + "contractIds": [ + "TestContractID" + ], + "groupId": 123 + } + ], + "latestVersion": 1, + "networkConfiguration": { + "additionalCdn": "RUSSIA_CDN", + "securityNetwork": "ENHANCED_TLS" + } +} \ No newline at end of file diff --git a/pkg/cloudaccess/testdata/AccessKey/ListAccessKey.resp.json b/pkg/cloudaccess/testdata/AccessKey/ListAccessKey.resp.json new file mode 100644 index 00000000..451a94da --- /dev/null +++ b/pkg/cloudaccess/testdata/AccessKey/ListAccessKey.resp.json @@ -0,0 +1,23 @@ +{ + "accessKeys": [ + { + "accessKeyName": "key1", + "AccessKeyUID": 1, + "authenticationMethod": "AWS4_HMAC_SHA256", + "createdBy": "user1", + "groups": [ + { + "contractIds": [ + "TestContractID" + ], + "groupId": 123 + } + ], + "latestVersion": 1, + "networkConfiguration": { + "additionalCdn": "RUSSIA_CDN", + "securityNetwork": "ENHANCED_TLS" + } + } + ] +} \ No newline at end of file diff --git a/pkg/cloudaccess/testdata/AccessKeyStatus/GetAccessKeyStatus.resp.json b/pkg/cloudaccess/testdata/AccessKeyStatus/GetAccessKeyStatus.resp.json new file mode 100644 index 00000000..e60bf148 --- /dev/null +++ b/pkg/cloudaccess/testdata/AccessKeyStatus/GetAccessKeyStatus.resp.json @@ -0,0 +1,25 @@ +{ + "accessKey": { + "accessKeyUid": 123, + "link": "/cam/v1/access-keys/123" + }, + "accessKeyVersion": { + "accessKeyUid": 123, + "link": "/cam/v1/access-keys/123/versions/1", + "version": 1 + }, + "processingStatus": "IN_PROGRESS", + "request": { + "accessKeyName": "TestAccessKeyName", + "authenticationMethod": "AWS4_HMAC_SHA256", + "contractId": "TestContractID", + "groupId": 123, + "networkConfiguration": { + "additionalCdn": "CHINA_CDN", + "securityNetwork": "ENHANCED_TLS" + } + }, + "requestDate": "2021-02-26T13:34:36.715643Z", + "requestId": 1, + "requestedBy": "user" +} \ No newline at end of file diff --git a/pkg/cloudaccess/testdata/AccessKeyStatus/GetAccessKeyStatusMinimal.resp.json b/pkg/cloudaccess/testdata/AccessKeyStatus/GetAccessKeyStatusMinimal.resp.json new file mode 100644 index 00000000..57073750 --- /dev/null +++ b/pkg/cloudaccess/testdata/AccessKeyStatus/GetAccessKeyStatusMinimal.resp.json @@ -0,0 +1,9 @@ +{ + "accessKey": null, + "accessKeyVersion": null, + "processingStatus": "IN_PROGRESS", + "request": null, + "requestDate": "2021-02-26T13:34:36.715643Z", + "requestId": 1, + "requestedBy": "user" +} \ No newline at end of file diff --git a/pkg/cloudlets/v3/policy_activation_test.go b/pkg/cloudlets/v3/policy_activation_test.go index b8a152c3..adb39289 100644 --- a/pkg/cloudlets/v3/policy_activation_test.go +++ b/pkg/cloudlets/v3/policy_activation_test.go @@ -7,8 +7,9 @@ import ( "net/http" "net/http/httptest" "testing" - "time" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/internal/test" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/ptr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +38,7 @@ func TestListPolicyActivations(t *testing.T) { PolicyActivations: []PolicyActivation{ { CreatedBy: "testUser", - CreatedDate: *newTimeFromString(t, "2023-10-25T10:33:47.982Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-25T10:33:47.982Z"), FinishDate: nil, ID: 234, Links: []Link{ @@ -55,8 +56,8 @@ func TestListPolicyActivations(t *testing.T) { }, { CreatedBy: "testUser", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 123, Links: []Link{ { @@ -299,7 +300,7 @@ func TestActivatePolicy(t *testing.T) { expectedPath: "/cloudlets/v3/policies/1234/activations", expectedResponse: &PolicyActivation{ CreatedBy: "testUser", - CreatedDate: *newTimeFromString(t, "2023-10-25T10:33:47.982Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-25T10:33:47.982Z"), FinishDate: nil, ID: 123, Links: []Link{ @@ -447,7 +448,7 @@ func TestDeactivatePolicy(t *testing.T) { expectedPath: "/cloudlets/v3/policies/1234/activations", expectedResponse: &PolicyActivation{ CreatedBy: "testUser", - CreatedDate: *newTimeFromString(t, "2023-10-25T10:33:47.982Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-25T10:33:47.982Z"), FinishDate: nil, ID: 123, Links: []Link{ @@ -596,8 +597,8 @@ func TestGetPolicyActivation(t *testing.T) { expectedPath: "/cloudlets/v3/policies/1234/activations/123", expectedResponse: &PolicyActivation{ CreatedBy: "testUser", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: activationID, Links: []Link{ { @@ -731,9 +732,3 @@ func TestGetPolicyActivation(t *testing.T) { }) } } - -func newTimeFromString(t *testing.T, s string) *time.Time { - parsedTime, err := time.Parse(time.RFC3339Nano, s) - require.NoError(t, err) - return &parsedTime -} diff --git a/pkg/cloudlets/v3/policy_test.go b/pkg/cloudlets/v3/policy_test.go index 5e0931a9..e6ceef4e 100644 --- a/pkg/cloudlets/v3/policy_test.go +++ b/pkg/cloudlets/v3/policy_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/internal/test" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/ptr" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -174,7 +176,7 @@ func TestListPolicies(t *testing.T) { { CloudletType: CloudletTypeCD, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: nil, GroupID: 1, @@ -186,20 +188,20 @@ func TestListPolicies(t *testing.T) { }, }, ModifiedBy: "User2", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "Name1", PolicyType: PolicyTypeShared, }, { CloudletType: CloudletTypeCD, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{ Production: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 123, Links: []Link{ { @@ -216,8 +218,8 @@ func TestListPolicies(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 321, Links: []Link{ { @@ -236,8 +238,8 @@ func TestListPolicies(t *testing.T) { Staging: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 789, Links: []Link{ { @@ -254,8 +256,8 @@ func TestListPolicies(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 987, Links: []Link{ { @@ -282,7 +284,7 @@ func TestListPolicies(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -359,7 +361,7 @@ func TestListPolicies(t *testing.T) { { CloudletType: CloudletTypeCD, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: nil, GroupID: 1, @@ -371,7 +373,7 @@ func TestListPolicies(t *testing.T) { }, }, ModifiedBy: "User2", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "Name1", PolicyType: PolicyTypeShared, }, @@ -545,7 +547,7 @@ func TestCreatePolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: nil, GroupID: 1, @@ -557,7 +559,7 @@ func TestCreatePolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -614,7 +616,7 @@ func TestCreatePolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: tools.StringPtr("Description"), GroupID: 1, @@ -626,7 +628,7 @@ func TestCreatePolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -771,7 +773,7 @@ func TestGetPolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: nil, GroupID: 1, @@ -783,7 +785,7 @@ func TestGetPolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -898,13 +900,13 @@ func TestGetPolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{ Production: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 123, Links: []Link{ { @@ -921,8 +923,8 @@ func TestGetPolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 321, Links: []Link{ { @@ -941,8 +943,8 @@ func TestGetPolicy(t *testing.T) { Staging: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 789, Links: []Link{ { @@ -959,8 +961,8 @@ func TestGetPolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 987, Links: []Link{ { @@ -987,7 +989,7 @@ func TestGetPolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -1068,7 +1070,7 @@ func TestGetPolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{ Production: ActivationInfo{ Effective: nil, @@ -1077,8 +1079,8 @@ func TestGetPolicy(t *testing.T) { Staging: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 789, Links: []Link{ { @@ -1095,8 +1097,8 @@ func TestGetPolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 987, Links: []Link{ { @@ -1123,7 +1125,7 @@ func TestGetPolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -1214,7 +1216,7 @@ func TestUpdatePolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: nil, GroupID: 1, @@ -1226,7 +1228,7 @@ func TestUpdatePolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -1348,13 +1350,13 @@ func TestUpdatePolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: CloudletTypeFR, CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{ Production: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 123, Links: []Link{ { @@ -1371,8 +1373,8 @@ func TestUpdatePolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 321, Links: []Link{ { @@ -1391,8 +1393,8 @@ func TestUpdatePolicy(t *testing.T) { Staging: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 789, Links: []Link{ { @@ -1409,8 +1411,8 @@ func TestUpdatePolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 987, Links: []Link{ { @@ -1437,7 +1439,7 @@ func TestUpdatePolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "TestName", PolicyType: PolicyTypeShared, }, @@ -1545,7 +1547,7 @@ func TestClonePolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: "FR", CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{}, Description: nil, GroupID: 1, @@ -1557,7 +1559,7 @@ func TestClonePolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "NewName", PolicyType: PolicyTypeShared, }, @@ -1681,13 +1683,13 @@ func TestClonePolicy(t *testing.T) { expectedResponse: &Policy{ CloudletType: "FR", CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), CurrentActivations: CurrentActivations{ Production: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 123, Links: []Link{ { @@ -1704,8 +1706,8 @@ func TestClonePolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User1", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 321, Links: []Link{ { @@ -1724,8 +1726,8 @@ func TestClonePolicy(t *testing.T) { Staging: ActivationInfo{ Effective: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 789, Links: []Link{ { @@ -1742,8 +1744,8 @@ func TestClonePolicy(t *testing.T) { }, Latest: &PolicyActivation{ CreatedBy: "User3", - CreatedDate: *newTimeFromString(t, "2023-10-23T11:21:19.896Z"), - FinishDate: newTimeFromString(t, "2023-10-23T11:22:57.589Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z"), + FinishDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:22:57.589Z")), ID: 987, Links: []Link{ { @@ -1770,7 +1772,7 @@ func TestClonePolicy(t *testing.T) { }, }, ModifiedBy: "User1", - ModifiedDate: newTimeFromString(t, "2023-10-23T11:21:19.896Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-23T11:21:19.896Z")), Name: "NewName", PolicyType: PolicyTypeShared, }, diff --git a/pkg/cloudlets/v3/policy_version_test.go b/pkg/cloudlets/v3/policy_version_test.go index 4f45a457..82aeb101 100644 --- a/pkg/cloudlets/v3/policy_version_test.go +++ b/pkg/cloudlets/v3/policy_version_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/internal/test" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/ptr" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/tools" "github.com/stretchr/testify/require" "github.com/tj/assert" @@ -87,37 +89,37 @@ func TestListPolicyVersions(t *testing.T) { PolicyVersions: []ListPolicyVersionsItem{ { CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), Description: nil, ID: 6551184, Immutable: false, Links: []Link{}, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 670790, PolicyVersion: 3, }, { CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:46.225Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:46.225Z"), Description: nil, ID: 6551183, Immutable: false, Links: []Link{}, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:46.225Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:46.225Z")), PolicyID: 670790, PolicyVersion: 2, }, { CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T08:44:34.398Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:44:34.398Z"), Description: nil, ID: 6551182, Immutable: false, Links: []Link{}, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:44:34.398Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:44:34.398Z")), PolicyID: 670790, PolicyVersion: 1, }, @@ -279,13 +281,13 @@ func TestGetPolicyVersion(t *testing.T) { expectedPath: "/cloudlets/v3/policies/670798/versions/2", expectedResponse: &PolicyVersion{ CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z"), Description: tools.StringPtr("test description"), ID: 6551191, MatchRules: nil, MatchRulesWarnings: []MatchRulesWarning{}, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z")), PolicyID: 670798, PolicyVersion: 2, }, @@ -326,7 +328,7 @@ func TestGetPolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions/1", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T10:45:30.619Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T10:45:30.619Z"), CreatedBy: "jsmith", Description: nil, PolicyID: 276858, @@ -349,7 +351,7 @@ func TestGetPolicyVersion(t *testing.T) { }, MatchRulesWarnings: []MatchRulesWarning{}, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T10:45:30.619Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T10:45:30.619Z")), }, }, "200 OK, AS rule with disabled=false": { @@ -400,7 +402,7 @@ func TestGetPolicyVersion(t *testing.T) { expectedPath: "/cloudlets/v3/policies/355557/versions/2", expectedResponse: &PolicyVersion{ CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z"), Description: tools.StringPtr("Initial version"), MatchRules: MatchRules{ &MatchRuleAS{ @@ -430,7 +432,7 @@ func TestGetPolicyVersion(t *testing.T) { }, }, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T10:45:30.619Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T10:45:30.619Z")), PolicyID: 355557, PolicyVersion: 2, }, @@ -481,10 +483,10 @@ func TestGetPolicyVersion(t *testing.T) { expectedPath: "/cloudlets/v3/policies/276858/versions/6", expectedResponse: &PolicyVersion{ CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z"), Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z")), MatchRules: MatchRules{ &MatchRulePR{ Type: "cdMatchRule", @@ -547,11 +549,11 @@ func TestGetPolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions/6", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T09:46:57.395Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T09:46:57.395Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -604,12 +606,12 @@ func TestGetPolicyVersion(t *testing.T) { expectedPath: "/cloudlets/v3/policies/276858/versions/6", expectedResponse: &PolicyVersion{ CreatedBy: "jsmith", - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), Description: nil, PolicyID: 276858, PolicyVersion: 6, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), MatchRules: MatchRules{ &MatchRuleFR{ Name: "rule 1", @@ -706,11 +708,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: tools.StringPtr("Description for the policy"), ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), MatchRules: nil, PolicyID: 276858, PolicyVersion: 2, @@ -846,11 +848,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/355557/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: tools.StringPtr("Initial version"), ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), MatchRules: MatchRules{ &MatchRuleAS{ Type: "asMatchRule", @@ -1050,11 +1052,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -1185,11 +1187,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -1304,11 +1306,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -1584,11 +1586,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -1727,11 +1729,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -1846,11 +1848,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -1977,11 +1979,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -2201,11 +2203,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -2355,11 +2357,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/139743/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: tools.StringPtr("New version 1630480693371"), ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), MatchRules: MatchRules{ &MatchRuleFR{ ForwardSettings: ForwardSettingsFR{}, @@ -2468,11 +2470,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/139743/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: tools.StringPtr("New version 1630480693371"), ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), MatchRules: MatchRules{ &MatchRuleFR{ ForwardSettings: ForwardSettingsFR{ @@ -2568,11 +2570,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -2673,7 +2675,7 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", @@ -2822,11 +2824,11 @@ func TestCreatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: nil, ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), PolicyID: 276858, PolicyVersion: 6, MatchRules: MatchRules{ @@ -3142,11 +3144,11 @@ func TestUpdatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions/5", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z"), CreatedBy: "jsmith", Description: tools.StringPtr("Updated description"), ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-19T08:50:47.350Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-19T08:50:47.350Z")), MatchRules: nil, PolicyID: 276858, PolicyVersion: 5, @@ -3221,11 +3223,11 @@ func TestUpdatePolicyVersion(t *testing.T) { }`, expectedPath: "/cloudlets/v3/policies/276858/versions/5", expectedResponse: &PolicyVersion{ - CreatedDate: *newTimeFromString(t, "2023-10-20T09:21:04.180Z"), + CreatedDate: test.NewTimeFromString(t, "2023-10-20T09:21:04.180Z"), CreatedBy: "jsmith", Description: tools.StringPtr("Updated description"), ModifiedBy: "jsmith", - ModifiedDate: newTimeFromString(t, "2023-10-20T10:32:56.316Z"), + ModifiedDate: ptr.To(test.NewTimeFromString(t, "2023-10-20T10:32:56.316Z")), ID: 6552305, MatchRules: MatchRules{ &MatchRuleER{ diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index 7f3b3316..bbb72ac5 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -4,11 +4,17 @@ package dns import ( + "errors" "net/http" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" ) +var ( + // ErrStructValidation is returned when given struct validation failed + ErrStructValidation = errors.New("struct validation") +) + type ( // DNS is the dns api interface DNS interface { diff --git a/pkg/dns/mocks.go b/pkg/dns/mocks.go index cd614228..e9998be0 100644 --- a/pkg/dns/mocks.go +++ b/pkg/dns/mocks.go @@ -30,6 +30,16 @@ func (d *Mock) ListZones(ctx context.Context, query ...ZoneListQueryArgs) (*Zone return args.Get(0).(*ZoneListResponse), args.Error(1) } +func (d *Mock) GetZonesDNSSecStatus(ctx context.Context, params GetZonesDNSSecStatusRequest) (*GetZonesDNSSecStatusResponse, error) { + args := d.Called(ctx, params) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetZonesDNSSecStatusResponse), args.Error(1) +} + func (d *Mock) GetZone(ctx context.Context, name string) (*ZoneResponse, error) { args := d.Called(ctx, name) diff --git a/pkg/dns/zone.go b/pkg/dns/zone.go index 5b567b89..79ab0538 100644 --- a/pkg/dns/zone.go +++ b/pkg/dns/zone.go @@ -1,18 +1,21 @@ package dns import ( + "bytes" "context" + "encoding/json" "fmt" "io" "io/ioutil" "net/http" - - "bytes" - "encoding/json" "reflect" "strconv" "strings" "sync" + "time" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" ) var ( @@ -90,6 +93,10 @@ type ( // // See: https://techdocs.akamai.com/edge-dns/reference/get-zones-delete-requests-requestid-result GetBulkZoneDeleteResult(context.Context, string) (*BulkDeleteResultResponse, error) + // GetZonesDNSSecStatus returns the current DNSSEC status for one or more zones. + // + // See: https://techdocs.akamai.com/edge-dns/reference/post-zones-dns-sec-status + GetZonesDNSSecStatus(context.Context, GetZonesDNSSecStatusRequest) (*GetZonesDNSSecStatusResponse, error) } // ZoneQueryString contains zone query parameters @@ -182,8 +189,42 @@ type ( ZoneNameTypesResponse struct { Types []string `json:"types"` } + + // GetZonesDNSSecStatusRequest is used to get the DNSSEC status for one or more zones + GetZonesDNSSecStatusRequest struct { + Zones []string `json:"zones"` + } + + // GetZonesDNSSecStatusResponse represents a list of DNSSEC statuses for DNS zones specified + // in the GetZonesDNSSecStatus request + GetZonesDNSSecStatusResponse struct { + DNSSecStatuses []SecStatus `json:"dnsSecStatuses"` + } + + // SecStatus represents the DNSSEC status for a DNS zone + SecStatus struct { + Zone string `json:"zone"` + Alerts []string `json:"alerts"` + CurrentRecords SecRecords `json:"currentRecords"` + NewRecords *SecRecords `json:"newRecords"` + } + + // SecRecords represents a set of DNSSEC records for a DNS zone + SecRecords struct { + DNSKeyRecord string `json:"dnskeyRecord"` + DSRecord string `json:"dsRecord"` + ExpectedTTL int64 `json:"expectedTtl"` + LastModifiedDate time.Time `json:"lastModifiedDate"` + } ) +// Validate validates GetZonesDNSSecStatusRequest +func (r GetZonesDNSSecStatusRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "Zones": validation.Validate(r.Zones, validation.Required), + }) +} + var zoneStructMap = map[string]string{ "Zone": "zone", "Type": "type", @@ -648,3 +689,29 @@ func (d *dns) GetZoneNameTypes(ctx context.Context, zName, zone string) (*ZoneNa return &result, nil } + +func (d *dns) GetZonesDNSSecStatus(ctx context.Context, params GetZonesDNSSecStatusRequest) (*GetZonesDNSSecStatusResponse, error) { + logger := d.Log(ctx) + logger.Debug("GetZonesDNSSecStatus") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/config-dns/v2/zones/dns-sec-status", nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetZonesDNSSecStatus request: %w", err) + } + + var result GetZonesDNSSecStatusResponse + resp, err := d.Exec(req, &result, params) + if err != nil { + return nil, fmt.Errorf("GetZonesDNSSecStatus request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, d.Error(resp) + } + + return &result, nil +} diff --git a/pkg/dns/zone_test.go b/pkg/dns/zone_test.go index e9e9b0e6..8a66ca9f 100644 --- a/pkg/dns/zone_test.go +++ b/pkg/dns/zone_test.go @@ -3,10 +3,12 @@ package dns import ( "context" "errors" + "io/ioutil" "net/http" "net/http/httptest" "testing" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/internal/test" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -141,6 +143,164 @@ func TestDNS_ListZones(t *testing.T) { } } +func TestDNS_GetZonesDNSSecStatus(t *testing.T) { + + tests := map[string]struct { + zones []string + responseStatus int + responseBody string + expectedPath string + expectedRequestBody string + expectedResponse *GetZonesDNSSecStatusResponse + withError error + headers http.Header + }{ + "200 OK current records only": { + zones: []string{"foo.test.net"}, + headers: http.Header{ + "Accept": []string{"application/json"}, + }, + responseStatus: http.StatusOK, + responseBody: ` + { + "dnsSecStatuses": [ + { + "zone": "foo.test.net", + "alerts": [ + "PARENT_DS_MISSING" + ], + "currentRecords": { + "dsRecord": "foo.test.net. 86400 IN DS 42061 7 2 ( DUMMY_HASH_1 ) ", + "dnskeyRecord": "foo.test.net. 7200 IN DNSKEY 257 3 7 (DUMMY_HASH_2 ) ", + "lastModifiedDate": "2024-05-28T06:58:26Z", + "expectedTtl": 0 + } + } + ] + }`, + expectedPath: "/config-dns/v2/zones/dns-sec-status", + expectedRequestBody: `{"zones":["foo.test.net"]}`, + expectedResponse: &GetZonesDNSSecStatusResponse{ + DNSSecStatuses: []SecStatus{{ + Zone: "foo.test.net", + Alerts: []string{"PARENT_DS_MISSING"}, + CurrentRecords: SecRecords{ + DNSKeyRecord: "foo.test.net. 7200 IN DNSKEY 257 3 7 (DUMMY_HASH_2 ) ", + DSRecord: "foo.test.net. 86400 IN DS 42061 7 2 ( DUMMY_HASH_1 ) ", + ExpectedTTL: 0, + LastModifiedDate: test.NewTimeFromString(t, "2024-05-28T06:58:26Z"), + }, + }}, + }, + }, + "200 OK new records returned": { + zones: []string{"foo.test.net"}, + headers: http.Header{ + "Accept": []string{"application/json"}, + }, + responseStatus: http.StatusOK, + responseBody: ` + { + "dnsSecStatuses": [ + { + "alerts": [ + "PARENT_DS_MISSING" + ], + "currentRecords": { + "dnskeyRecord": "foo.test.net. 7200 IN DNSKEY 257 3 13 (DUMMY_HASH_1 ) ", + "dsRecord": "foo.test.net. 86400 IN DS 3622 13 2 ( DUMMY_HASH_2 ) ", + "expectedTtl": 3600, + "lastModifiedDate": "2022-06-19T10:14:35Z" + }, + "newRecords": { + "dnskeyRecord": "foo.test.net. 7200 IN DNSKEY 257 3 13 (DUMMY_HASH_3 ) ", + "dsRecord": "foo.test.net. 86400 IN DS 39035 13 2 ( DUMMY_HASH_4 ) ", + "expectedTtl": 3600, + "lastModifiedDate": "2023-06-19T10:14:35Z" + }, + "zone": "foo.test.net" + } + ] + }`, + expectedPath: "/config-dns/v2/zones/dns-sec-status", + expectedRequestBody: `{"zones":["foo.test.net"]}`, + expectedResponse: &GetZonesDNSSecStatusResponse{ + DNSSecStatuses: []SecStatus{{ + Zone: "foo.test.net", + Alerts: []string{"PARENT_DS_MISSING"}, + CurrentRecords: SecRecords{ + DNSKeyRecord: "foo.test.net. 7200 IN DNSKEY 257 3 13 (DUMMY_HASH_1 ) ", + DSRecord: "foo.test.net. 86400 IN DS 3622 13 2 ( DUMMY_HASH_2 ) ", + ExpectedTTL: 3600, + LastModifiedDate: test.NewTimeFromString(t, "2022-06-19T10:14:35Z"), + }, + NewRecords: &SecRecords{ + DNSKeyRecord: "foo.test.net. 7200 IN DNSKEY 257 3 13 (DUMMY_HASH_3 ) ", + DSRecord: "foo.test.net. 86400 IN DS 39035 13 2 ( DUMMY_HASH_4 ) ", + ExpectedTTL: 3600, + LastModifiedDate: test.NewTimeFromString(t, "2023-06-19T10:14:35Z"), + }, + }}, + }, + }, + "500 internal server error": { + zones: []string{"foo.test.net"}, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "https://problems.luna.akamaiapis.net/authoritative-dns/serverError", + "title": "Server error", + "instance": "29aa48de-ec7d-4214-ad6c-649163889be7", + "status": 500, + "detail": "An internal error occurred.", + "problemId": "29aa48de-ec7d-4214-ad6c-649163889be7" + }`, + expectedPath: "/config-dns/v2/zones/dns-sec-status", + expectedRequestBody: `{"zones":["foo.test.net"]}`, + withError: &Error{ + Type: "https://problems.luna.akamaiapis.net/authoritative-dns/serverError", + Title: "Server error", + Detail: "An internal error occurred.", + StatusCode: http.StatusInternalServerError, + }, + }, + "validation error: empty zone list": { + zones: []string{}, + 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) + if len(test.expectedRequestBody) > 0 { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(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.GetZonesDNSSecStatus( + session.ContextWithOptions( + context.Background(), + session.WithContextHeaders(test.headers)), + GetZonesDNSSecStatusRequest{ + Zones: test.zones}) + 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 TestDNS_GetZone(t *testing.T) { tests := map[string]struct { zone string diff --git a/pkg/hapi/edgehostname_test.go b/pkg/hapi/edgehostname_test.go index 6cbaaaca..e3030aed 100644 --- a/pkg/hapi/edgehostname_test.go +++ b/pkg/hapi/edgehostname_test.go @@ -7,8 +7,8 @@ import ( "net/http" "net/http/httptest" "testing" - "time" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/internal/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -488,7 +488,7 @@ func TestGetCertificate(t *testing.T) { CommonName: "example.com", SerialNumber: "12:34:56:78:90:AB:CD:EF", SlotNumber: 8927, - ExpirationDate: *newTimeFromString(t, "2019-10-31T23:59:59Z"), + ExpirationDate: test.NewTimeFromString(t, "2019-10-31T23:59:59Z"), CertificateType: "SAN", ValidationType: "DOMAIN_VALIDATION", Status: "PENDING", @@ -577,9 +577,3 @@ func TestGetCertificate(t *testing.T) { }) } } - -func newTimeFromString(t *testing.T, s string) *time.Time { - parsedTime, err := time.Parse(time.RFC3339Nano, s) - require.NoError(t, err) - return &parsedTime -} diff --git a/pkg/ptr/ptr.go b/pkg/ptr/ptr.go new file mode 100644 index 00000000..bfa14172 --- /dev/null +++ b/pkg/ptr/ptr.go @@ -0,0 +1,7 @@ +// Package ptr helps with creating pointers to values of any type. +package ptr + +// To returns a pointer to a copy of a value of any type. +func To[T any](t T) *T { + return &t +} diff --git a/pkg/tools/ptr.go b/pkg/tools/ptr.go index 12f15c93..d7cfc0b7 100644 --- a/pkg/tools/ptr.go +++ b/pkg/tools/ptr.go @@ -2,31 +2,43 @@ package tools // BoolPtr returns the address of the bool +// +// Deprecated: this function will be removed in a future release. Use [ptr.To] instead. func BoolPtr(b bool) *bool { return &b } // IntPtr returns the address of the int +// +// Deprecated: this function will be removed in a future release. Use [ptr.To] instead. func IntPtr(i int) *int { return &i } // Int64Ptr returns the address of the int64 +// +// Deprecated: this function will be removed in a future release. Use [ptr.To] instead. func Int64Ptr(i int64) *int64 { return &i } // Float32Ptr returns the address of the float32 +// +// Deprecated: this function will be removed in a future release. Use [ptr.To] instead. func Float32Ptr(f float32) *float32 { return &f } // Float64Ptr returns the address of the float64 +// +// Deprecated: this function will be removed in a future release. Use [ptr.To] instead. func Float64Ptr(f float64) *float64 { return &f } // StringPtr returns the address of the string +// +// Deprecated: this function will be removed in a future release. Use [ptr.To] instead. func StringPtr(s string) *string { return &s }