diff --git a/CHANGELOG.md b/CHANGELOG.md index 47dc7eb5..ca34b324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # EDGEGRID GOLANG RELEASE NOTES +## 8.2.0 (May 21, 2024) + +#### FEATURES/ENHANCEMENTS: + +* APPSEC + * Added `CounterType` field to `CreateRatePolicyResponse`, `UpdateRatePolicyResponse`, `RemoveRatePolicyResponse`, `GetRatePoliciesResponse` and `GetRatePolicyResponse` structs to support managing rate policy counter type + +* BOTMAN + * Added [GetCustomBotCategoryItemSequence](https://techdocs.akamai.com/bot-manager/reference/get-custom-bot-category-item-sequence) and [UpdateCustomBotCategoryItemSequence](https://techdocs.akamai.com/bot-manager/reference/put-custom-bot-category-item-sequence) + +* HAPI + * Added method to return certificate for the edge hostname + * [GetCertificate](https://techdocs.akamai.com/edge-hostnames/reference/get-edge-hostname-certificate) + * Added fields to `GetEdgeHostnameResponse`: `ProductID`, `MapAlias` and `UseCases` + +#### BUG FIXES: + +* APPSEC + * The `Override` field in the following structs has been updated from a pointer to a value type within the `AdvancedSettingsAttackPayloadLogging` interface: + * `GetAdvancedSettingsAttackPayloadLoggingResponse`, + * `UpdateAdvancedSettingsAttackPayloadLoggingResponse`, + * `RemoveAdvancedSettingsAttackPayloadLoggingRequest`, + * `RemoveAdvancedSettingsAttackPayloadLoggingResponse` + This update was made to address a drift issue related to policy level settings. + * Omit `Prefetch` within `AdvancedOptions` in `GetExportConfigurationResponse` when empty + +* CLOUDLETS + * Added validation that `ObjectMatchValue` is not supported with `MatchType` `query` in `MatchRuleER` ([#535](https://github.com/akamai/terraform-provider-akamai/issues/535)) + ## 8.1.0 (April 11, 2024) #### FEATURES/ENHANCEMENTS: @@ -33,7 +62,7 @@ * `Gid` into `GID` in `TSIGQueryString` and `TSIGReportMeta` * `TsigKey` into `TSIGKey` in `ZoneCreate` and `ZoneResponse` * `VersionId` into `VersionID` in `ZoneResponse` - * `RequestId` into `RequestID` in `BulkZonesResponse`, `BulkStatusResponse`, `BulkCreateResultResponse` and `BulkDeleteResultResponse` + * `RequestId` into `RequestID` in `BulkZonesResponse`, `BulkStatusResponse`, `BulkCreateResultResponse` and `BulkDeleteResultResponse` * Renamed `RecordSets` interface into `Recordsets` * Renamed following methods: * `ListTsigKeys` into `ListTSIGKeys` @@ -150,7 +179,7 @@ * DNS * Removed not working `DeleteZone` method -* +* * PAPI * Updated documentation link for `GetProperties` method @@ -159,7 +188,7 @@ #### BUG FIXES: * Edgeworkers - * Fixed case when not providing optional `note` field in `ActivateVersion` would cause activation to fail + * Fixed case when not providing optional `note` field in `ActivateVersion` would cause activation to fail ## 7.6.0 (February 8, 2024) diff --git a/pkg/appsec/advanced_settings_attack_payload_logging.go b/pkg/appsec/advanced_settings_attack_payload_logging.go index 8698acd2..539bfb22 100644 --- a/pkg/appsec/advanced_settings_attack_payload_logging.go +++ b/pkg/appsec/advanced_settings_attack_payload_logging.go @@ -45,7 +45,7 @@ type ( // GetAdvancedSettingsAttackPayloadLoggingResponse is returned from a call to GetAdvancedSettingsAttackPayloadLogging. GetAdvancedSettingsAttackPayloadLoggingResponse struct { - Override *bool `json:"override,omitempty"` + Override bool `json:"override"` Enabled bool `json:"enabled"` RequestBody AttackPayloadLoggingRequestBody `json:"requestBody"` ResponseBody AttackPayloadLoggingResponseBody `json:"responseBody"` @@ -71,7 +71,7 @@ type ( // UpdateAdvancedSettingsAttackPayloadLoggingResponse is returned from a call to UpdateAdvancedSettingsAttackPayloadLogging. UpdateAdvancedSettingsAttackPayloadLoggingResponse struct { - Override *bool `json:"override,omitempty"` + Override bool `json:"override"` Enabled bool `json:"enabled"` RequestBody AttackPayloadLoggingRequestBody `json:"requestBody"` ResponseBody AttackPayloadLoggingResponseBody `json:"responseBody"` @@ -90,7 +90,7 @@ type ( // RemoveAdvancedSettingsAttackPayloadLoggingResponse is returned from a call to RemoveAdvancedSettingsAttackPayloadLogging. RemoveAdvancedSettingsAttackPayloadLoggingResponse struct { - Override *bool `json:"override,omitempty"` + Override bool `json:"override"` Enabled bool `json:"enabled"` RequestBody AttackPayloadLoggingRequestBody `json:"requestBody"` ResponseBody AttackPayloadLoggingResponseBody `json:"responseBody"` diff --git a/pkg/appsec/advanced_settings_attack_payload_logging_test.go b/pkg/appsec/advanced_settings_attack_payload_logging_test.go index 19e70960..d2aabdb9 100644 --- a/pkg/appsec/advanced_settings_attack_payload_logging_test.go +++ b/pkg/appsec/advanced_settings_attack_payload_logging_test.go @@ -17,7 +17,7 @@ func TestAppSec_ListAdvancedSettingsAttackPayloadLogging(t *testing.T) { result := GetAdvancedSettingsAttackPayloadLoggingResponse{} - respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLogging.json")) + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingConfig.json")) err := json.Unmarshal([]byte(respData), &result) require.NoError(t, err) @@ -98,7 +98,7 @@ func TestAppSec_GetAdvancedSettingsAttackPayloadLoggingPolicy(t *testing.T) { result := GetAdvancedSettingsAttackPayloadLoggingResponse{} - respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLogging.json")) + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingPolicy.json")) err := json.Unmarshal([]byte(respData), &result) require.NoError(t, err) @@ -169,13 +169,13 @@ func TestAppSec_GetAdvancedSettingsAttackPayloadLoggingPolicy(t *testing.T) { func TestAppSec_UpdateAdvancedSettingsAttackPayloadLogging(t *testing.T) { result := UpdateAdvancedSettingsAttackPayloadLoggingResponse{} - respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsLogging/AdvancedSettingsLogging.json")) + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingConfig.json")) err := json.Unmarshal([]byte(respData), &result) require.NoError(t, err) req := UpdateAdvancedSettingsAttackPayloadLoggingRequest{} - reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsLogging/AdvancedSettingsLogging.json")) + reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingConfig.json")) err = json.Unmarshal([]byte(reqData), &req) require.NoError(t, err) @@ -252,13 +252,13 @@ func TestAppSec_UpdateAdvancedSettingsAttackPayloadLogging(t *testing.T) { func TestAppSec_UpdateAdvancedSettingsAttackPayloadLoggingPolicy(t *testing.T) { result := UpdateAdvancedSettingsAttackPayloadLoggingResponse{} - respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsLogging/AdvancedSettingsLogging.json")) + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingPolicy.json")) err := json.Unmarshal([]byte(respData), &result) require.NoError(t, err) req := UpdateAdvancedSettingsAttackPayloadLoggingRequest{} - reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsLogging/AdvancedSettingsLogging.json")) + reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingPolicy.json")) err = json.Unmarshal([]byte(reqData), &req) require.NoError(t, err) diff --git a/pkg/appsec/export_configuration.go b/pkg/appsec/export_configuration.go index beed2b33..dd946bc8 100644 --- a/pkg/appsec/export_configuration.go +++ b/pkg/appsec/export_configuration.go @@ -503,18 +503,13 @@ type ( // AdvancedOptionsexp is returned as part of GetExportConfigurationResponse. AdvancedOptionsexp struct { - Logging *Loggingexp `json:"logging"` - AttackPayloadLogging *AttackPayloadLogging `json:"attackPayloadLogging"` - EvasivePathMatch *EvasivePathMatchexp `json:"evasivePathMatch,omitempty"` - Prefetch struct { - AllExtensions bool `json:"allExtensions"` - EnableAppLayer bool `json:"enableAppLayer"` - EnableRateControls bool `json:"enableRateControls"` - Extensions []string `json:"extensions,omitempty"` - } `json:"prefetch"` - PragmaHeader *GetAdvancedSettingsPragmaResponse `json:"pragmaHeader,omitempty"` - RequestBody *RequestBody `json:"requestBody,omitempty"` - PIILearning *PIILearningexp `json:"piiLearning,omitempty"` + Logging *Loggingexp `json:"logging"` + AttackPayloadLogging *AttackPayloadLogging `json:"attackPayloadLogging"` + EvasivePathMatch *EvasivePathMatchexp `json:"evasivePathMatch,omitempty"` + Prefetch *Prefetch `json:"prefetch"` + PragmaHeader *GetAdvancedSettingsPragmaResponse `json:"pragmaHeader,omitempty"` + RequestBody *RequestBody `json:"requestBody,omitempty"` + PIILearning *PIILearningexp `json:"piiLearning,omitempty"` } // CustomDenyListexp is returned as part of GetExportConfigurationResponse. @@ -689,6 +684,14 @@ type ( EnablePIILearning bool `json:"enabled"` } + // Prefetch is returned as part of AdvancedOptionsexp + Prefetch struct { + AllExtensions bool `json:"allExtensions"` + EnableAppLayer bool `json:"enableAppLayer"` + EnableRateControls bool `json:"enableRateControls"` + Extensions []string `json:"extensions,omitempty"` + } + // RequestBody is returned as part of GetExportConfigurationResponse. RequestBody struct { RequestBodyInspectionLimitInKB string `json:"requestBodyInspectionLimitInKB"` diff --git a/pkg/appsec/rate_policy.go b/pkg/appsec/rate_policy.go index 94997e89..6c73a1b4 100644 --- a/pkg/appsec/rate_policy.go +++ b/pkg/appsec/rate_policy.go @@ -82,9 +82,10 @@ type ( PositiveMatch bool `json:"positiveMatch"` ValueInRange bool `json:"valueInRange"` } `json:"queryParameters"` - CreateDate string `json:"-"` - UpdateDate string `json:"-"` - Used json.RawMessage `json:"used"` + CreateDate string `json:"-"` + UpdateDate string `json:"-"` + Used json.RawMessage `json:"used"` + CounterType string `json:"counterType"` } // UpdateRatePolicyRequest is used to modify an existing rate policy. @@ -131,9 +132,10 @@ type ( PositiveMatch bool `json:"positiveMatch"` ValueInRange bool `json:"valueInRange"` } `json:"queryParameters"` - CreateDate string `json:"-"` - UpdateDate string `json:"-"` - Used json.RawMessage `json:"used"` + CreateDate string `json:"-"` + UpdateDate string `json:"-"` + Used json.RawMessage `json:"used"` + CounterType string `json:"counterType"` } // RemoveRatePolicyRequest is used to remove a rate policy. @@ -179,9 +181,10 @@ type ( PositiveMatch bool `json:"positiveMatch"` ValueInRange bool `json:"valueInRange"` } `json:"queryParameters"` - CreateDate string `json:"-"` - UpdateDate string `json:"-"` - Used json.RawMessage `json:"used"` + CreateDate string `json:"-"` + UpdateDate string `json:"-"` + Used json.RawMessage `json:"used"` + CounterType string `json:"counterType"` } // GetRatePoliciesRequest is used to retrieve the rate policies for a configuration. @@ -223,6 +226,7 @@ type ( SameActionOnIpv bool `json:"sameActionOnIpv"` APISelectors *RatePolicyAPISelectors `json:"apiSelectors,omitempty"` BodyParameters *RatePolicyBodyParameters `json:"bodyParameters,omitempty"` + CounterType string `json:"counterType"` } `json:"ratePolicies,omitempty"` } @@ -261,6 +265,7 @@ type ( CreateDate string `json:"-"` UpdateDate string `json:"-"` Used bool `json:"-"` + CounterType string `json:"counterType"` } // RatePolicyAPISelectors is used as part of a rate policy description. diff --git a/pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingConfig.json b/pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingConfig.json new file mode 100644 index 00000000..5472aa9d --- /dev/null +++ b/pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingConfig.json @@ -0,0 +1,9 @@ +{ + "enabled": true, + "requestBody": { + "type": "NONE" + }, + "responseBody": { + "type": "ATTACK_PAYLOAD" + } +} \ No newline at end of file diff --git a/pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLogging.json b/pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingPolicy.json similarity index 100% rename from pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLogging.json rename to pkg/appsec/testdata/TestAdvancedSettingsAttackPayloadLogging/AdvancedSettingsAttackPayloadLoggingPolicy.json diff --git a/pkg/appsec/testdata/TestRatePolicies/RatePolicies.json b/pkg/appsec/testdata/TestRatePolicies/RatePolicies.json index b031e3e2..89849d92 100644 --- a/pkg/appsec/testdata/TestRatePolicies/RatePolicies.json +++ b/pkg/appsec/testdata/TestRatePolicies/RatePolicies.json @@ -24,6 +24,7 @@ "clientIdentifier": "ip", "createDate": "2020-10-29T04:00:22Z", "description": "AFW Test Extensions", + "counterType": "per_edge", "fileExtensions": { "positiveMatch": false, "values": [ diff --git a/pkg/appsec/testdata/TestRatePolicies/RatePolicy.json b/pkg/appsec/testdata/TestRatePolicies/RatePolicy.json index f2703d31..0e8c79ed 100644 --- a/pkg/appsec/testdata/TestRatePolicies/RatePolicy.json +++ b/pkg/appsec/testdata/TestRatePolicies/RatePolicy.json @@ -22,6 +22,7 @@ "clientIdentifier": "ip", "createDate": "2020-10-29T04:00:22Z", "description": "AFW Test Extensions", + "counterType": "per_edge", "fileExtensions": { "positiveMatch": false, "values": [ diff --git a/pkg/botman/botman.go b/pkg/botman/botman.go index 5c062114..0cde2b6e 100644 --- a/pkg/botman/botman.go +++ b/pkg/botman/botman.go @@ -32,6 +32,7 @@ type ( ConditionalAction CustomBotCategory CustomBotCategoryAction + CustomBotCategoryItemSequence CustomBotCategorySequence CustomClient CustomClientSequence diff --git a/pkg/botman/custom_bot_category_item_sequence.go b/pkg/botman/custom_bot_category_item_sequence.go new file mode 100644 index 00000000..1d18cfc7 --- /dev/null +++ b/pkg/botman/custom_bot_category_item_sequence.go @@ -0,0 +1,129 @@ +package botman + +import ( + "context" + "fmt" + "net/http" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // The CustomBotCategoryItemSequence interface supports retrieving and updating custom bot category item sequence + CustomBotCategoryItemSequence interface { + // GetCustomBotCategoryItemSequence https://techdocs.akamai.com/bot-manager/reference/get-custom-bot-category-item-sequence + GetCustomBotCategoryItemSequence(ctx context.Context, params GetCustomBotCategoryItemSequenceRequest) (*GetCustomBotCategoryItemSequenceResponse, error) + // UpdateCustomBotCategoryItemSequence https://techdocs.akamai.com/bot-manager/reference/put-custom-bot-category-item-sequence + UpdateCustomBotCategoryItemSequence(ctx context.Context, params UpdateCustomBotCategoryItemSequenceRequest) (*UpdateCustomBotCategoryItemSequenceResponse, error) + } + + // GetCustomBotCategoryItemSequenceRequest is used to retrieve custom bot category sequence + GetCustomBotCategoryItemSequenceRequest struct { + ConfigID int64 + Version int64 + CategoryID string + } + + // GetCustomBotCategoryItemSequenceResponse contains the sequence of botIds + GetCustomBotCategoryItemSequenceResponse UUIDSequence + + // UpdateCustomBotCategoryItemSequenceResponse contains the sequence of botIds + UpdateCustomBotCategoryItemSequenceResponse UUIDSequence + + // UpdateCustomBotCategoryItemSequenceRequest is used to update custom bot category item sequence + UpdateCustomBotCategoryItemSequenceRequest struct { + ConfigID int64 + Version int64 + CategoryID string + Sequence UUIDSequence + } + + // UUIDSequence is nothing more than a sequence of UUIDs, in this case bots, but this could be reused. + UUIDSequence struct { + Sequence []string `json:"sequence"` + } +) + +// Validate validates a GetCustomBotCategoryItemSequenceRequest. +func (v GetCustomBotCategoryItemSequenceRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "CategoryID": validation.Validate(v.CategoryID, validation.Required), + }) +} + +// Validate validates an UpdateCustomBotCategoryItemSequenceRequest. +func (v UpdateCustomBotCategoryItemSequenceRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "CategoryID": validation.Validate(v.CategoryID, validation.Required), + "Sequence": validation.Validate(v.Sequence.Sequence, validation.Required), + }) +} + +func (b *botman) GetCustomBotCategoryItemSequence(ctx context.Context, params GetCustomBotCategoryItemSequenceRequest) (*GetCustomBotCategoryItemSequenceResponse, error) { + logger := b.Log(ctx) + logger.Debug("GetCustomBotCategoryItemSequence") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/custom-bot-categories/%s/custom-bot-category-item-sequence", + params.ConfigID, + params.Version, + params.CategoryID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetCustomBotCategoryItemSequence request: %w", err) + } + + var result GetCustomBotCategoryItemSequenceResponse + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetCustomBotCategoryItemSequence request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return &result, nil +} + +func (b *botman) UpdateCustomBotCategoryItemSequence(ctx context.Context, params UpdateCustomBotCategoryItemSequenceRequest) (*UpdateCustomBotCategoryItemSequenceResponse, error) { + logger := b.Log(ctx) + logger.Debug("UpdateCustomBotCategoryItemSequence") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + putURL := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/custom-bot-categories/%s/custom-bot-category-item-sequence", + params.ConfigID, + params.Version, + params.CategoryID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateCustomBotCategoryItemSequence request: %w", err) + } + + var result UpdateCustomBotCategoryItemSequenceResponse + resp, err := b.Exec(req, &result, params.Sequence) + if err != nil { + return nil, fmt.Errorf("UpdateCustomBotCategoryItemSequence request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return &result, nil +} diff --git a/pkg/botman/custom_bot_category_item_sequence_test.go b/pkg/botman/custom_bot_category_item_sequence_test.go new file mode 100644 index 00000000..0463fba2 --- /dev/null +++ b/pkg/botman/custom_bot_category_item_sequence_test.go @@ -0,0 +1,171 @@ +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 CustomBotCategoryItemSequence +func TestBotman_GetCustomBotCategoryItemSequence(t *testing.T) { + tests := map[string]struct { + params GetCustomBotCategoryItemSequenceRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetCustomBotCategoryItemSequenceResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetCustomBotCategoryItemSequenceRequest{ + ConfigID: 43253, + Version: 15, + CategoryID: "f4f0cb20-eddb-4421-93d9-90954e509d5f", + }, + responseStatus: http.StatusOK, + responseBody: `{"sequence":["fake3f89-e179-4892-89cf-d5e623ba9dc7","fake85df-e399-43e8-bb0f-c0d980a88e4f","fake9b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-bot-categories/f4f0cb20-eddb-4421-93d9-90954e509d5f/custom-bot-category-item-sequence", + expectedResponse: &GetCustomBotCategoryItemSequenceResponse{ + Sequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + }, + "500 internal server error": { + params: GetCustomBotCategoryItemSequenceRequest{ + ConfigID: 43253, + Version: 15, + CategoryID: "f4f0cb20-eddb-4421-93d9-90954e509d5f", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-bot-categories/f4f0cb20-eddb-4421-93d9-90954e509d5f/custom-bot-category-item-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 required params - validation error": { + params: GetCustomBotCategoryItemSequenceRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "struct validation: CategoryID: cannot be blank\nConfigID: 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.GetCustomBotCategoryItemSequence(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 CustomBotCategoryItemSequence. +func TestBotman_UpdateCustomBotCategoryItemSequence(t *testing.T) { + tests := map[string]struct { + params UpdateCustomBotCategoryItemSequenceRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *UpdateCustomBotCategoryItemSequenceResponse + withError func(*testing.T, error) + }{ + "200 Success": { + params: UpdateCustomBotCategoryItemSequenceRequest{ + ConfigID: 43253, + Version: 15, + CategoryID: "f4f0cb20-eddb-4421-93d9-90954e509d5f", + Sequence: UUIDSequence{Sequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"}}, + }, + responseStatus: http.StatusOK, + responseBody: `{"sequence":["fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"]}`, + expectedResponse: &UpdateCustomBotCategoryItemSequenceResponse{ + Sequence: []string{"fake3f89-e179-4892-89cf-d5e623ba9dc7", "fake85df-e399-43e8-bb0f-c0d980a88e4f", "fake9b8-4fd5-430e-a061-1c61df1d2ac2"}, + }, + expectedPath: "/appsec/v1/configs/43253/versions/15/custom-bot-categories/f4f0cb20-eddb-4421-93d9-90954e509d5f/custom-bot-category-item-sequence", + }, + "500 internal server error": { + params: UpdateCustomBotCategoryItemSequenceRequest{ + ConfigID: 43253, + Version: 15, + CategoryID: "f4f0cb20-eddb-4421-93d9-90954e509d5f", + Sequence: UUIDSequence{Sequence: []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/custom-bot-categories/f4f0cb20-eddb-4421-93d9-90954e509d5f/custom-bot-category-item-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 required params - validation error": { + params: UpdateCustomBotCategoryItemSequenceRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "struct validation: CategoryID: cannot be blank\nConfigID: cannot be blank\nSequence: 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.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.UpdateCustomBotCategoryItemSequence( + session.ContextWithOptions( + context.Background()), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/botman/mocks.go b/pkg/botman/mocks.go index 4cfa2d63..b9e973b3 100644 --- a/pkg/botman/mocks.go +++ b/pkg/botman/mocks.go @@ -580,3 +580,19 @@ func (p *Mock) UpdateCustomCode(ctx context.Context, params UpdateCustomCodeRequ } return args.Get(0).(map[string]interface{}), nil } + +func (p *Mock) GetCustomBotCategoryItemSequence(ctx context.Context, params GetCustomBotCategoryItemSequenceRequest) (*GetCustomBotCategoryItemSequenceResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*GetCustomBotCategoryItemSequenceResponse), nil +} + +func (p *Mock) UpdateCustomBotCategoryItemSequence(ctx context.Context, params UpdateCustomBotCategoryItemSequenceRequest) (*UpdateCustomBotCategoryItemSequenceResponse, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(*UpdateCustomBotCategoryItemSequenceResponse), nil +} diff --git a/pkg/cloudlets/match_rule.go b/pkg/cloudlets/match_rule.go index 766f0496..3532f45b 100644 --- a/pkg/cloudlets/match_rule.go +++ b/pkg/cloudlets/match_rule.go @@ -568,7 +568,7 @@ func (m MatchCriteriaPR) Validate() error { // Validate validates MatchCriteriaER func (m MatchCriteriaER) Validate() error { - return validation.Errors{ + errs := validation.Errors{ "MatchType": validation.Validate(m.MatchType, validation.In("header", "hostname", "path", "extension", "query", "regex", "cookie", "deviceCharacteristics", "clientip", "continent", "countrycode", "regioncode", "protocol", "method", "proxy").Error( fmt.Sprintf("value '%s' is invalid. Must be one of: 'header', 'hostname', 'path', 'extension', 'query', 'regex', 'cookie', "+ @@ -581,7 +581,12 @@ func (m MatchCriteriaER) Validate() error { fmt.Sprintf("value '%s' is invalid. Must be one of: 'CONNECTING_IP', 'XFF_HEADERS', 'CONNECTING_IP XFF_HEADERS' or '' (empty)", (&m).CheckIPs))), "ObjectMatchValue": validation.Validate(m.ObjectMatchValue, validation.Required.When(m.MatchValue == "").Error("cannot be blank when MatchValue is blank"), validation.Empty.When(m.MatchValue != "").Error("must be blank when MatchValue is set"), validation.By(objectMatchValueSimpleOrObjectValidation)), - }.Filter() + } + // Additional validation for MatchType and ObjectMatchValue + if m.MatchType == "query" && m.ObjectMatchValue != nil { + errs["ObjectMatchValue"] = errors.New("ObjectMatchValue not supported with MatchType 'query'") + } + return errs.Filter() } // Validate validates MatchCriteriaFR diff --git a/pkg/cloudlets/match_rule_test.go b/pkg/cloudlets/match_rule_test.go index bb1586d9..b299448f 100644 --- a/pkg/cloudlets/match_rule_test.go +++ b/pkg/cloudlets/match_rule_test.go @@ -1234,6 +1234,20 @@ MatchRules[4]: { }, }, }, + MatchRuleER{ + StatusCode: 301, + RedirectURL: "abc.com", + Type: "erMatchRule", + Matches: []MatchCriteriaER{ + { + MatchType: "query", + ObjectMatchValue: ObjectMatchValueObject{ + Type: "simple", + Name: "p", + }, + }, + }, + }, }, withError: ` MatchRules[0]: { @@ -1247,6 +1261,11 @@ MatchRules[1]: { } MatchRules[2]: { Matches/MatchesAlways: only one of [ "Matches", "MatchesAlways" ] can be specified +} +MatchRules[3]: { + Matches[0]: { + ObjectMatchValue: ObjectMatchValue not supported with MatchType 'query' + } }`, }, "valid match rules FR": { diff --git a/pkg/hapi/edgehostname.go b/pkg/hapi/edgehostname.go index 05dc1d2b..738fed8c 100644 --- a/pkg/hapi/edgehostname.go +++ b/pkg/hapi/edgehostname.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "strings" + "time" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" @@ -35,6 +36,11 @@ type ( // // See: https://techdocs.akamai.com/edge-hostnames/reference/patch-edgehostnames UpdateEdgeHostname(context.Context, UpdateEdgeHostnameRequest) (*UpdateEdgeHostnameResponse, error) + + // GetCertificate gets the certificate associated with an enhanced TLS edge hostname + // + // See: https://techdocs.akamai.com/edge-hostnames/reference/get-edge-hostname-certificate + GetCertificate(context.Context, GetCertificateRequest) (*GetCertificateResponse, error) } // DeleteEdgeHostnameRequest is used to delete edge hostname @@ -128,40 +134,61 @@ type ( // GetEdgeHostnameResponse represents edge hostname GetEdgeHostnameResponse struct { - EdgeHostnameID int `json:"edgeHostnameId"` - RecordName string `json:"recordName"` - DNSZone string `json:"dnsZone"` - SecurityType string `json:"securityType"` - UseDefaultTTL bool `json:"useDefaultTtl"` - UseDefaultMap bool `json:"useDefaultMap"` - IPVersionBehavior string `json:"ipVersionBehavior"` - TTL int `json:"ttl"` - Map string `json:"map,omitempty"` - SlotNumber int `json:"slotNumber,omitempty"` - Comments string `json:"comments"` - SerialNumber int `json:"serialNumber,omitempty"` - CustomTarget string `json:"customTarget,omitempty"` - ChinaCdn ChinaCDN `json:"chinaCdn,omitempty"` - IsEdgeIPBindingEnabled bool `json:"isEdgeIPBindingEnabled,omitempty"` + EdgeHostnameID int `json:"edgeHostnameId"` + RecordName string `json:"recordName"` + DNSZone string `json:"dnsZone"` + SecurityType string `json:"securityType"` + UseDefaultTTL bool `json:"useDefaultTtl"` + UseDefaultMap bool `json:"useDefaultMap"` + IPVersionBehavior string `json:"ipVersionBehavior"` + ProductID string `json:"productId"` + TTL int `json:"ttl"` + Map string `json:"map,omitempty"` + SlotNumber int `json:"slotNumber,omitempty"` + Comments string `json:"comments"` + SerialNumber int `json:"serialNumber,omitempty"` + CustomTarget string `json:"customTarget,omitempty"` + ChinaCdn ChinaCDN `json:"chinaCdn,omitempty"` + IsEdgeIPBindingEnabled bool `json:"isEdgeIPBindingEnabled,omitempty"` + MapAlias string `json:"mapAlias"` + UseCases []UseCase `json:"useCases"` + } + + // GetCertificateRequest is used to get certificate associated with edge hostname + GetCertificateRequest struct { + DNSZone string + RecordName string + } + + // GetCertificateResponse represents edge hostname certificate + GetCertificateResponse struct { + AvailableDomains []string `json:"availableDomains"` + CertificateID string `json:"certificateId"` + CertificateType string `json:"certificateType"` + CommonName string `json:"commonName"` + ExpirationDate time.Time `json:"expirationDate"` + SerialNumber string `json:"serialNumber"` + SlotNumber int `json:"slotNumber"` + Status string `json:"status"` + ValidationType string `json:"validationType"` } ) // Validate validates DeleteEdgeHostnameRequest func (r DeleteEdgeHostnameRequest) Validate() error { - return validation.Errors{ + return edgegriderr.ParseValidationErrors(validation.Errors{ "DNSZone": validation.Validate(r.DNSZone, validation.Required), "RecordName": validation.Validate(r.RecordName, validation.Required), - }.Filter() + }) } // Validate validates DeleteEdgeHostnameRequest func (r UpdateEdgeHostnameRequest) Validate() error { - errs := validation.Errors{ + return edgegriderr.ParseValidationErrors(validation.Errors{ "DNSZone": validation.Validate(r.DNSZone, validation.Required), "RecordName": validation.Validate(r.RecordName, validation.Required), "Body": validation.Validate(r.Body), - } - return edgegriderr.ParseValidationErrors(errs) + }) } // Validate validates UpdateEdgeHostnameRequestBody @@ -173,6 +200,14 @@ func (b UpdateEdgeHostnameRequestBody) Validate() error { }.Filter() } +// Validate validates GetCertificateRequest +func (r GetCertificateRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "DNSZone": validation.Validate(r.DNSZone, validation.Required), + "RecordName": validation.Validate(r.RecordName, validation.Required), + }) +} + var ( // ErrDeleteEdgeHostname represents error when deleting edge hostname fails ErrDeleteEdgeHostname = errors.New("delete edge hostname") @@ -180,16 +215,20 @@ var ( ErrGetEdgeHostname = errors.New("get edge hostname") // ErrUpdateEdgeHostname represents error when updating edge hostname fails ErrUpdateEdgeHostname = errors.New("update edge hostname") + // ErrGetCertificate represents error when getting edge hostname certificate fails + ErrGetCertificate = errors.New("get edge hostname certificate") + // ErrNotFound represents error when getting edge hostname fails + ErrNotFound = errors.New("not found") ) func (h *hapi) DeleteEdgeHostname(ctx context.Context, params DeleteEdgeHostnameRequest) (*DeleteEdgeHostnameResponse, error) { + logger := h.Log(ctx) + logger.Debug("DeleteEdgeHostname") + if err := params.Validate(); err != nil { return nil, fmt.Errorf("%s: %w: %s", ErrDeleteEdgeHostname, ErrStructValidation, err) } - logger := h.Log(ctx) - logger.Debug("DeleteEdgeHostname") - uri := fmt.Sprintf( "/hapi/v1/dns-zones/%s/edge-hostnames/%s", params.DNSZone, @@ -249,13 +288,13 @@ func (h *hapi) GetEdgeHostname(ctx context.Context, edgeHostnameID int) (*GetEdg } func (h *hapi) UpdateEdgeHostname(ctx context.Context, request UpdateEdgeHostnameRequest) (*UpdateEdgeHostnameResponse, error) { + logger := h.Log(ctx) + logger.Debug("UpdateEdgeHostname") + if err := request.Validate(); err != nil { return nil, fmt.Errorf("%w: %s: %s", ErrUpdateEdgeHostname, ErrStructValidation, err) } - logger := h.Log(ctx) - logger.Debug("UpdateEdgeHostname") - uri := fmt.Sprintf("/hapi/v1/dns-zones/%s/edge-hostnames/%s", request.DNSZone, request.RecordName) body, err := buildBody(request.Body) @@ -294,6 +333,41 @@ func (h *hapi) UpdateEdgeHostname(ctx context.Context, request UpdateEdgeHostnam return &result, nil } +func (h *hapi) GetCertificate(ctx context.Context, params GetCertificateRequest) (*GetCertificateResponse, error) { + logger := h.Log(ctx) + logger.Debug("GetCertificate") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrGetCertificate, ErrStructValidation, err) + } + + uri := fmt.Sprintf( + "/hapi/v1/dns-zones/%s/edge-hostnames/%s/certificate", + params.DNSZone, + params.RecordName, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetCertificate, err) + } + + var result GetCertificateResponse + + resp, err := h.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrGetCertificate, err) + } + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%s: %s: %w", ErrGetCertificate, ErrNotFound, h.Error(resp)) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrGetCertificate, h.Error(resp)) + } + + return &result, nil +} + func buildBody(body []UpdateEdgeHostnameRequestBody) (io.Reader, error) { reqBody, err := json.Marshal(body) if err != nil { diff --git a/pkg/hapi/edgehostname_test.go b/pkg/hapi/edgehostname_test.go index dc3d55d3..6cbaaaca 100644 --- a/pkg/hapi/edgehostname_test.go +++ b/pkg/hapi/edgehostname_test.go @@ -3,9 +3,11 @@ package hapi import ( "context" "errors" + "fmt" "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -187,13 +189,22 @@ func TestGetEdgeHostname(t *testing.T) { "dnsZone": "edgekey.net", "edgeHostnameId": 4617960, "ipVersionBehavior": "IPV6_IPV4_DUALSTACK", + "productId": "DSA", "map": "e;dscx.akamaiedge.net", "recordName": "aws_ci_pearltest-asorigin-na-as-eu-ionp.cumulus-essl.webexp-ipqa-ion.com-v2", "securityType": "ENHANCED-TLS", "slotNumber": 47463, "ttl": 21600, "useDefaultMap": true, - "useDefaultTtl": true + "useDefaultTtl": true, + "mapAlias": "al", + "useCases": [ + { + "type": "GLOBAL", + "option": "LIVE", + "useCase": "Segmented_Media_Mode" + } + ] }`, expectedPath: "/hapi/v1/edge-hostnames/1234", expectedResponse: &GetEdgeHostnameResponse{ @@ -204,6 +215,7 @@ func TestGetEdgeHostname(t *testing.T) { DNSZone: "edgekey.net", EdgeHostnameID: 4617960, IPVersionBehavior: "IPV6_IPV4_DUALSTACK", + ProductID: "DSA", Map: "e;dscx.akamaiedge.net", RecordName: "aws_ci_pearltest-asorigin-na-as-eu-ionp.cumulus-essl.webexp-ipqa-ion.com-v2", SecurityType: "ENHANCED-TLS", @@ -211,6 +223,14 @@ func TestGetEdgeHostname(t *testing.T) { TTL: 21600, UseDefaultMap: true, UseDefaultTTL: true, + MapAlias: "al", + UseCases: []UseCase{ + { + Type: "GLOBAL", + Option: "LIVE", + UseCase: "Segmented_Media_Mode", + }, + }, }, }, "404 could not find edge hostname": { @@ -431,3 +451,135 @@ func TestPatchEdgeHostname(t *testing.T) { }) } } + +func TestGetCertificate(t *testing.T) { + tests := map[string]struct { + request GetCertificateRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetCertificateResponse + withError error + }{ + "200 OK": { + request: GetCertificateRequest{ + DNSZone: "edgekey.net", + RecordName: "mgw-test-002", + }, + responseStatus: http.StatusOK, + responseBody: `{ + "certificateId": "1234", + "commonName": "example.com", + "serialNumber": "12:34:56:78:90:AB:CD:EF", + "slotNumber": 8927, + "expirationDate": "2019-10-31T23:59:59Z", + "certificateType": "SAN", + "validationType": "DOMAIN_VALIDATION", + "status": "PENDING", + "availableDomains": [ + "live.example.com", + "secure.example.com", + "www.example.com" + ] + }`, + expectedPath: "/hapi/v1/dns-zones/edgekey.net/edge-hostnames/mgw-test-002/certificate", + expectedResponse: &GetCertificateResponse{ + CertificateID: "1234", + CommonName: "example.com", + SerialNumber: "12:34:56:78:90:AB:CD:EF", + SlotNumber: 8927, + ExpirationDate: *newTimeFromString(t, "2019-10-31T23:59:59Z"), + CertificateType: "SAN", + ValidationType: "DOMAIN_VALIDATION", + Status: "PENDING", + AvailableDomains: []string{ + "live.example.com", + "secure.example.com", + "www.example.com", + }, + }, + }, + "404 certificate not found": { + request: GetCertificateRequest{ + DNSZone: "edgekey.net", + RecordName: "unknown", + }, + responseStatus: http.StatusNotFound, + responseBody: ` + { + "type": "CERTIFICATE_NOT_FOUND", + "title": "Certificate Not Found", + "status": 404, + "detail": "Details are not available for this certificate; the certificate is missing or access is denied", + "instance": "/hapi/error-instances/a30f67cc-df20-4e02-bbc3-cf7c204a4aab", + "requestInstance": "http://origin.pulsar.akamai.com/hapi/open/v1/dns-zones/edgekey.net/edge-hostnames/example.com/certificate?depth=ALL&accountSwitchKey=F-AC-1937217#d7aa7348", + "method": "GET", + "requestTime": "2022-11-30T18:51:43.482982Z", + "errors": [], + "extensionFields": [] + }`, + expectedPath: "/hapi/v1/dns-zones/edgekey.net/edge-hostnames/unknown/certificate", + withError: fmt.Errorf("%s: %s: %w", ErrGetCertificate, ErrNotFound, &Error{ + Type: "CERTIFICATE_NOT_FOUND", + Title: "Certificate Not Found", + Status: 404, + Detail: "Details are not available for this certificate; the certificate is missing or access is denied", + Instance: "/hapi/error-instances/a30f67cc-df20-4e02-bbc3-cf7c204a4aab", + RequestInstance: "http://origin.pulsar.akamai.com/hapi/open/v1/dns-zones/edgekey.net/edge-hostnames/example.com/certificate?depth=ALL&accountSwitchKey=F-AC-1937217#d7aa7348", + Method: "GET", + RequestTime: "2022-11-30T18:51:43.482982Z", + }), + }, + "500 internal server error": { + request: GetCertificateRequest{ + DNSZone: "edgekey.net", + RecordName: "mgw-test-002", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error deleting activation", + "status": 500 + }`, + expectedPath: "/hapi/v1/dns-zones/edgekey.net/edge-hostnames/mgw-test-002/certificate", + withError: &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error deleting activation", + Status: http.StatusInternalServerError, + }, + }, + "missing required values": { + request: GetCertificateRequest{}, + withError: ErrStructValidation, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetCertificate(context.Background(), test.request) + 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 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/hapi/errors.go b/pkg/hapi/errors.go index bd00273b..9d0f159c 100644 --- a/pkg/hapi/errors.go +++ b/pkg/hapi/errors.go @@ -71,6 +71,10 @@ func (e *Error) Error() string { // Is handles error comparisons func (e *Error) Is(target error) bool { + if errors.Is(target, ErrNotFound) { + return e.isErrNotFound() + } + var t *Error if !errors.As(target, &t) { return false @@ -86,3 +90,7 @@ func (e *Error) Is(target error) bool { return e.Error() == t.Error() } + +func (e *Error) isErrNotFound() bool { + return e.Status == http.StatusNotFound +} diff --git a/pkg/hapi/mocks.go b/pkg/hapi/mocks.go index 7c16beec..0566b644 100644 --- a/pkg/hapi/mocks.go +++ b/pkg/hapi/mocks.go @@ -53,3 +53,13 @@ func (m *Mock) GetChangeRequest(ctx context.Context, req GetChangeRequest) (*Cha return args.Get(0).(*ChangeRequest), nil } + +func (m *Mock) GetCertificate(ctx context.Context, req GetCertificateRequest) (*GetCertificateResponse, error) { + args := m.Called(ctx, req) + + if args.Error(1) != nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetCertificateResponse), nil +}