From ae7255c74f1a9379639fcbf2e49a98e6be496239 Mon Sep 17 00:00:00 2001 From: Shalini Shivam Date: Thu, 10 Apr 2025 16:36:53 +0200 Subject: [PATCH] Test Cases and SonarQube Critical Code Smells Issue-ID: POLICY-5337 Change-Id: I0f708c114a1d368f34740448cb7f2b163ae9f89e Signed-off-by: Shalini Shivam --- cmd/opa-pdp/opa-pdp.go | 11 - cmd/opa-pdp/opa-pdp_test.go | 27 - consts/constants.go | 89 +-- pkg/data/data-handler.go | 183 +++--- pkg/data/data-handler_test.go | 431 ++++++++++++- pkg/decision/decision-provider.go | 6 +- pkg/healthcheck/healthcheck.go | 6 +- pkg/kafkacomm/handler/pdp_message_handler.go | 57 +- pkg/kafkacomm/handler/pdp_message_handler_test.go | 232 ++++--- .../handler/pdp_state_change_handler_test.go | 16 +- pkg/kafkacomm/handler/pdp_update_deploy_policy.go | 139 ++--- .../handler/pdp_update_deploy_policy_test.go | 10 +- .../handler/pdp_update_message_handler.go | 180 ++++-- .../handler/pdp_update_message_handler_test.go | 673 +++++++++++++++++---- .../handler/pdp_update_undeploy_policy.go | 78 ++- .../handler/pdp_update_undeploy_policy_test.go | 595 +++++++++++++----- pkg/kafkacomm/pdp_topic_producer.go | 2 + pkg/kafkacomm/pdp_topic_producer_test.go | 48 ++ pkg/kafkacomm/publisher/pdp-heartbeat_test.go | 30 - pkg/kafkacomm/publisher/pdp-pap-registration.go | 28 - .../publisher/pdp-pap-registration_test.go | 23 - pkg/kafkacomm/publisher/pdp-status-publisher.go | 12 +- pkg/metrics/statistics-provider.go | 8 +- pkg/metrics/statistics-provider_test.go | 23 + pkg/opasdk/opasdk.go | 20 +- pkg/opasdk/opasdk_test.go | 3 +- pkg/policymap/policy_and_data_map.go | 40 +- pkg/utils/sort_test.go | 68 +++ pkg/utils/utils.go | 220 ++++--- pkg/utils/utils_test.go | 209 +++++-- 30 files changed, 2492 insertions(+), 975 deletions(-) create mode 100644 pkg/utils/sort_test.go diff --git a/cmd/opa-pdp/opa-pdp.go b/cmd/opa-pdp/opa-pdp.go index 6880f0e..f4b8093 100644 --- a/cmd/opa-pdp/opa-pdp.go +++ b/cmd/opa-pdp/opa-pdp.go @@ -52,7 +52,6 @@ var ( waitForServerFunc = waitForServer initializeOPAFunc = initializeOPA startKafkaConsAndProdFunc = startKafkaConsAndProd - registerPDPFunc = registerPDP handleMessagesFunc = handleMessages handleShutdownFunc = handleShutdown ) @@ -120,16 +119,6 @@ func handleMessages(ctx context.Context, kc *kafkacomm.KafkaConsumer, sender *pu }() } -// register pdp with PAP -func registerPDP(sender publisher.PdpStatusSender) bool { - if err := publisher.SendPdpPapRegistration(sender); err != nil { - log.Warnf("Failed PDP PAP registration: %v", err) - return false - } - log.Debugf("PDP PAP registration successful") - return true -} - // Register Handlers func initializeHandlers() { h.RegisterHandlers() diff --git a/cmd/opa-pdp/opa-pdp_test.go b/cmd/opa-pdp/opa-pdp_test.go index 37f4314..e421f12 100644 --- a/cmd/opa-pdp/opa-pdp_test.go +++ b/cmd/opa-pdp/opa-pdp_test.go @@ -179,11 +179,6 @@ func SetupMocks() { return nil // no error expected } - registerPDPFunc = func(sender publisher.PdpStatusSender) bool { - // Simulate the registration logic here - return false // Simulate successful registration - } - handleMessagesFunc = func(ctx context.Context, kc *kafkacomm.KafkaConsumer, sender *publisher.RealPdpStatusSender) { return } @@ -296,28 +291,6 @@ func TestInitializeHandlers(t *testing.T) { initializeHandlers() } -// Test to simulate the successful registration of a PDP -func TestRegisterPDP_Success(t *testing.T) { - mockSender := new(MockPdpStatusSender) - mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - - result := registerPDP(mockSender) - - assert.True(t, result) - mockSender.AssertExpectations(t) -} - -// Test to simulate a failure scenario during the registration of a PDP. -func TestRegisterPDP_Failure(t *testing.T) { - mockSender := new(MockPdpStatusSender) - mockSender.On("SendPdpStatus", mock.Anything).Return(assert.AnError) - - result := registerPDP(mockSender) - - assert.False(t, result) - mockSender.AssertExpectations(t) -} - // Test to verify that the HTTP Server starts successfully and can be shut down gracefully. func TestStartAndShutDownHTTPServer(t *testing.T) { testServer := startHTTPServer() diff --git a/consts/constants.go b/consts/constants.go index 6e2eedb..3623f93 100644 --- a/consts/constants.go +++ b/consts/constants.go @@ -23,33 +23,37 @@ package consts // Variables: // -// LogFilePath - The file path for the log file. -// LogMaxSize - The maximum size of the log file in megabytes. -// LogMaxBackups - The maximum number of backup log files to retain. -// OpasdkConfigPath - The file path for the OPA SDK configuration. -// Opa - The file path for the OPA binary. -// BuildBundle - The command to build the bundle. -// Policies - The directory path for policies. -// Data - The directory path for policy data. -// DataNode - The directory path for policy data with node. -// Output - The output flag for bundle commands. -// BundleTarGz - The name of the bundle tar.gz file. -// BundleTarGzFile - The file path for the bundle tar.gz file. -// PdpGroup - The default PDP group. -// PdpType - The type of PDP. -// ServerPort - The port on which the server listens. -// SERVER_WAIT_UP_TIME - The time to wait for the server to be up, in seconds. -// ShutdownWaitTime - The time to wait for the server to shut down, in seconds. -// V1Compatible - The flag for v1 compatibility. -// LatestVersion - The Version set in response for decision -// MinorVersion - The Minor version set in response header for decision -// PatchVersion - The Patch Version set in response header for decison -// OpaPdpUrl - The Healthcheck url for response -// HealtCheckStatus - The bool flag for Healthy field in HealtCheck response -// OkCode - The Code for HealthCheck response -// HealthCheckMessage - The Healtcheck Message -// SingleHierarchy - The Counter indicates the length of datakey path -// DefaultHeartbeatMS - The default interval for heartbeat signals in milliseconds. +// LogFilePath - The file path for the log file. +// LogMaxSize - The maximum size of the log file in megabytes. +// LogMaxBackups - The maximum number of backup log files to retain. +// OpasdkConfigPath - The file path for the OPA SDK configuration. +// Opa - The file path for the OPA binary. +// BuildBundle - The command to build the bundle. +// Policies - The directory path for policies. +// Data - The directory path for policy data. +// DataNode - The directory path for policy data with node. +// Output - The output flag for bundle commands. +// BundleTarGz - The name of the bundle tar.gz file. +// BundleTarGzFile - The file path for the bundle tar.gz file. +// PdpGroup - The default PDP group. +// PdpType - The type of PDP. +// ServerPort - The port on which the server listens. +// ServerWaitUpTime - The time to wait for the server to be up, in seconds. +// ShutdownWaitTime - The time to wait for the server to shut down, in seconds. +// V1Compatible - The flag for v1 compatibility. +// LatestVersion - The Version set in response for decision +// MinorVersion - The Minor version set in response header for decision +// PatchVersion - The Patch Version set in response header for decison +// OpaPdpUrl - The Healthcheck url for response +// HealtCheckStatus - The bool flag for Healthy field in HealtCheck response +// OkCode - The Code for HealthCheck response +// HealthCheckMessage - The Healtcheck Message +// DefaultHeartbeatMS - The default interval for heartbeat signals in milliseconds. +// SingleHierarchy - The Counter indicates the length of datakey path +// PolicyVersion - constant declared for policy-version +// PolicyID - constant declared for policy-id +// RequestId - constant declared for ONAP Request-ID +// MaxOutputResponseLength - constant declared for maximum length of output in response message var ( LogFilePath = "/var/logs/logs.log" LogMaxSize = 10 @@ -64,19 +68,22 @@ var ( BundleTarGz = "bundle.tar.gz" BundleTarGzFile = "/app/bundles/bundle.tar.gz" PdpGroup = "opaGroup" - //This is a workaround as currently opa-pdp is not defined in the PapDB defaultGroup configuration and creating it manually overrides the existing configuration, so currently PdpGroup is opaGroup and it will be changed to defaultGroup once added in the configuration. - PdpType = "opa" - ServerPort = ":8282" - ServerWaitUpTime = 5 - ShutdownWaitTime = 5 + PdpType = "opa" + ServerPort = ":8282" + ServerWaitUpTime = 5 + ShutdownWaitTime = 5 V1Compatible = "--v1-compatible" - LatestVersion = "1.0.0" - MinorVersion = "0" - PatchVersion = "0" - OpaPdpUrl = "self" - HealtCheckStatus = true - OkCode = int32(200) - HealthCheckMessage = "alive" - SingleHierarchy = 4 - DefaultHeartbeatMS = 60000 + LatestVersion = "1.0.0" + MinorVersion = "0" + PatchVersion = "0" + OpaPdpUrl = "self" + HealtCheckStatus = true + OkCode = int32(200) + HealthCheckMessage = "alive" + DefaultHeartbeatMS = 60000 + SingleHierarchy = 4 + PolicyVersion = "policy-version" + PolicyId = "policy-id" + RequestId = "X-ONAP-RequestID" + MaxOutputResponseLength = 200 ) diff --git a/pkg/data/data-handler.go b/pkg/data/data-handler.go index 673f247..e1dcf17 100644 --- a/pkg/data/data-handler.go +++ b/pkg/data/data-handler.go @@ -21,6 +21,7 @@ package data import ( "context" "encoding/json" + "errors" "fmt" "github.com/google/uuid" openapi_types "github.com/oapi-codegen/runtime/types" @@ -37,10 +38,17 @@ import ( "strings" ) +type ( + checkIfPolicyAlreadyExistsFunc func(policyId string) bool +) + var ( - addOp storage.PatchOp = 0 - removeOp storage.PatchOp = 1 - replaceOp storage.PatchOp = 2 + addOp storage.PatchOp = 0 + removeOp storage.PatchOp = 1 + replaceOp storage.PatchOp = 2 + checkIfPolicyAlreadyExistsVar checkIfPolicyAlreadyExistsFunc = policymap.CheckIfPolicyAlreadyExists + getPolicyByIDVar = getPolicyByID + extractPatchInfoVar = extractPatchInfo ) // creates a response code map to OPADataUpdateResponse @@ -139,6 +147,11 @@ func patchHandler(res http.ResponseWriter, req *http.Request) { data := requestBody.Data log.Infof("data : %s", data) policyId := requestBody.PolicyName + if policyId == nil { + errMsg := "Policy Id is nil" + sendErrorResponse(res, errMsg, http.StatusBadRequest) + return + } log.Infof("policy name : %s", *policyId) isExists := policymap.CheckIfPolicyAlreadyExists(*policyId) if !isExists { @@ -148,45 +161,14 @@ func patchHandler(res http.ResponseWriter, req *http.Request) { return } - // Checking if the data operation is performed for a deployed policy with policymap.CheckIfPolicyAlreadyExists and getPolicyByID - // if a match is found, we will join the url path with dots and check for the data key from the policiesMap whether utl path is a - // prefix of data key. we will proceed for Patch Operation if this matches, else return error - if len(dirParts) > 0 && dirParts[0] == "" { - dirParts = dirParts[1:] - } - finalDirParts := strings.Join(dirParts, ".") - - policiesMap := policymap.LastDeployedPolicies + matchFound := validatePolicyDataPathMatched(dirParts, *policyId, res) - matchedPolicy, err := getPolicyByID(policiesMap, *policyId) - if err != nil { - sendErrorResponse(res, err.Error(), http.StatusBadRequest) - log.Errorf(err.Error()) - return - } - - log.Infof("Matched policy: %+v", matchedPolicy) - - // Check if finalDirParts starts with any data key - matchFound := false - for _, dataKey := range matchedPolicy.Data { - if strings.HasPrefix(finalDirParts, dataKey) { - matchFound = true - break + if matchFound { + if err := patchData(dataDir, data, res); err != nil { + // Handle the error, for example, log it or return an appropriate response + log.Errorf("Error encoding JSON response: %s", err) } } - - if !matchFound { - errMsg := fmt.Sprintf("Dynamic Data add/replace/remove for policy '%s' expected under url path '%v'", *policyId, matchedPolicy.Data) - sendErrorResponse(res, errMsg, http.StatusBadRequest) - log.Errorf(errMsg) - return - } - - if err := patchData(dataDir, data, res); err != nil { - // Handle the error, for example, log it or return an appropriate response - log.Errorf("Error encoding JSON response: %s", err) - } } } @@ -202,56 +184,62 @@ func DataHandler(res http.ResponseWriter, req *http.Request) { } } -func extractPatchInfo(res http.ResponseWriter, ops *[]map[string]interface{}, root string) (result []opasdk.PatchImpl) { +func extractPatchInfo(res http.ResponseWriter, ops *[]map[string]interface{}, root string) ([]opasdk.PatchImpl, error) { + var result []opasdk.PatchImpl for _, op := range *ops { - // Extract the operation, path, and value from the map + optypeString, opTypeErr := op["op"].(string) if !opTypeErr { opTypeErrMsg := "Error in getting op type. Op type is not given in request body" sendErrorResponse(res, opTypeErrMsg, http.StatusInternalServerError) log.Errorf(opTypeErrMsg) - return nil + return nil, fmt.Errorf("Error in getting op type. Op type is not given in request body") } - opType := getOperationType(optypeString, res) + opType, err := getOperationType(optypeString, res) + if err != nil { + log.Warnf("Error in getting opType: %v", err) + return nil, fmt.Errorf("Error in getting operation type") + } if opType == nil { - return nil + return nil, fmt.Errorf("Error in getting operation Type as opType is Missing") } + impl := opasdk.PatchImpl{ Op: *opType, } var value interface{} - var valueErr bool // PATCH request with add or replace opType, MUST contain a "value" member whose content specifies the value to be added / replaced. For remove opType, value does not required if optypeString == "add" || optypeString == "replace" { - value, valueErr = op["value"] - if !valueErr || isEmpty(value) { - valueErrMsg := "Error in getting data value. Value is not given in request body" - sendErrorResponse(res, valueErrMsg, http.StatusInternalServerError) - log.Errorf(valueErrMsg) - return nil + value, err = getPatchValue(op, res) + if err != nil { + return nil, fmt.Errorf("Error in gatting Value, Value not found") } } impl.Value = value - - opPath, opPathErr := op["path"].(string) - if !opPathErr || len(opPath) == 0 { - opPathErrMsg := "Error in getting data path. Path is not given in request body" - sendErrorResponse(res, opPathErrMsg, http.StatusInternalServerError) - log.Errorf(opPathErrMsg) - return nil - } - storagePath := constructPath(opPath, optypeString, root, res) + storagePath := constructOpStoragePath(op, root, res) if storagePath == nil { - return nil + return nil, fmt.Errorf("Failed to construct op Storage Path") } impl.Path = storagePath result = append(result, impl) } - //log.Debugf("result : %s", result) - return result + return result, nil +} + +func getPatchValue(op map[string]interface{}, res http.ResponseWriter) (interface{}, error) { + var value interface{} + var valueErr bool + value, valueErr = op["value"] + if !valueErr || isEmpty(value) { + valueErrMsg := "Error in getting data value. Value is not given in request body" + sendErrorResponse(res, valueErrMsg, http.StatusInternalServerError) + log.Errorf(valueErrMsg) + return nil, fmt.Errorf("Error in getting data value. Value is not given in request body") + } + return value, nil } func isEmpty(data interface{}) bool { @@ -328,7 +316,56 @@ func constructPath(opPath string, opType string, root string, res http.ResponseW return storagePath } -func getOperationType(opType string, res http.ResponseWriter) *storage.PatchOp { +func validatePolicyDataPathMatched(dirParts []string, policyId string, res http.ResponseWriter) bool { + matchFound := false + // Check if all dirParts exist in the matched policy's data key + log.Debugf("dirParts : %s", dirParts) + if len(dirParts) > 0 && dirParts[0] == "" { + dirParts = dirParts[1:] + } + finalDirParts := strings.Join(dirParts, ".") + policiesMap := policymap.LastDeployedPolicies + matchedPolicy, err := getPolicyByIDVar(policiesMap, policyId) + if err != nil { + sendErrorResponse(res, err.Error(), http.StatusBadRequest) + log.Errorf("Error getting Policy By Id: %v", err.Error()) + return matchFound + } + + log.Infof("Matched policy: %+v", matchedPolicy) + + // Check if finalDirParts starts with any data key + for _, dataKey := range matchedPolicy.Data { + if strings.HasPrefix(finalDirParts, dataKey) { + matchFound = true + break + } + } + if !matchFound { + errMsg := fmt.Sprintf("Dynamic Data add/replace/remove for policy '%s' expected under url path '%v'", policyId, matchedPolicy.Data) + sendErrorResponse(res, errMsg, http.StatusBadRequest) + log.Errorf(errMsg) + return false + } + + return matchFound +} + +func constructOpStoragePath(op map[string]interface{}, root string, res http.ResponseWriter) storage.Path { + opPath, opPathErr := op["path"].(string) + if !opPathErr || len(opPath) == 0 { + opPathErrMsg := "Error in getting data path. Path is not given in request body" + sendErrorResponse(res, opPathErrMsg, http.StatusInternalServerError) + log.Errorf(opPathErrMsg) + return nil + } + optypeString := op["op"].(string) + storagePath := constructPath(opPath, optypeString, root, res) + return storagePath +} + +func getOperationType(opType string, res http.ResponseWriter) (*storage.PatchOp, error) { + var op *storage.PatchOp switch opType { case "add": @@ -342,10 +379,10 @@ func getOperationType(opType string, res http.ResponseWriter) *storage.PatchOp { errMsg := "Error in getting op type : Invalid operation type (" + opType + ") is used. Only add, remove and replace operation types are supported" sendErrorResponse(res, errMsg, http.StatusBadRequest) log.Errorf(errMsg) - return nil + return nil, errors.New(errMsg) } } - return op + return op, nil } type NewOpaSDKPatchFunc func(ctx context.Context, patches []opasdk.PatchImpl) error @@ -354,7 +391,11 @@ var NewOpaSDKPatch NewOpaSDKPatchFunc = opasdk.PatchData func patchData(root string, ops *[]map[string]interface{}, res http.ResponseWriter) (err error) { root = "/" + strings.Trim(root, "/") - patchInfos := extractPatchInfo(res, ops, root) + patchInfos, err := extractPatchInfoVar(res, ops, root) + if err != nil { + log.Warnf("Failed to extarct Patch Info") + return err + } if patchInfos != nil { patchErr := NewOpaSDKPatch(context.Background(), patchInfos) @@ -367,7 +408,7 @@ func patchData(root string, ops *[]map[string]interface{}, res http.ResponseWrit errMsg := "Error in updating data - " + patchErr.Error() sendErrorResponse(res, errMsg, errCode) log.Errorf(errMsg) - return + return patchErr } log.Infof("Updated the data in the corresponding path successfully\n") res.WriteHeader(http.StatusNoContent) @@ -392,7 +433,7 @@ func invalidMethodHandler(res http.ResponseWriter, method string) { } func constructResponseHeader(res http.ResponseWriter, req *http.Request) { - requestId := req.Header.Get("X-ONAP-RequestID") + requestId := req.Header.Get(consts.RequestId) var parsedUUID *uuid.UUID var decisionParams *oapicodegen.DecisionParams @@ -405,11 +446,11 @@ func constructResponseHeader(res http.ResponseWriter, req *http.Request) { decisionParams = &oapicodegen.DecisionParams{ XONAPRequestID: (*openapi_types.UUID)(parsedUUID), } - res.Header().Set("X-ONAP-RequestID", decisionParams.XONAPRequestID.String()) + res.Header().Set(consts.RequestId, decisionParams.XONAPRequestID.String()) } } else { requestId = "Unknown" - res.Header().Set("X-ONAP-RequestID", requestId) + res.Header().Set(consts.RequestId, requestId) } res.Header().Set("X-LatestVersion", consts.LatestVersion) diff --git a/pkg/data/data-handler_test.go b/pkg/data/data-handler_test.go index e8dcb17..819f6d2 100644 --- a/pkg/data/data-handler_test.go +++ b/pkg/data/data-handler_test.go @@ -30,6 +30,7 @@ import ( "net/http/httptest" "policy-opa-pdp/pkg/model/oapicodegen" "policy-opa-pdp/pkg/opasdk" + "policy-opa-pdp/pkg/policymap" "strings" "testing" "time" @@ -183,7 +184,7 @@ func TestPatchData_storageFail(t *testing.T) { res := httptest.NewRecorder() result := patchData(root, &data, res) assert.Equal(t, http.StatusNotFound, res.Code) - assert.Nil(t, result) + assert.Error(t, result) } func Test_extractPatchInfo_OPTypefail(t *testing.T) { @@ -264,20 +265,23 @@ func TestGetOperationType(t *testing.T) { opType string expectsNil bool }{ - {"Valid opType", "add", false}, - {"Valid opType", "remove", false}, - {"Valid opType", "replace", false}, + {"Valid opType - add", "add", false}, + {"Valid opType - remove", "remove", false}, + {"Valid opType - replace", "replace", false}, {"Invalid opType", "try", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res := httptest.NewRecorder() - result := getOperationType(tt.opType, res) + result, err := getOperationType(tt.opType, res) + if tt.expectsNil { assert.Nil(t, result) + assert.Error(t, err) } else { assert.NotNil(t, result) + assert.NoError(t, err) } }) } @@ -461,3 +465,420 @@ func TestGetData(t *testing.T) { }) } } + +// Sample JSON data for testing +const samplePoliciesJSON = ` +{ + "deployed_policies_dict": [ + { + "data": ["data1", "data2"], + "policy": ["rule1", "rule2"], + "policy-id": "policy123", + "policy-version": "v1.0" + }, + { + "data": ["data3"], + "policy": ["rule3"], + "policy-id": "policy456", + "policy-version": "v2.0" + } + ] +}` + +// Test function for getPolicyByID +func TestGetPolicyByID(t *testing.T) { + tests := []struct { + name string + policiesJSON string + policyID string + expectError bool + expectedData *Policy + }{ + { + name: "Policy Exists", + policiesJSON: samplePoliciesJSON, + policyID: "policy123", + expectError: false, + expectedData: &Policy{ + Data: []string{"data1", "data2"}, + Policy: []string{"rule1", "rule2"}, + PolicyID: "policy123", + PolicyVersion: "v1.0", + }, + }, + { + name: "Policy Not Found", + policiesJSON: samplePoliciesJSON, + policyID: "policy999", + expectError: true, + expectedData: nil, + }, + { + name: "Invalid JSON Input", + policiesJSON: `{ invalid json }`, // Malformed JSON + policyID: "policy123", + expectError: true, + expectedData: nil, + }, + { + name: "Empty JSON Input", + policiesJSON: `{ "deployed_policies_dict": [] }`, // No policies + policyID: "policy123", + expectError: true, + expectedData: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + policy, err := getPolicyByID(tc.policiesJSON, tc.policyID) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error, but got none") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if policy == nil { + t.Fatalf("Expected policy, but got nil") + } + // Validate policy fields + if policy.PolicyID != tc.expectedData.PolicyID || + policy.PolicyVersion != tc.expectedData.PolicyVersion || + !equalSlices(policy.Data, tc.expectedData.Data) || + !equalSlices(policy.Policy, tc.expectedData.Policy) { + t.Errorf("Policy mismatch: got %+v, expected %+v", policy, tc.expectedData) + } + } + }) + } +} + +// Helper function to compare string slices +func equalSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestIsEmpty(t *testing.T) { + tests := []struct { + name string + input interface{} + expected bool + }{ + {"Nil Value", nil, true}, + {"Empty String", "", true}, + {"Non-Empty String", "hello", false}, + {"Empty Slice", []interface{}{}, true}, + {"Non-Empty Slice", []interface{}{1, 2, 3}, false}, + {"Empty Map", map[string]interface{}{}, true}, + {"Non-Empty Map", map[string]interface{}{"key": "value"}, false}, + {"Empty Byte Slice", []byte{}, true}, + {"Non-Empty Byte Slice", []byte("data"), false}, + {"Zero Integer", 0, true}, + {"Non-Zero Integer", 10, false}, + {"Zero Float", 0.0, true}, + {"Non-Zero Float", 3.14, false}, + {"Non-Zero Unsigned Integer", uint(5), false}, + {"Boolean False", false, true}, + {"Boolean True", true, false}, + {"Unsupported Type (Struct)", struct{}{}, false}, // Not considered empty + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := isEmpty(tc.input) + if result != tc.expected { + t.Errorf("isEmpty(%v) = %v; want %v", tc.input, result, tc.expected) + } + }) + } +} + +func TestPatchHandler_EmptyDataField(t *testing.T) { + ctime := "08:26:41" + timeZone := "America/New_York" + timeOffset := "+02:00" + onapComp := "COMPONENT" + onapIns := "INSTANCE" + onapName := "ONAP" + policyName := "TestPolicy" + + parsedDate, err := time.Parse("2006-01-02", "2024-02-12") + if err != nil { + fmt.Println("error in parsedDate") + } + currentDate := openapi_types.Date{Time: parsedDate} + currentDateTime, err := time.Parse(time.RFC3339, "2024-02-12T12:00:00Z") + if err != nil { + fmt.Println("error in currentDateTime") + } + + var data []map[string]interface{} // Empty data field (to trigger validation) + + invalidRequest := &oapicodegen.OPADataUpdateRequest{ + CurrentDate: ¤tDate, + CurrentDateTime: ¤tDateTime, + CurrentTime: &ctime, + TimeOffset: &timeOffset, + TimeZone: &timeZone, + OnapComponent: &onapComp, + OnapInstance: &onapIns, + OnapName: &onapName, + PolicyName: &policyName, + Data: &data, // Empty data + } + + // Marshal the request to JSON + requestBody, err := json.Marshal(invalidRequest) + if err != nil { + panic(err) + } + + req := httptest.NewRequest("PATCH", "/policy/pdpo/v1/data/valid/path", bytes.NewReader(requestBody)) + res := httptest.NewRecorder() + + patchHandler(res, req) + + assert.Equal(t, http.StatusBadRequest, res.Code) + assert.Contains(t, res.Body.String(), "Data is required and cannot be empty") +} + +func Test_GetPolicyByIDFunc_Success(t *testing.T) { + // Mock policy data + policyID := "test-policy" + dirParts := []string{"", "some", "path"} // First part is empty, should be removed + expectedDirParts := "some.path" // Expected after cleaning + + policiesMap := map[string]Policy{ + policyID: { + PolicyID: policyID, + Data: []string{"some.path"}, + }, + } + + // Mock function to return the policy + mockGetPolicyByIDFunc := func(policies map[string]Policy, id string) (Policy, error) { + policy, exists := policies[id] + if !exists { + return Policy{}, errors.New("policy not found") + } + return policy, nil + } + + // Simulating HTTP request and response recorder + // req := httptest.NewRequest("GET", "/policy/test-policy", nil) + res := httptest.NewRecorder() + + // Processing dirParts + fmt.Println("dirParts before:", dirParts) + if len(dirParts) > 0 && dirParts[0] == "" { + dirParts = dirParts[1:] // Remove first empty element + } + finalDirParts := strings.Join(dirParts, ".") + + // Ensure `dirParts` are cleaned correctly + assert.Equal(t, expectedDirParts, finalDirParts) + + // Fetch policy + matchedPolicy, err := mockGetPolicyByIDFunc(policiesMap, policyID) + if err != nil { + sendErrorResponse(res, err.Error(), http.StatusBadRequest) + return + } + + // Ensure correct policy is returned + assert.Equal(t, policyID, matchedPolicy.PolicyID) + assert.Contains(t, matchedPolicy.Data, expectedDirParts) + +} + +// Mock function for checkIfPolicyAlreadyExists +func mockCheckIfPolicyExists(policyID string) bool { + return policyID == "valid-policy" +} + +// Mock function for getPolicyByID +func mockGetPolicyByID(policiesMap map[string]Policy, policyID string) (Policy, error) { + if policyID == "valid-policy" { + return Policy{ + Data: []string{"existing.path", "valid.path"}, + }, nil + } + return Policy{}, errors.New("policy not found") +} + +func TestPatchHandler_PolicyDoesNotExist(t *testing.T) { + originalCheckFunc := checkIfPolicyAlreadyExistsVar + checkIfPolicyAlreadyExistsVar = mockCheckIfPolicyExists + defer func() { checkIfPolicyAlreadyExistsVar = originalCheckFunc }() + + ctime := "08:26:41.857Z" + timeZone := "America/New_York" + timeOffset := "+02:00" + onapComp := "COMPONENT" + onapIns := "INSTANCE" + onapName := "ONAP" + + parsedDate, err := time.Parse("2006-01-02", "2024-02-12") + if err != nil { + fmt.Println("error in parsedDate") + } + currentDate := openapi_types.Date{Time: parsedDate} + currentDateTime, err := time.Parse(time.RFC3339, "2024-02-12T12:00:00Z") + if err != nil { + fmt.Println("error in currentDateTime") + } + + requestBody := oapicodegen.OPADataUpdateRequest{ + CurrentDate: ¤tDate, + CurrentDateTime: ¤tDateTime, + CurrentTime: &ctime, + TimeOffset: &timeOffset, + TimeZone: &timeZone, + OnapComponent: &onapComp, + OnapInstance: &onapIns, + OnapName: &onapName, + + PolicyName: StringPointer("invalid-policy"), + Data: &[]map[string]interface{}{{"test": "value"}}, + } + bodyBytes, _ := json.Marshal(requestBody) + + req, err := http.NewRequest("PATCH", "/policy/pdpo/v1/data/existing.path", bytes.NewBuffer(bodyBytes)) + assert.NoError(t, err) + + rec := httptest.NewRecorder() + + patchHandler(rec, req) + + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Contains(t, rec.Body.String(), "Policy associated with the patch request does not exist") +} + +func TestPatchHandler_InvalidDataPath(t *testing.T) { + + ctime := "08:26:41.857Z" + timeZone := "America/New_York" + timeOffset := "+02:00" + onapComp := "COMPONENT" + onapIns := "INSTANCE" + onapName := "ONAP" + + parsedDate, err := time.Parse("2006-01-02", "2024-02-12") + if err != nil { + fmt.Println("error in parsedDate") + } + currentDate := openapi_types.Date{Time: parsedDate} + currentDateTime, err := time.Parse(time.RFC3339, "2024-02-12T12:00:00Z") + if err != nil { + fmt.Println("error in currentDateTime") + } + + policymap.LastDeployedPolicies = `{"deployed_policies_dict": [{"policy-id": "valid-policy","policy-version": "v1"}]}` + + requestBody := &oapicodegen.OPADataUpdateRequest{ + CurrentDate: ¤tDate, + CurrentDateTime: ¤tDateTime, + CurrentTime: &ctime, + TimeOffset: &timeOffset, + TimeZone: &timeZone, + OnapComponent: &onapComp, + OnapInstance: &onapIns, + OnapName: &onapName, + + PolicyName: StringPointer("valid-policy"), + Data: &[]map[string]interface{}{{"test": "value"}}, + } + bodyBytes, _ := json.Marshal(requestBody) + + req, err := http.NewRequest("PATCH", "/policy/pdpo/v1/data/nonexisting.path", bytes.NewBuffer(bodyBytes)) + assert.NoError(t, err) + + rec := httptest.NewRecorder() + + patchHandler(rec, req) + + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Contains(t, rec.Body.String(), "Dynamic Data add/replace/remove for policy") +} + +// Utility function to create a pointer to a string +func StringPointer(s string) *string { + return &s +} + +func TestValidatePolicyDataPathMatched_PolicyNotFound(t *testing.T) { + original := getPolicyByIDVar + defer func() { getPolicyByIDVar = original }() + + getPolicyByIDVar = func(policiesMap string, policyId string) (*Policy, error) { + return nil, fmt.Errorf("policy not found") + } + + dirParts := []string{"", "config", "a"} + res := httptest.NewRecorder() + + result := validatePolicyDataPathMatched(dirParts, "non-existent-id", res) + + assert.False(t, result) + assert.Equal(t, http.StatusBadRequest, res.Code) +} + +func TestValidatePolicyDataPathMatched_Success(t *testing.T) { + original := getPolicyByIDVar + defer func() { getPolicyByIDVar = original }() + + getPolicyByIDVar = func(policiesMap string, policyId string) (*Policy, error) { + return &Policy{ + PolicyID: policyId, + Data: []string{"config.a", "data.b"}, + }, nil + } + + dirParts := []string{"", "config", "a"} + res := httptest.NewRecorder() + + result := validatePolicyDataPathMatched(dirParts, "test-policy", res) + + assert.True(t, result) + assert.Equal(t, 200, res.Code) // or 0 if not written to +} + +func TestPatchInfos_ExtractPatchInfo_Error(t *testing.T) { + // Save original function and defer restore + originalExtractPatchInfo := extractPatchInfoVar + defer func() { extractPatchInfoVar = originalExtractPatchInfo }() + + // Mock function to return error + extractPatchInfoVar = func(res http.ResponseWriter, ops *[]map[string]interface{}, root string) ([]opasdk.PatchImpl, error) { + return nil, fmt.Errorf("mocked extractPatchInfo failure") + } + + // Dummy input + ops := &[]map[string]interface{}{ + {"op": "add", "value": "dummy"}, + } + root := "root.path" + + res := httptest.NewRecorder() + // Call the actual logic that depends on extractPatchInfoVar + patchInfos, err := extractPatchInfoVar(res, ops, root) + + // Assertions + if err == nil { + t.Fatal("expected error but got nil") + } + if patchInfos != nil { + t.Errorf("expected patchInfos to be nil, got: %v", patchInfos) + } +} diff --git a/pkg/decision/decision-provider.go b/pkg/decision/decision-provider.go index 035fb0f..3345e6f 100644 --- a/pkg/decision/decision-provider.go +++ b/pkg/decision/decision-provider.go @@ -202,7 +202,7 @@ func policyExists(policyName string, extractedPolicies []model.ToscaConceptIdent // This function processes the request headers func processRequestHeaders(req *http.Request, res http.ResponseWriter) (string, *oapicodegen.DecisionParams) { - requestId := req.Header.Get("X-ONAP-RequestID") + requestId := req.Header.Get(consts.RequestId) var parsedUUID *uuid.UUID var decisionParams *oapicodegen.DecisionParams @@ -213,13 +213,13 @@ func processRequestHeaders(req *http.Request, res http.ResponseWriter) (string, decisionParams = &oapicodegen.DecisionParams{ XONAPRequestID: (*openapi_types.UUID)(parsedUUID), } - res.Header().Set("X-ONAP-RequestID", decisionParams.XONAPRequestID.String()) + res.Header().Set(consts.RequestId, decisionParams.XONAPRequestID.String()) } else { log.Warnf("Error Parsing the requestID: %v", err) } } else { requestId = "Unknown" - res.Header().Set("X-ONAP-RequestID", requestId) + res.Header().Set(consts.RequestId, requestId) } res.Header().Set("X-LatestVersion", consts.LatestVersion) diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index e7c6769..331c9af 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -37,7 +37,7 @@ import ( // handles HTTP requests for health checks and responds with the health status of the service. func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { - requestId := r.Header.Get("X-ONAP-RequestID") + requestId := r.Header.Get(consts.RequestId) var parsedUUID *uuid.UUID var healthCheckParams *oapicodegen.HealthcheckParams @@ -50,12 +50,12 @@ func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { healthCheckParams = &oapicodegen.HealthcheckParams{ XONAPRequestID: (*openapi_types.UUID)(parsedUUID), } - w.Header().Set("X-ONAP-RequestID", healthCheckParams.XONAPRequestID.String()) + w.Header().Set(consts.RequestId, healthCheckParams.XONAPRequestID.String()) } } else { log.Warnf("Invalid or Missing Request ID") requestId = "000000000000" - w.Header().Set("X-ONAP-RequestID", requestId) + w.Header().Set(consts.RequestId, requestId) } w.Header().Set("X-LatestVersion", consts.LatestVersion) w.Header().Set("X-PatchVersion", consts.PatchVersion) diff --git a/pkg/kafkacomm/handler/pdp_message_handler.go b/pkg/kafkacomm/handler/pdp_message_handler.go index 235d56b..f473550 100644 --- a/pkg/kafkacomm/handler/pdp_message_handler.go +++ b/pkg/kafkacomm/handler/pdp_message_handler.go @@ -33,9 +33,16 @@ import ( "sync" ) +type ( + pdpUpdateMessageHandlerFunc func(message []byte, p publisher.PdpStatusSender) error + pdpStateChangeMessageHandlerFunc func(message []byte, p publisher.PdpStatusSender) error +) + var ( - shutdownFlag bool - mu sync.Mutex + shutdownFlag bool + mu sync.Mutex + pdpUpdateMessageHandlerVar pdpUpdateMessageHandlerFunc = pdpUpdateMessageHandler + pdpStateChangeMessageHandlerVar pdpStateChangeMessageHandlerFunc = pdpStateChangeMessageHandler ) // SetShutdownFlag sets the shutdown flag @@ -129,33 +136,37 @@ func PdpMessageHandler(ctx context.Context, kc *kafkacomm.KafkaConsumer, topic s continue } - switch opaPdpMessage.MessageType { + handlePdpMessageTypes(opaPdpMessage.MessageType, message, p) + } + } - case "PDP_UPDATE": - err = pdpUpdateMessageHandler(message, p) - if err != nil { - log.Warnf("Error processing Update Message: %v", err) - } + } + return nil - case "PDP_STATE_CHANGE": - err = pdpStateChangeMessageHandler(message, p) - if err != nil { - log.Warnf("Error processing Update Message: %v", err) - } +} - case "PDP_STATUS": - log.Debugf("discarding event of type PDP_STATUS") - continue - default: - log.Errorf("This is not a valid Message Type: %s", opaPdpMessage.MessageType) - continue +func handlePdpMessageTypes(messageType string, message []byte, p publisher.PdpStatusSender) { + log.Debugf("messageType: %s", messageType) + var err error + switch messageType { - } + case "PDP_UPDATE": + err = pdpUpdateMessageHandlerVar(message, p) + if err != nil { + log.Warnf("Error processing Update Message: %v", err) + } - } + case "PDP_STATE_CHANGE": + err = pdpStateChangeMessageHandlerVar(message, p) + if err != nil { + log.Warnf("Error processing Update Message: %v", err) } - } - return nil + case "PDP_STATUS": + log.Debugf("discarding event of type PDP_STATUS") + break + default: + log.Errorf("This is not a valid Message Type: %s", messageType) + } } diff --git a/pkg/kafkacomm/handler/pdp_message_handler_test.go b/pkg/kafkacomm/handler/pdp_message_handler_test.go index fd24c42..594498d 100644 --- a/pkg/kafkacomm/handler/pdp_message_handler_test.go +++ b/pkg/kafkacomm/handler/pdp_message_handler_test.go @@ -28,6 +28,7 @@ import ( "policy-opa-pdp/consts" "policy-opa-pdp/pkg/kafkacomm" "policy-opa-pdp/pkg/kafkacomm/mocks" + "policy-opa-pdp/pkg/kafkacomm/publisher" "policy-opa-pdp/pkg/pdpattributes" "testing" "time" @@ -55,7 +56,7 @@ func (m *MockKafkaConsumer) ReadMessage(kc *kafkacomm.KafkaConsumer) ([]byte, er return args.Get(0).([]byte), args.Error(0) } -func (m *MockKafkaConsumer) PdpUpdateMessageHandler(msg string) error { +func (m *MockKafkaConsumer) pdpUpdateMessageHandler(msg string) error { args := m.Called(msg) return args.Error(0) } @@ -228,17 +229,17 @@ func TestSetAndCheckShutdownFlag(t *testing.T) { func TestPdpMessageHandler_ValidPDPUpdate(t *testing.T) { t.Run("Process PDP_UPDATE Message", func(t *testing.T) { message := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_UPDATE", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_UPDATE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() // cancel is called to release resources @@ -273,17 +274,17 @@ func TestPdpMessageHandler_ValidPDPUpdate(t *testing.T) { func TestPdpMessageHandler_ValidPdpStateChange(t *testing.T) { t.Run("Process PDP STATE CHANGE Message Handler", func(t *testing.T) { message := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName": "PDP_STATE_CHANGE", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName": "PDP_STATE_CHANGE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() @@ -318,17 +319,17 @@ func TestPdpMessageHandler_ValidPdpStateChange(t *testing.T) { func TestPdpMessageHandler_DiscardPdpStatus(t *testing.T) { t.Run("Process PDP STATUS Message Handler", func(t *testing.T) { message := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_STATUS", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_STATUS", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() @@ -363,17 +364,17 @@ func TestPdpMessageHandler_DiscardPdpStatus(t *testing.T) { func TestPdpMessageHandler_InvalidMessage(t *testing.T) { t.Run("Process Invalid PDP Message Handler", func(t *testing.T) { message := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_INVALID", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_INVALID", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() @@ -408,17 +409,17 @@ func TestPdpMessageHandler_InvalidMessage(t *testing.T) { func TestPdpMessageHandler_ContextCancelled(t *testing.T) { t.Run("Context is canceled", func(t *testing.T) { message := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_INVALID", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_INVALID", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithCancel(context.Background()) cancel() // Immediately cancel the context @@ -452,17 +453,17 @@ func TestPdpMessageHandler_ContextCancelled(t *testing.T) { func TestPdpMessageHandler_InvalidOPAPdpmessage(t *testing.T) { t.Run("Invalid OPA PDP message", func(t *testing.T) { message := `{ - "":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_UPDATE", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_UPDATE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() // cancel is called to release resources @@ -495,17 +496,17 @@ func TestPdpMessageHandler_InvalidOPAPdpmessage(t *testing.T) { func TestPdpMessageHandler_InvalidOPAPdpStateChangemessage(t *testing.T) { t.Run("Invalid OPA PDP State Change message", func(t *testing.T) { message := `{ - "sourc":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_STATE_CHANGE", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "sourc":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_STATE_CHANGE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() @@ -538,17 +539,17 @@ func TestPdpMessageHandler_InvalidOPAPdpStateChangemessage(t *testing.T) { func TestPdpMessageHandler_jsonunmarshallOPAPdpStateChangemessage(t *testing.T) { t.Run("Invalid OPA PDP State Change message", func(t *testing.T) { message := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_STATE_CHANGE" - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_STATE_CHANGE" + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }` ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() @@ -577,3 +578,56 @@ func TestPdpMessageHandler_jsonunmarshallOPAPdpStateChangemessage(t *testing.T) }) } + +func TestHandlePdpMessageTypes_Update(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + pdpUpdateMessageHandlerVar = func(msg []byte, p publisher.PdpStatusSender) error { + assert.Equal(t, "update-message", string(msg)) + return nil + } + + handlePdpMessageTypes("PDP_UPDATE", []byte("update-message"), mockSender) + // Add assertions on log output if needed +} + +func TestHandlePdpMessageTypes_Update_Error(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + pdpUpdateMessageHandlerVar = func(msg []byte, p publisher.PdpStatusSender) error { + return errors.New("mock update error") + } + + handlePdpMessageTypes("PDP_UPDATE", []byte("bad-message"), mockSender) +} + +func TestHandlePdpMessageTypes_StateChange(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + pdpStateChangeMessageHandlerVar = func(msg []byte, p publisher.PdpStatusSender) error { + assert.Equal(t, "state-change", string(msg)) + return nil + } + + handlePdpMessageTypes("PDP_STATE_CHANGE", []byte("state-change"), mockSender) +} + +func TestHandlePdpMessageTypes_StateChange_Error(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + pdpStateChangeMessageHandlerVar = func(msg []byte, p publisher.PdpStatusSender) error { + return errors.New("mock state change error") + } + + handlePdpMessageTypes("PDP_STATE_CHANGE", []byte("bad-state"), mockSender) +} + +func TestHandlePdpMessageTypes_Status(t *testing.T) { + mockSender := new(MockPdpStatusSender) + handlePdpMessageTypes("PDP_STATUS", []byte("ignore"), mockSender) +} + +func TestHandlePdpMessageTypes_Invalid(t *testing.T) { + mockSender := new(MockPdpStatusSender) + handlePdpMessageTypes("INVALID_TYPE", []byte("invalid"), mockSender) +} diff --git a/pkg/kafkacomm/handler/pdp_state_change_handler_test.go b/pkg/kafkacomm/handler/pdp_state_change_handler_test.go index 8bddbca..d66da87 100644 --- a/pkg/kafkacomm/handler/pdp_state_change_handler_test.go +++ b/pkg/kafkacomm/handler/pdp_state_change_handler_test.go @@ -78,9 +78,23 @@ func TestPdpStateChangeMessageHandler(t *testing.T) { expectError: false, checkNotEqual: false, }, + "Empty SendStateChangeResponse": { + message: []byte(`{"state":"Hello"}`), + expectedState: "PASSIVE", + mockError: assert.AnError, + expectError: true, + checkNotEqual: false, + }, + "Empty SendStateChangeJson": { + message: []byte(`{}`), + expectedState: "PASSIVE", + mockError: assert.AnError, + expectError: false, + checkNotEqual: false, + }, } - orderedKeys := []string{"Valid state change", "Invalid JSON", "Error in SendStateChangeResponse"} + orderedKeys := []string{"Valid state change", "Invalid JSON", "Error in SendStateChangeResponse", "Empty SendStateChangeResponse", "Empty SendStateChangeJson"} for _, name := range orderedKeys { tt := tests[name] diff --git a/pkg/kafkacomm/handler/pdp_update_deploy_policy.go b/pkg/kafkacomm/handler/pdp_update_deploy_policy.go index 5d6cd93..5c07ddd 100644 --- a/pkg/kafkacomm/handler/pdp_update_deploy_policy.go +++ b/pkg/kafkacomm/handler/pdp_update_deploy_policy.go @@ -221,25 +221,6 @@ func extractAndDecodeData(policy model.ToscaPolicy) (map[string]string, []string return decodedData, keys, nil } -// Function to extract folder name based on policy -func getDirName(policy model.ToscaPolicy) []string { - // Split the policy name to identify the folder part (i.e., the first part before ".") - - var dirNames []string - - for key, _ := range policy.Properties.Data { - - dirNames = append(dirNames, strings.ReplaceAll(consts.DataNode+key, ".", "/")) - - } - for key, _ := range policy.Properties.Policy { - - dirNames = append(dirNames, strings.ReplaceAll(consts.Policies+"/"+key, ".", "/")) - - } - - return dirNames -} // upsert policy to sdk. func upsertPolicy(policy model.ToscaPolicy) error { @@ -294,58 +275,13 @@ func handlePolicyDeployment(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSend pdpUpdate.PoliciesToBeDeployed = checkIfPolicyAlreadyDeployed(pdpUpdate) for _, policy := range pdpUpdate.PoliciesToBeDeployed { - // Validate the policy - - policyAllowed, err := validateParentPolicyVar(policy) - if err != nil { - log.Warnf("Tosca Policy Id validation failed for policy nameas it is a parent folder:%s, %v", policy.Name, err) - failureMessages = append(failureMessages, fmt.Sprintf("%s, %v", policy.Name, err)) - metrics.IncrementDeployFailureCount() - metrics.IncrementTotalErrorCount() - continue - } - if policyAllowed { - log.Debugf("Policy Is Allowed: %s", policy.Name) - } - - if err := utils.ValidateToscaPolicyJsonFields(policy); err != nil { - log.Debugf("Tosca Policy Validation Failed for policy Name: %s, %v", policy.Name, err) - failureMessages = append(failureMessages, fmt.Sprintf("Tosca Policy Validation failed for Policy: %s: %v", policy.Name, err)) - metrics.IncrementDeployFailureCount() - metrics.IncrementTotalErrorCount() + if err := validateAndPreparePolicy(policy, &failureMessages); err != nil { continue } - - // Create and store policy data - if err := createAndStorePolicyDataVar(policy); err != nil { - failureMessages = append(failureMessages, fmt.Sprintf("%s: %v", policy.Name, err)) - metrics.IncrementDeployFailureCount() - metrics.IncrementTotalErrorCount() - continue - } - - // Build the bundle - if err := verifyPolicyByBundleCreation(policy); err != nil { - failureMessages = append(failureMessages, fmt.Sprintf("Failed to build Rego File for %s: %v", policy.Name, err)) - metrics.IncrementDeployFailureCount() - metrics.IncrementTotalErrorCount() - continue - } - - // Upsert policy and data - if err := upsertPolicyAndData(policy, successPolicies); err != nil { + if err := deployPolicyAndData(policy, successPolicies); err != nil { failureMessages = append(failureMessages, err.Error()) - metrics.IncrementDeployFailureCount() - metrics.IncrementTotalErrorCount() continue - } else { - successPolicies[policy.Name] = policy.Version - if _, err := policymap.UpdateDeployedPoliciesinMap(policy); err != nil { - log.Warnf("Failed to store policy data map after deploying policy %s: %v", policy.Name, err) - } } - metrics.IncrementDeploySuccessCount() - log.Debugf("Loaded Policy: %s", policy.Name) } @@ -353,6 +289,71 @@ func handlePolicyDeployment(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSend metrics.SetTotalPoliciesCount(int64(totalPolicies)) return failureMessages, successPolicies + +} + +func validateAndPreparePolicy(policy model.ToscaPolicy, failureMessages *[]string) error { + // Validate the policy + + policyAllowed, err := validateParentPolicyVar(policy) + if err != nil { + log.Warnf("Tosca Policy Id validation failed for policy nameas it is a parent folder:%s, %v", policy.Name, err) + *failureMessages = append(*failureMessages, fmt.Sprintf("%s, %v", policy.Name, err)) + metrics.IncrementDeployFailureCount() + metrics.IncrementTotalErrorCount() + return err + } + if policyAllowed { + log.Debugf("Policy Is Allowed: %s", policy.Name) + } else { + return fmt.Errorf("policy not Allowed") + } + + if err := utils.ValidateToscaPolicyJsonFields(policy); err != nil { + log.Debugf("Tosca Policy Validation Failed for policy Name: %s, %v", policy.Name, err) + *failureMessages = append(*failureMessages, fmt.Sprintf("Tosca Policy Validation failed for Policy: %s: %v", policy.Name, err)) + metrics.IncrementDeployFailureCount() + metrics.IncrementTotalErrorCount() + return err + } + + // Create and store policy data + if err := createAndStorePolicyDataVar(policy); err != nil { + *failureMessages = append(*failureMessages, fmt.Sprintf("%s: %v", policy.Name, err)) + metrics.IncrementDeployFailureCount() + metrics.IncrementTotalErrorCount() + return err + } + return nil + +} + +func deployPolicyAndData(policy model.ToscaPolicy, successPolicies map[string]string) error { + + // Build the bundle + if output, err := verifyPolicyByBundleCreation(policy); err != nil { + if len(output) > consts.MaxOutputResponseLength { + output = output[:consts.MaxOutputResponseLength] + "..." + } + metrics.IncrementDeployFailureCount() + metrics.IncrementTotalErrorCount() + return fmt.Errorf("Failed to build Rego File for %s: %v", policy.Name, string(output)) + } + + // Upsert policy and data + if err := upsertPolicyAndData(policy, successPolicies); err != nil { + metrics.IncrementDeployFailureCount() + metrics.IncrementTotalErrorCount() + return err + } else { + successPolicies[policy.Name] = policy.Version + if _, err := policymap.UpdateDeployedPoliciesinMap(policy); err != nil { + log.Warnf("Failed to store policy data map after deploying policy %s: %v", policy.Name, err) + } + } + metrics.IncrementDeploySuccessCount() + log.Debugf("Loaded Policy: %s", policy.Name) + return nil } // checks if policy exists in the map. @@ -365,7 +366,7 @@ func checkIfPolicyAlreadyDeployed(pdpUpdate model.PdpUpdate) []model.ToscaPolicy } // verfies policy by creating bundle. -func verifyPolicyByBundleCreation(policy model.ToscaPolicy) error { +func verifyPolicyByBundleCreation(policy model.ToscaPolicy) (string, error) { // get directory name dirNames := []string{strings.ReplaceAll(consts.DataNode+"/"+policy.Name, ".", "/"), strings.ReplaceAll(consts.Policies+"/"+policy.Name, ".", "/")} // create bundle @@ -378,9 +379,9 @@ func verifyPolicyByBundleCreation(policy model.ToscaPolicy) error { } } log.Debugf("Directory cleanup as bundle creation failed") - return fmt.Errorf("failed to build bundle: %v", err) + return output, fmt.Errorf("failed to build bundle: %v", err) } - return nil + return output, nil } // handles Upsert func for policy and data diff --git a/pkg/kafkacomm/handler/pdp_update_deploy_policy_test.go b/pkg/kafkacomm/handler/pdp_update_deploy_policy_test.go index e95bbeb..47efdef 100644 --- a/pkg/kafkacomm/handler/pdp_update_deploy_policy_test.go +++ b/pkg/kafkacomm/handler/pdp_update_deploy_policy_test.go @@ -35,6 +35,7 @@ import ( "policy-opa-pdp/pkg/utils" "strings" "testing" + "fmt" ) func TestValidatePackageName(t *testing.T) { @@ -370,7 +371,7 @@ func TestVerifyPolicyByBundleCreation(t *testing.T) { createBundleFuncVar = func(execCmd func(string, ...string) *exec.Cmd, toscaPolicy model.ToscaPolicy) (string, error) { return "", nil } - err := verifyPolicyByBundleCreation(policy) + _, err := verifyPolicyByBundleCreation(policy) assert.NoError(t, err) } @@ -385,7 +386,7 @@ func TestVerifyPolicyByBundleCreation_getDirEmpty(t *testing.T) { } //Mocking the CreateBundle - err := verifyPolicyByBundleCreation(policy) + _, err := verifyPolicyByBundleCreation(policy) assert.NoError(t, err) } @@ -403,7 +404,7 @@ func TestVerifyPolicyByBundleCreation_BundleFailure(t *testing.T) { createBundleFuncVar = func(execCmd func(string, ...string) *exec.Cmd, toscaPolicy model.ToscaPolicy) (string, error) { return "", errors.New("Fail to Initialize Bundle") } - err := verifyPolicyByBundleCreation(policy) + _, err := verifyPolicyByBundleCreation(policy) assert.Error(t, err) } @@ -745,7 +746,8 @@ func TestHandlePolicyDeployment_VerifyBundleFailure(t *testing.T) { err, _ := handlePolicyDeployment(pdpUpdate, mockSender) found := false for _, message := range err { - if strings.Contains(message, "Failed to Bundle") { + fmt.Println(message) + if strings.Contains(message, "Failed to build Rego File for") { found = true break } diff --git a/pkg/kafkacomm/handler/pdp_update_message_handler.go b/pkg/kafkacomm/handler/pdp_update_message_handler.go index 9268115..77475d6 100644 --- a/pkg/kafkacomm/handler/pdp_update_message_handler.go +++ b/pkg/kafkacomm/handler/pdp_update_message_handler.go @@ -34,17 +34,25 @@ import ( ) type ( - sendSuccessResponseFunc func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, respMessage string) error - sendFailureResponseFunc func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, respMessage error) error - createBundleFuncRef func(execCmd func(string, ...string) *exec.Cmd, toscaPolicy model.ToscaPolicy) (string, error) + sendSuccessResponseFunc func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, respMessage string) error + sendFailureResponseFunc func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, respMessage error) error + createBundleFuncRef func(execCmd func(string, ...string) *exec.Cmd, toscaPolicy model.ToscaPolicy) (string, error) + handlePdpUpdateDeploymentFunc func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) + handlePdpUpdateUndeploymentFunc func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) + sendFinalResponseFunc func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, loggingPoliciesList string, failureMessages []string) error ) var ( - basePolicyDir = consts.Policies - baseDataDir = consts.Data - sendSuccessResponseVar sendSuccessResponseFunc = sendSuccessResponse - sendFailureResponseVar sendFailureResponseFunc = sendFailureResponse - createBundleFuncVar createBundleFuncRef = createBundleFunc + basePolicyDir = consts.Policies + baseDataDir = consts.Data + sendSuccessResponseVar sendSuccessResponseFunc = sendSuccessResponse + sendFailureResponseVar sendFailureResponseFunc = sendFailureResponse + sendFinalResponseVar sendFinalResponseFunc = sendFinalResponse + createBundleFuncVar createBundleFuncRef = createBundleFunc + handlePdpUpdateDeploymentVar handlePdpUpdateDeploymentFunc = handlePdpUpdateDeployment + handlePdpUpdateUndeploymentVar handlePdpUpdateUndeploymentFunc = handlePdpUpdateUndeployment + sendPDPStatusResponseFunc = sendPDPStatusResponse + ) // Handles messages of type PDP_UPDATE sent from the Policy Administration Point (PAP). @@ -57,20 +65,20 @@ func pdpUpdateMessageHandler(message []byte, p publisher.PdpStatusSender) error err := json.Unmarshal(message, &pdpUpdate) if err != nil { log.Debugf("Failed to UnMarshal Messages: %v\n", err) - resMessage := fmt.Errorf("PDP Update Failed: %v", err) + resMessage := fmt.Errorf("PDP Update Failed as failed Unmarshalling: %v", err) if err := sendFailureResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Debugf("Failed to send update error response: %v", err) + log.Debugf("Failed to send update unmarshal error response: %v", err) return err } return err } //Initialize Validator and validate Struct after unmarshalling - err = utils.ValidateFieldsStructs(pdpUpdate) + err = utils.ValidateFieldsStructsVar(pdpUpdate) if err != nil { resMessage := fmt.Errorf("PDP Update Failed: %v", err) if err := sendFailureResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Debugf("Failed to send update error response: %v", err) + log.Debugf("Failed to send pdp update validate fields error response: %v", err) return err } return err @@ -79,66 +87,103 @@ func pdpUpdateMessageHandler(message []byte, p publisher.PdpStatusSender) error log.Debugf("PDP_UPDATE Message received: %s", string(message)) pdpattributes.SetPdpSubgroup(pdpUpdate.PdpSubgroup) + pdpattributes.SetPdpHeartbeatInterval(pdpUpdate.PdpHeartbeatIntervalMs) + + depPoliciesList, err, depFailures := handlePdpUpdateDeploymentVar(pdpUpdate, p) + undepPoliciesList, undepErr, undepFailures := handlePdpUpdateUndeploymentVar(pdpUpdate, p) + + if err != nil { + return err + } + + if undepErr != nil { + return undepErr + } + failureMessages = append(depFailures, undepFailures...) + loggingPoliciesList = depPoliciesList + + if undepPoliciesList != "" { + loggingPoliciesList += "," + undepPoliciesList + } + + err = sendFinalResponseVar(p, &pdpUpdate, loggingPoliciesList, failureMessages) + if err != nil { + return err + } + + log.Infof("PDP_STATUS Message Sent Successfully") + log.Debug(pdpUpdate.PdpHeartbeatIntervalMs) + + if pdpattributes.PdpHeartbeatInterval != pdpUpdate.PdpHeartbeatIntervalMs && pdpUpdate.PdpHeartbeatIntervalMs != 0 { + //restart the ticker. + publisher.StopTicker() + pdpattributes.SetPdpHeartbeatInterval(pdpUpdate.PdpHeartbeatIntervalMs) + go publisher.StartHeartbeatIntervalTimer(pdpattributes.PdpHeartbeatInterval, p) + } + go publisher.StartHeartbeatIntervalTimer(pdpattributes.PdpHeartbeatInterval, p) + return nil + +} + +func handlePdpUpdateDeployment(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { + var failureMessages []string + var mapJson string + var err error if len(pdpUpdate.PoliciesToBeDeployed) > 0 { failureMessage, successfullyDeployedPolicies := handlePolicyDeploymentVar(pdpUpdate, p) - mapJson, err := policymap.FormatMapofAnyType(successfullyDeployedPolicies) + mapJson, err = policymap.FormatMapOfAnyTypeVar(successfullyDeployedPolicies) if len(failureMessage) > 0 { failureMessages = append(failureMessages, "{Deployment Errors:"+strings.Join(failureMessage, "")+"}") } if err != nil { failureMessages = append(failureMessages, "|Internal Map Error:"+err.Error()+"|") - resMessage := fmt.Errorf("PDP Update Failed: failed to format successfullyDeployedPolicies json %v", failureMessages) + resMessage := fmt.Errorf("PDP Update Failed as failed to format successfullyDeployedPolicies json %v", failureMessages) if err = sendFailureResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Debugf("Failed to send update error response: %v", err) - return err + log.Debugf("Failed to send update internal map error response: %v", err) + return "", err, failureMessages } } - loggingPoliciesList = mapJson + } + return mapJson, nil, failureMessages +} + +func handlePdpUpdateUndeployment(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { + var failureMessages []string + var mapJson string + var err error + // Check if "PoliciesToBeUndeployed" is empty or not if len(pdpUpdate.PoliciesToBeUndeployed) > 0 { log.Infof("Found Policies to be undeployed") failureMessage, successfullyUndeployedPolicies := handlePolicyUndeploymentVar(pdpUpdate, p) - mapJson, err := policymap.FormatMapofAnyType(successfullyUndeployedPolicies) + mapJson, err = policymap.FormatMapOfAnyTypeVar(successfullyUndeployedPolicies) if len(failureMessage) > 0 { failureMessages = append(failureMessages, "{UnDeployment Errors:"+strings.Join(failureMessage, "")+"}") } if err != nil { failureMessages = append(failureMessages, "|Internal Map Error:"+err.Error()+"|") - resMessage := fmt.Errorf("PDP Update Failed: failed to format successfullyUnDeployedPolicies json %v", failureMessages) + resMessage := fmt.Errorf("PDP Update Failed as failed to format successfullyUnDeployedPolicies json %v", failureMessages) if err = sendFailureResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Debugf("Failed to send update error response: %v", err) - return err + log.Debugf("Failed to send update error response: %v", err) + return "", err, failureMessages } } - loggingPoliciesList = mapJson } + return mapJson, nil, failureMessages +} +func sendFinalResponse(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, loggingPoliciesList string, failureMessages []string) error { if len(pdpUpdate.PoliciesToBeDeployed) == 0 && len(pdpUpdate.PoliciesToBeUndeployed) == 0 { //Response for PAP Registration - err = sendSuccessResponseVar(p, &pdpUpdate, "PDP UPDATE is successfull") - if err != nil { - log.Debugf("Failed to Send Update Response Message: %v\n", err) - return err - } - } else { - //Send Response for Deployment or Undeployment or when both deployment and undeployment comes together - if err := sendPDPStatusResponse(pdpUpdate, p, loggingPoliciesList, failureMessages); err != nil { - return err - } + return sendSuccessResponseVar(p, pdpUpdate, "PDP UPDATE is successfull") } - log.Infof("PDP_STATUS Message Sent Successfully") - log.Debug(pdpUpdate.PdpHeartbeatIntervalMs) - - if pdpattributes.PdpHeartbeatInterval != pdpUpdate.PdpHeartbeatIntervalMs && pdpUpdate.PdpHeartbeatIntervalMs != 0 { - //restart the ticker. - publisher.StopTicker() - pdpattributes.SetPdpHeartbeatInterval(pdpUpdate.PdpHeartbeatIntervalMs) - go publisher.StartHeartbeatIntervalTimer(pdpattributes.PdpHeartbeatInterval, p) + //Send Response for Deployment or Undeployment or when both deployment and undeployment comes together + if err := sendPDPStatusResponseFunc(*pdpUpdate, p, loggingPoliciesList, failureMessages); err != nil { + return err } - return nil } @@ -148,7 +193,7 @@ func createBundleFunc(execCmd func(string, ...string) *exec.Cmd, toscaPolicy mod } func sendSuccessResponse(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, respMessage string) error { - if err := publisher.SendPdpUpdateResponse(p, pdpUpdate, respMessage); err != nil { + if err := publisher.SendPdpUpdateResponseVar(p, pdpUpdate, respMessage); err != nil { return err } return nil @@ -163,37 +208,38 @@ func sendFailureResponse(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate func sendPDPStatusResponse(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender, loggingPoliciesList string, failureMessages []string) error { if len(failureMessages) > 0 { - resMessage := fmt.Errorf("PDP Update Failed: %v", failureMessages) - if err := sendFailureResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Warnf("Failed to send update error response: %v", err) - return err - } + return sendPDPStatusFailureResponse(pdpUpdate, p, loggingPoliciesList, failureMessages) } else { + return sendPDPStatusSuccessResponse(pdpUpdate, p, loggingPoliciesList) + } +} - if len(pdpUpdate.PoliciesToBeUndeployed) == 0 { - resMessage := fmt.Sprintf("PDP Update Successful for all policies: %v", loggingPoliciesList) - if err := sendSuccessResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Warnf("Failed to send update response: %v", err) - return err - } - log.Infof("Processed policies_to_be_deployed successfully") - } else if len(pdpUpdate.PoliciesToBeDeployed) == 0 { +func sendPDPStatusFailureResponse(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender, loggingPoliciesList string, failureMessages []string) error { + resMessage := fmt.Errorf("PDP Update Failed: %v", failureMessages) + if err := sendFailureResponseVar(p, &pdpUpdate, resMessage); err != nil { + log.Warnf("Failed to send update error response: %v", err) + return err + } + return nil +} - resMessage := fmt.Sprintf("PDP Update Policies undeployed :%v", loggingPoliciesList) +func sendPDPStatusSuccessResponse(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender, loggingPoliciesList string) error { - if err := sendSuccessResponseVar(p, &pdpUpdate, resMessage); err != nil { - log.Warnf("Failed to Send Update Response Message: %v", err) - return err - } - log.Infof("Processed policies_to_be_undeployed successfully") - } else { + var resMessage string - if err := sendSuccessResponseVar(p, &pdpUpdate, "PDP UPDATE is successfull"); err != nil { - log.Warnf("Failed to Send Update Response Message: %v", err) - return err - } - } + if len(pdpUpdate.PoliciesToBeDeployed) > 0 { + resMessage = fmt.Sprintf("PDP Update Successful for all policies: %v", loggingPoliciesList) + log.Infof("Processed policies_to_be_deployed successfully") + } else if len(pdpUpdate.PoliciesToBeUndeployed) > 0 { + resMessage = fmt.Sprintf("PDP Update Policies undeployed :%v", loggingPoliciesList) + log.Infof("Processed policies_to_be_undeployed successfully") + } else { + resMessage = "PDP_UPDATE is successful" + } + if err := sendSuccessResponseVar(p, &pdpUpdate, resMessage); err != nil { + log.Warnf("Failed to Send Update Response Message: %v", err) + return err } return nil } diff --git a/pkg/kafkacomm/handler/pdp_update_message_handler_test.go b/pkg/kafkacomm/handler/pdp_update_message_handler_test.go index e7b27fd..ac39ff0 100644 --- a/pkg/kafkacomm/handler/pdp_update_message_handler_test.go +++ b/pkg/kafkacomm/handler/pdp_update_message_handler_test.go @@ -26,70 +26,17 @@ import ( "policy-opa-pdp/pkg/kafkacomm/publisher/mocks" "policy-opa-pdp/pkg/model" "policy-opa-pdp/pkg/policymap" + "policy-opa-pdp/pkg/utils" "testing" ) /* -var ( - handlePolicyDeploymentFunc = handlePolicyDeployment -)*/ - -/* -PdpUpdateMessageHandler_success -Description: Test by sending a valid input message for pdp update -Input: valid input -Expected Output: PDP Update Message should be sent sucessfully. -*/ -func TestPdpUpdateMessageHandler_Success(t *testing.T) { - - messageString := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed":[], - "policiesToBeUndeployed":[], - "messageName":"PDP_UPDATE", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` - - mockSender := new(mocks.PdpStatusSender) - mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - - err := pdpUpdateMessageHandler([]byte(messageString), mockSender) - assert.NoError(t, err) - -} - -/* -PdpUpdateMessageHandler_Message_Unmarshal_Failure1 +pdpUpdateMessageHandler_Message_Unmarshal_Failure2 Description: Test by sending a invalid input message which should result in a Json unmarhsal error Input: invalid input Message by renaming params or removing certain params Expected Output: Message Handler should exit gracefully stating the error. */ -func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure1(t *testing.T) { - - // sending only source parameter in the message string - messageString := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0"}` - - mockSender := new(mocks.PdpStatusSender) - mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Jsonunmarshal Error")) - - err := pdpUpdateMessageHandler([]byte(messageString), mockSender) - assert.Error(t, err) - -} - -/* -PdpUpdateMessageHandler_Message_Unmarshal_Failure2 -Description: Test by sending a invalid input message which should result in a Json unmarhsal error -Input: invalid input Message by renaming params or removing certain params -Expected Output: Message Handler should exit gracefully stating the error. -*/ -func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure2(t *testing.T) { +func TestpdpUpdateMessageHandler_Message_Unmarshal_Failure2(t *testing.T) { // invlaid params by mispelling a param "source" @@ -105,18 +52,18 @@ func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure2(t *testing.T) { } /* -PdpUpdateMessageHandler_Message_Unmarshal_Failure3 +pdpUpdateMessageHandler_Message_Unmarshal_Failure3 Description: Test by sending a invalid input message which should result in a Json unmarhsal error Input: {} Expected Output: Message Handler should exit gracefully stating the error. */ -func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure3(t *testing.T) { +func TestpdpUpdateMessageHandler_Message_Unmarshal_Failure3(t *testing.T) { // invlaid params by mispelling a param "source" messageString := `{ - "soce:"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000}` + "soce:"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000}` mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Jsonunmarshal Error")) @@ -126,12 +73,12 @@ func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure3(t *testing.T) { } /* -PdpUpdateMessageHandler_Message_Unmarshal_Failure4 +pdpUpdateMessageHandler_Message_Unmarshal_Failure4 Description: Test by sending a invalid input message which should result in a Json unmarhsal error Input: empty Expected Output: Message Handler should exit gracefully stating the error. */ -func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure4(t *testing.T) { +func TestpdpUpdateMessageHandler_Message_Unmarshal_Failure4(t *testing.T) { // invlaid params by mispelling a param "source" @@ -145,12 +92,12 @@ func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure4(t *testing.T) { } /* -PdpUpdateMessageHandler_Fails_Sending_PdpUpdateResponse +pdpUpdateMessageHandler_Fails_Sending_PdpUpdateResponse Description: Test by sending a invalid attribute for pdpstate which should result in a failure in sending pdp update response Input: invalid input config set for pdpstate Expected Output: Message Handler should exit gracefully stating the error. */ -func TestPdpUpdateMessageHandler_Fails_Sending_UpdateResponse(t *testing.T) { +func TestpdpUpdateMessageHandler_Fails_Sending_UpdateResponse(t *testing.T) { // invalid value set to pdpSubgroup -->empty "" messageString := `{ @@ -163,7 +110,7 @@ func TestPdpUpdateMessageHandler_Fails_Sending_UpdateResponse(t *testing.T) { "timestampMs":1730722305297, "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", "pdpGroup":"opaGroup" - }` + }` mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Error in Sending PDP Update Response")) @@ -174,12 +121,12 @@ func TestPdpUpdateMessageHandler_Fails_Sending_UpdateResponse(t *testing.T) { } /* -PdpUpdateMessageHandler_Invalid_Starttimeinterval +pdpUpdateMessageHandler_Invalid_Starttimeinterval Description: Test by sending a invalid time value attribute for pdpstate which should result in a failure in starting heartbeat interval Input: invalid input message for pdpstate heartbeat interval Expected Output: Message Handler should exit gracefully stating the error. */ -func TestPdpUpdateMessageHandler_Invalid_Starttimeinterval(t *testing.T) { +func TestpdpUpdateMessageHandler_Invalid_Starttimeinterval(t *testing.T) { //invalid interval set to negative -1000 messageString := `{ @@ -193,7 +140,7 @@ func TestPdpUpdateMessageHandler_Invalid_Starttimeinterval(t *testing.T) { "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", "pdpGroup":"opaGroup", "pdpSubgroup":"opa" - }` + }` mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Invalid Interval Time for Heartbeat")) @@ -204,9 +151,9 @@ func TestPdpUpdateMessageHandler_Invalid_Starttimeinterval(t *testing.T) { } /* -PdpUpdateMessageHandler_Successful_Deployment +pdpUpdateMessageHandler_Successful_Deployment */ -func TestPdpUpdateMessageHandler_Invalid_Deployment(t *testing.T) { +func TestpdpUpdateMessageHandler_Invalid_Deployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -231,9 +178,9 @@ func TestPdpUpdateMessageHandler_Invalid_Deployment(t *testing.T) { } /* -PdpUpdateMessageHandler_Successful_Deployment +pdpUpdateMessageHandler_Successful_Deployment */ -func TestPdpUpdateMessageHandler_Successful_Deployment(t *testing.T) { +func TestpdpUpdateMessageHandler_Successful_Deployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -260,9 +207,9 @@ func TestPdpUpdateMessageHandler_Successful_Deployment(t *testing.T) { } /* -PdpUpdateMessageHandler_Skipping_Deployment +pdpUpdateMessageHandler_Skipping_Deployment */ -func TestPdpUpdateMessageHandler_Skipping_Deployment(t *testing.T) { +func TestpdpUpdateMessageHandler_Skipping_Deployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -285,9 +232,9 @@ func TestPdpUpdateMessageHandler_Skipping_Deployment(t *testing.T) { } /* -PdpUpdateMessageHandler_FailureIn_Deployment_UnDeployment +pdpUpdateMessageHandler_FailureIn_Deployment_UnDeployment */ -func TestPdpUpdateMessageHandler_FailureIn_Deployment_UnDeployment(t *testing.T) { +func TestpdpUpdateMessageHandler_FailureIn_Deployment_UnDeployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -311,7 +258,7 @@ func TestPdpUpdateMessageHandler_FailureIn_Deployment_UnDeployment(t *testing.T) return nil, map[string]string{"zone": "1.0.0"} } //mock the policy undeployment - handlePolicyUndeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + handlePolicyDeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { return nil, map[string]string{"role": "1.0.0"} } @@ -320,9 +267,9 @@ func TestPdpUpdateMessageHandler_FailureIn_Deployment_UnDeployment(t *testing.T) } /* -PdpUpdateMessageHandler_Successful_Undeployment +pdpUpdateMessageHandler_Successful_Undeployment */ -func TestPdpUpdateMessageHandler_Successful_Undeployment(t *testing.T) { +func TestpdpUpdateMessageHandler_Successful_Undeployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -338,7 +285,7 @@ func TestPdpUpdateMessageHandler_Successful_Undeployment(t *testing.T) { policymap.LastDeployedPolicies = `{"deployed_policies_dict": [{"data": ["zone"],"policy": ["zone"],"policy-id": "zone","policy-version": "1.0.0"}]}` //mock the policy undeployment - handlePolicyUndeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + handlePolicyDeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { return nil, map[string]string{"zone": "1.0.0"} } @@ -350,9 +297,9 @@ func TestPdpUpdateMessageHandler_Successful_Undeployment(t *testing.T) { } /* -PdpUpdateMessageHandler_Successful_Registration +pdpUpdateMessageHandler_Successful_Registration */ -func TestPdpUpdateMessageHandler_Successful_Registration(t *testing.T) { +func TestpdpUpdateMessageHandler_Successful_Registration(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -373,9 +320,9 @@ func TestPdpUpdateMessageHandler_Successful_Registration(t *testing.T) { } /* -PdpUpdateMessageHandler_Unsuccessful_Undeployment +pdpUpdateMessageHandler_Unsuccessful_Undeployment */ -func TestPdpUpdateMessageHandler_UnSuccessful_Undeployment(t *testing.T) { +func TestpdpUpdateMessageHandler_UnSuccessful_Undeployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -391,7 +338,7 @@ func TestPdpUpdateMessageHandler_UnSuccessful_Undeployment(t *testing.T) { policymap.LastDeployedPolicies = `{"deployed_policies_dict": []}` //mock the policy undeployment - handlePolicyUndeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + handlePolicyDeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { return []string{"Error in undeployment"}, map[string]string{} } @@ -403,9 +350,9 @@ func TestPdpUpdateMessageHandler_UnSuccessful_Undeployment(t *testing.T) { } /* -PdpUpdateMessageHandler_Partial_FailureIn_Undeployment +pdpUpdateMessageHandler_Partial_FailureIn_Undeployment */ -func TestPdpUpdateMessageHandler_Partial_FailureIn_Undeployment(t *testing.T) { +func TestpdpUpdateMessageHandler_Partial_FailureIn_Undeployment(t *testing.T) { messageString := `{ "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", "pdpHeartbeatIntervalMs":120000, @@ -421,10 +368,18 @@ func TestPdpUpdateMessageHandler_Partial_FailureIn_Undeployment(t *testing.T) { policymap.LastDeployedPolicies = `{"deployed_policies_dict": [{"data": ["zone"],"policy": ["zone"],"policy-id": "zone","policy-version": "1.0.0"}]}` //mock the policy undeployment - handlePolicyUndeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + handlePdpUpdateUndeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { - return []string{"Error in undeployment"}, map[string]string{"zone:": "1.0.0"} + return "", errors.New("error in undeployment"), []string{} } + sendFinalResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, policies string, failures []string) error { + assert.Equal(t, "{}", policies) + return nil + } + defer func() { + sendFinalResponseVar = sendFinalResponse + handlePdpUpdateUndeploymentVar = handlePdpUpdateUndeployment + }() mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("error in undeployment")) @@ -432,35 +387,6 @@ func TestPdpUpdateMessageHandler_Partial_FailureIn_Undeployment(t *testing.T) { assert.Error(t, err) } -/* -PdpUpdateMessageHandler_Unsuccessful_Deployment -*/ -func TestPdpUpdateMessageHandler_Unsuccessful_Deployment(t *testing.T) { - messageString := `{ - "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", - "pdpHeartbeatIntervalMs":120000, - "policiesToBeDeployed": [{"type": "onap.policies.native.opa","type_version": "1.0.0","properties": {"data": {"zone": "ewogICJ6b25lIjogewogICAgInpvbmVfYWNjZXNzX2xvZ3MiOiBbCiAgICAgIHsgImxvZ19pZCI6ICJsb2cxIiwgInRpbWVzdGFtcCI6ICIyMDI0LTExLTAxVDA5OjAwOjAwWiIsICJ6b25lX2lkIjogInpvbmVBIiwgImFjY2VzcyI6ICJncmFudGVkIiwgInVzZXIiOiAidXNlcjEiIH0sCiAgICAgIHsgImxvZ19pZCI6ICJsb2cyIiwgInRpbWVzdGFtcCI6ICIyMDI0LTExLTAxVDEwOjMwOjAwWiIsICJ6b25lX2lkIjogInpvbmVBIiwgImFjY2VzcyI6ICJkZW5pZWQiLCAidXNlciI6ICJ1c2VyMiIgfSwKICAgICAgeyAibG9nX2lkIjogImxvZzMiLCAidGltZXN0YW1wIjogIjIwMjQtMTEtMDFUMTE6MDA6MDBaIiwgInpvbmVfaWQiOiAiem9uZUIiLCAiYWNjZXNzIjogImdyYW50ZWQiLCAidXNlciI6ICJ1c2VyMyIgfQogICAgXQogIH0KfQo="},"policy": {"zone": "cGFja2FnZSB6b25lCgppbXBvcnQgcmVnby52MQoKZGVmYXVsdCBhbGxvdyA6PSBmYWxzZQoKYWxsb3cgaWYgewogICAgaGFzX3pvbmVfYWNjZXNzCiAgICBhY3Rpb25faXNfbG9nX3ZpZXcKfQoKYWN0aW9uX2lzX2xvZ192aWV3IGlmIHsKICAgICJ2aWV3IiBpbiBpbnB1dC5hY3Rpb25zCn0KCmhhc196b25lX2FjY2VzcyBjb250YWlucyBhY2Nlc3NfZGF0YSBpZiB7CiAgICBzb21lIHpvbmVfZGF0YSBpbiBkYXRhLnpvbmUuem9uZS56b25lX2FjY2Vzc19sb2dzCiAgICB6b25lX2RhdGEudGltZXN0YW1wID49IGlucHV0LnRpbWVfcGVyaW9kLmZyb20KICAgIHpvbmVfZGF0YS50aW1lc3RhbXAgPCBpbnB1dC50aW1lX3BlcmlvZC50bwogICAgem9uZV9kYXRhLnpvbmVfaWQgPT0gaW5wdXQuem9uZV9pZAogICAgYWNjZXNzX2RhdGEgOj0ge2RhdGF0eXBlOiB6b25lX2RhdGFbZGF0YXR5cGVdIHwgZGF0YXR5cGUgaW4gaW5wdXQuZGF0YXR5cGVzfQp9Cg=="}},"name": "zone","version": "1.0.0","metadata": {"policy-id": "zone","policy-version": "1.0.0"}}], - "policiesToBeUndeployed":[], - "messageName":"PDP_UPDATE", - "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", - "timestampMs":1730722305297, - "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", - "pdpGroup":"opaGroup", - "pdpSubgroup":"opa" - }` - mockSender := new(mocks.PdpStatusSender) - mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - - // Mock the policy deployment logic - handlePolicyDeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { - - return []string{"Error in Deployment with Rego Err"}, map[string]string{} - } - - err := pdpUpdateMessageHandler([]byte(messageString), mockSender) - assert.NoError(t, err) -} - func TestSendPDPStatusResponse(t *testing.T) { mockSender := new(MockPdpStatusSender) // Test case: Success with policies to be deployed @@ -636,3 +562,514 @@ func TestSendPDPStatusResponse_SimulateFailures(t *testing.T) { func TestCreateBundleFunc(t *testing.T) { } + +func TesthandlePdpUpdateDeploymentVarSuccess(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + handlePolicyDeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + return nil, map[string]string{"policy1": "deployed"} + } + policymap.FormatMapOfAnyTypeVar = func(input any) (string, error) { + return `{"policy1":"deployed"}`, nil + } + + update := model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{{Name: "policy1"}}, + } + + jsonStr, err, failureMsgs := handlePdpUpdateDeployment(update, mockSender) + + assert.NoError(t, err) + assert.Equal(t, `{"policy1":"deployed"}`, jsonStr) + assert.Empty(t, failureMsgs) +} + +func TesthandlePdpUpdateDeploymentVarFormatError(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + handlePolicyDeploymentVar = func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + return nil, map[string]string{"policy1": "deployed"} + } + policymap.FormatMapOfAnyTypeVar = func(input any) (string, error) { + return "", errors.New("formatting error") + } + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + return nil + } + + update := model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{{Name: "policy1"}}, + } + + jsonStr, err, failureMsgs := handlePdpUpdateDeployment(update, mockSender) + + assert.NoError(t, err) + assert.Equal(t, "", jsonStr) + assert.Contains(t, failureMsgs[0], "Internal Map Error") +} + +func TesthandlePdpUpdateUndeploymentVarSuccess(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + handlePolicyUndeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + return nil, map[string]string{"policy1": "undeployed"} + } + policymap.FormatMapOfAnyTypeVar = func(input any) (string, error) { + return `{"policy1":"undeployed"}`, nil + } + + update := model.PdpUpdate{ + PoliciesToBeUndeployed: []model.ToscaConceptIdentifier{{Name: "policy1"}}, + } + + jsonStr, err, failureMsgs := handlePdpUpdateUndeployment(update, mockSender) + + assert.NoError(t, err) + assert.Equal(t, `{"policy1":"undeployed"}`, jsonStr) + assert.Empty(t, failureMsgs) +} + +func TesthandlePdpUpdateUndeploymentVarFormatError(t *testing.T) { + mockSender := new(MockPdpStatusSender) + + handlePolicyUndeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + return nil, map[string]string{"policy1": "undeployed"} + } + policymap.FormatMapOfAnyTypeVar = func(input any) (string, error) { + return "", errors.New("formatting error") + } + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + return nil + } + + update := model.PdpUpdate{ + PoliciesToBeUndeployed: []model.ToscaConceptIdentifier{{Name: "policy1"}}, + } + + jsonStr, err, failureMsgs := handlePdpUpdateUndeployment(update, mockSender) + + assert.NoError(t, err) + assert.Equal(t, "", jsonStr) + assert.Contains(t, failureMsgs[0], "Internal Map Error") +} + +func Test_UnmarshalFails(t *testing.T) { + msg := []byte(`invalid json`) + mockPublisher := new(mocks.PdpStatusSender) + + var called bool + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + called = true + return nil + } + defer func() { sendFailureResponseVar = sendFailureResponse }() + + err := pdpUpdateMessageHandler(msg, mockPublisher) + assert.Error(t, err) + assert.True(t, called) +} + +func Test_ValidationFails(t *testing.T) { + msg := []byte(`{"pdpSubgroup":"test","pdpHeartbeatIntervalMs":3000}`) + mockPublisher := new(mocks.PdpStatusSender) + + utils.ValidateFieldsStructsVar = func(pdpUpdate model.PdpUpdate) error { + return errors.New("validation error") + } + defer func() { utils.ValidateFieldsStructsVar = utils.ValidateFieldsStructs }() + + var called bool + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + called = true + return nil + } + defer func() { sendFailureResponseVar = sendFailureResponse }() + + err := pdpUpdateMessageHandler(msg, mockPublisher) + assert.Error(t, err) + assert.True(t, called) +} + +func Test_DeploymentHandlerFails(t *testing.T) { + msg := []byte(`{ + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed": [{"type": "onap.policies.native.opa","type_version": "1.0.0","properties": {"data": {"zone": "ewogICJ6b25lIjogewogICAgInpvbmVfYWNjZXNzX2xvZ3MiOiBbCiAgICAgIHsgImxvZ19pZCI6ICJsb2cxIiwgInRpbWVzdGFtcCI6ICIyMDI0LTExLTAxVDA5OjAwOjAwWiIsICJ6b25lX2lkIjogInpvbmVBIiwgImFjY2VzcyI6ICJncmFudGVkIiwgInVzZXIiOiAidXNlcjEiIH0sCiAgICAgIHsgImxvZ19pZCI6ICJsb2cyIiwgInRpbWVzdGFtcCI6ICIyMDI0LTExLTAxVDEwOjMwOjAwWiIsICJ6b25lX2lkIjogInpvbmVBIiwgImFjY2VzcyI6ICJkZW5pZWQiLCAidXNlciI6ICJ1c2VyMiIgfSwKICAgICAgeyAibG9nX2lkIjogImxvZzMiLCAidGltZXN0YW1wIjogIjIwMjQtMTEtMDFUMTE6MDA6MDBaIiwgInpvbmVfaWQiOiAiem9uZUIiLCAiYWNjZXNzIjogImdyYW50ZWQiLCAidXNlciI6ICJ1c2VyMyIgfQogICAgXQogIH0KfQo="},"policy": {"zone": "cGFja2FnZSB6b25lCgppbXBvcnQgcmVnby52MQoKZGVmYXVsdCBhbGxvdyA6PSBmYWxzZQoKYWxsb3cgaWYgewogICAgaGFzX3pvbmVfYWNjZXNzCiAgICBhY3Rpb25faXNfbG9nX3ZpZXcKfQoKYWN0aW9uX2lzX2xvZ192aWV3IGlmIHsKICAgICJ2aWV3IiBpbiBpbnB1dC5hY3Rpb25zCn0KCmhhc196b25lX2FjY2VzcyBjb250YWlucyBhY2Nlc3NfZGF0YSBpZiB7CiAgICBzb21lIHpvbmVfZGF0YSBpbiBkYXRhLnpvbmUuem9uZS56b25lX2FjY2Vzc19sb2dzCiAgICB6b25lX2RhdGEudGltZXN0YW1wID49IGlucHV0LnRpbWVfcGVyaW9kLmZyb20KICAgIHpvbmVfZGF0YS50aW1lc3RhbXAgPCBpbnB1dC50aW1lX3BlcmlvZC50bwogICAgem9uZV9kYXRhLnpvbmVfaWQgPT0gaW5wdXQuem9uZV9pZAogICAgYWNjZXNzX2RhdGEgOj0ge2RhdGF0eXBlOiB6b25lX2RhdGFbZGF0YXR5cGVdIHwgZGF0YXR5cGUgaW4gaW5wdXQuZGF0YXR5cGVzfQp9Cg=="}},"name": "zone","version": "1.0.0","metadata": {"policy-id": "zone","policy-version": "1.0.0"}}], + "policiesToBeUndeployed":[], + "messageName":"PDP_UPDATE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }`) + + policymap.LastDeployedPolicies = `{"deployed_policies_dict": [{"data": ["zone"],"policy": ["zone"],"policy-id": "zone","policy-version": "1.0.0"}]}` + mockPublisher := new(mocks.PdpStatusSender) + mockPublisher.On("SendPdpStatus", mock.Anything).Return(nil) + sendFinalResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, policies string, failures []string) error { + assert.Equal(t, "{}", policies) + return nil + } + defer func() { + sendFinalResponseVar = sendFinalResponse + }() + err := pdpUpdateMessageHandler(msg, mockPublisher) + assert.NoError(t, err) +} + +func Test_UndeploymentHandlerFails(t *testing.T) { + msg := []byte(`{ + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[{"name":"zone","version":"1.0.0"}], + "messageName":"PDP_UPDATE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }`) + policymap.LastDeployedPolicies = `{"deployed_policies_dict": []}` + mockPublisher := new(mocks.PdpStatusSender) + handlePdpUpdateUndeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { + return "", errors.New("undeployment error"), []string{} + } + defer func() { + handlePdpUpdateDeploymentVar = handlePdpUpdateDeployment + handlePdpUpdateUndeploymentVar = handlePdpUpdateUndeployment + }() + mockPublisher.On("SendPdpStatus", mock.Anything).Return(errors.New("undeployment error")) + err := pdpUpdateMessageHandler(msg, mockPublisher) + assert.Error(t, err) + assert.Contains(t, err.Error(), "undeployment error") +} + +func Test_SuccessFlow(t *testing.T) { + msg := []byte(`{ + "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0", + "pdpHeartbeatIntervalMs":120000, + "policiesToBeDeployed":[], + "policiesToBeUndeployed":[], + "messageName":"PDP_UPDATE", + "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1", + "timestampMs":1730722305297, + "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059", + "pdpGroup":"opaGroup", + "pdpSubgroup":"opa" + }`) + + mockPublisher := new(mocks.PdpStatusSender) + + handlePdpUpdateDeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { + return "dep1", nil, []string{} + } + handlePdpUpdateUndeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { + return "undep1", nil, []string{} + } + sendFinalResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, policies string, failures []string) error { + assert.Equal(t, "dep1,undep1", policies) + return nil + } + + defer func() { + handlePdpUpdateDeploymentVar = handlePdpUpdateDeployment + handlePdpUpdateUndeploymentVar = handlePdpUpdateUndeployment + sendFinalResponseVar = sendFinalResponse + }() + mockPublisher.On("SendPdpStatus", mock.Anything).Return(nil) + err := pdpUpdateMessageHandler(msg, mockPublisher) + assert.NoError(t, err) +} + +func TestSendPDPStatusFailureResponse_Mocked(t *testing.T) { + original := sendFailureResponseVar + defer func() { sendFailureResponseVar = original }() // restore after test + + called := false + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, msg error) error { + called = true + assert.Equal(t, "PDP Update Failed: [mock fail]", msg.Error()) + return nil + } + + err := sendPDPStatusFailureResponse(model.PdpUpdate{}, nil, "mockPolicy", []string{"mock fail"}) + assert.NoError(t, err) + assert.True(t, called, "sendFailureResponseVar should be called") +} + +func TestSendPDPStatusSuccessResponse_Mocked(t *testing.T) { + original := sendSuccessResponseVar + defer func() { sendSuccessResponseVar = original }() // restore after test + + called := false + sendSuccessResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, msg string) error { + called = true + assert.Equal(t, "PDP Update Successful for all policies: p1,p2", msg) + return nil + } + + update := model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{ + { + Type: "onap.policies.native.opa", + TypeVersion: "1.0.0", + }, + }, + + PoliciesToBeUndeployed: []model.ToscaConceptIdentifier{ + { + Name: "policy-to-undeply", + }, + }, + } + + err := sendPDPStatusSuccessResponse(update, nil, "p1,p2") + assert.NoError(t, err) + assert.True(t, called, "sendSuccessResponseVar should be called") +} + +func TestSendFinalResponse_MockedSendPDPStatusResponse_Success(t *testing.T) { + defer func() { sendPDPStatusResponseFunc = sendPDPStatusResponse }() // Reset after test + + mockSender := new(mocks.PdpStatusSender) + pdpUpdate := &model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{{Name: "TestPolicy"}}, + } + + // Override the global function for testing + sendPDPStatusResponseFunc = func(update model.PdpUpdate, p publisher.PdpStatusSender, loggingPoliciesList string, failureMessages []string) error { + return nil + } + + err := sendFinalResponse(mockSender, pdpUpdate, "TestPolicy", nil) + assert.NoError(t, err) +} + +func TestSendFinalResponse_MockedSendPDPStatusResponse_Failure(t *testing.T) { + defer func() { sendPDPStatusResponseFunc = sendPDPStatusResponse }() // Reset after test + + mockSender := new(mocks.PdpStatusSender) + pdpUpdate := &model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{{Name: "FailPolicy"}}, + } + + sendPDPStatusResponseFunc = func(update model.PdpUpdate, p publisher.PdpStatusSender, loggingPoliciesList string, failureMessages []string) error { + return errors.New("mock failure") + } + + err := sendFinalResponse(mockSender, pdpUpdate, "FailPolicy", nil) + assert.Error(t, err) + assert.Equal(t, "mock failure", err.Error()) +} + +func TestHandlePdpUpdateUndeployment_WithFailureMessage(t *testing.T) { + defer func() { + handlePolicyUndeploymentVar = handlePolicyUndeployment + policymap.FormatMapOfAnyTypeVar = policymap.FormatMapofAnyType + }() + + mockSender := new(mocks.PdpStatusSender) + update := model.PdpUpdate{ + PoliciesToBeUndeployed: []model.ToscaConceptIdentifier{{Name: "Policy1"}}, + } + + handlePolicyUndeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + + return []string{"UndeployError"}, map[string]string{"Policy1": "undeployed"} + } + + policymap.FormatMapOfAnyTypeVar = func(data interface{}) (string, error) { + return "Policy1", nil + } + + json, err, failures := handlePdpUpdateUndeployment(update, mockSender) + + assert.NoError(t, err) + assert.Equal(t, "Policy1", json) + assert.Contains(t, failures[0], "UnDeployment Errors:UndeployError") +} + +func TestHandlePdpUpdateUndeployment_SendFailureResponseFails(t *testing.T) { + defer func() { + handlePolicyUndeploymentVar = handlePolicyUndeployment + policymap.FormatMapOfAnyTypeVar = policymap.FormatMapofAnyType + sendFailureResponseVar = sendFailureResponse + }() + + mockSender := new(mocks.PdpStatusSender) + update := model.PdpUpdate{ + PoliciesToBeUndeployed: []model.ToscaConceptIdentifier{{Name: "Policy1"}}, + } + + handlePolicyUndeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) { + return nil, map[string]string{"Policy1": "undeployed"} + } + + policymap.FormatMapOfAnyTypeVar = func(data interface{}) (string, error) { + return "", errors.New("mock format error") + } + + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + return errors.New("send failed") + } + + json, err, failures := handlePdpUpdateUndeployment(update, mockSender) + + assert.Error(t, err) + assert.Equal(t, "send failed", err.Error()) + assert.Empty(t, json) + assert.NotEmpty(t, failures) +} + +func TestSendSuccessResponse_Success(t *testing.T) { + original := publisher.SendPdpUpdateResponseVar + defer func() { publisher.SendPdpUpdateResponseVar = original }() + + mockSender := new(mocks.PdpStatusSender) + update := &model.PdpUpdate{RequestId: "123"} + + publisher.SendPdpUpdateResponseVar = func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, message string) error { + assert.Equal(t, "123", pdpUpdate.RequestId) + assert.Equal(t, "Success", message) + return nil + } + + err := sendSuccessResponse(mockSender, update, "Success") + assert.NoError(t, err) +} + +func TestSendSuccessResponse_Error(t *testing.T) { + original := publisher.SendPdpUpdateResponseVar + defer func() { publisher.SendPdpUpdateResponseVar = original }() + + mockSender := new(mocks.PdpStatusSender) + update := &model.PdpUpdate{RequestId: "123"} + + publisher.SendPdpUpdateResponseVar = func(p publisher.PdpStatusSender, pdpUpdate *model.PdpUpdate, message string) error { + return errors.New("mock error") + } + + err := sendSuccessResponse(mockSender, update, "test") + assert.Error(t, err) + assert.Equal(t, "mock error", err.Error()) +} + +func TestPdpUpdateMessageHandler_UnmarshalError_SendFailureFails(t *testing.T) { + defer func() { sendFailureResponseVar = sendFailureResponse }() + + mockSender := new(mocks.PdpStatusSender) + invalidMessage := []byte(`{invalid json}`) + + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + return errors.New("send failure after unmarshal error") + } + + err := pdpUpdateMessageHandler(invalidMessage, mockSender) + assert.Error(t, err) + assert.EqualError(t, err, "send failure after unmarshal error") +} + +func TestPdpUpdateMessageHandler_ValidationError_SendFailureFails(t *testing.T) { + defer func() { + sendFailureResponseVar = sendFailureResponse + utils.ValidateFieldsStructsVar = utils.ValidateFieldsStructs + }() + + mockSender := new(mocks.PdpStatusSender) + message := []byte(`{"PdpSubgroup":"test", "PdpHeartbeatIntervalMs":30000}`) + + utils.ValidateFieldsStructsVar = func(update model.PdpUpdate) error { + return errors.New("mock validation error") + } + + sendFailureResponseVar = func(p publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + return errors.New("send failure after validation") + } + + err := pdpUpdateMessageHandler(message, mockSender) + assert.Error(t, err) + assert.EqualError(t, err, "send failure after validation") +} + +func TestPdpUpdateMessageHandler_DeploymentFails(t *testing.T) { + defer func() { + handlePdpUpdateDeploymentVar = handlePdpUpdateDeployment + handlePdpUpdateUndeploymentVar = handlePdpUpdateUndeployment + utils.ValidateFieldsStructsVar = utils.ValidateFieldsStructs + sendFinalResponseVar = sendFinalResponse + }() + + mockSender := new(mocks.PdpStatusSender) + message := []byte(`{"PdpSubgroup":"test", "PdpHeartbeatIntervalMs":30000}`) + + utils.ValidateFieldsStructsVar = func(update model.PdpUpdate) error { + return nil + } + + handlePdpUpdateDeploymentVar = func(update model.PdpUpdate, p publisher.PdpStatusSender) (string, error, []string) { + return "", errors.New("deployment error"), []string{"failure"} + } + + err := pdpUpdateMessageHandler(message, mockSender) + assert.Error(t, err) + assert.EqualError(t, err, "deployment error") +} + +func TestHandlePdpUpdateDeployment_WithDeploymentFailures(t *testing.T) { + defer func() { + handlePolicyDeploymentVar = handlePolicyDeployment + policymap.FormatMapOfAnyTypeVar = policymap.FormatMapofAnyType + }() + + mockSender := new(mocks.PdpStatusSender) + + handlePolicyDeploymentVar = func(update model.PdpUpdate, sender publisher.PdpStatusSender) ([]string, map[string]string) { + return []string{"failedPolicy1"}, map[string]string{"successPolicy": "test"} + } + + policymap.FormatMapOfAnyTypeVar = func(data interface{}) (string, error) { + return `{"successPolicy":"test"}`, nil + } + + update := model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{{Name: "test-policy"}}, + } + + mapJson, err, failures := handlePdpUpdateDeployment(update, mockSender) + assert.NoError(t, err) + assert.Contains(t, failures[0], "Deployment Errors") + assert.Contains(t, mapJson, `"successPolicy":"test"`) +} + +func TestHandlePdpUpdateDeployment_FormatMapFails(t *testing.T) { + defer func() { + handlePolicyDeploymentVar = handlePolicyDeployment + policymap.FormatMapOfAnyTypeVar = policymap.FormatMapofAnyType + sendFailureResponseVar = sendFailureResponse + }() + + mockSender := new(mocks.PdpStatusSender) + + handlePolicyDeploymentVar = func(update model.PdpUpdate, sender publisher.PdpStatusSender) ([]string, map[string]string) { + return nil, map[string]string{"some": "policy"} + } + + policymap.FormatMapOfAnyTypeVar = func(data interface{}) (string, error) { + return "", errors.New("mock formatting error") + } + + sendFailureResponseVar = func(sender publisher.PdpStatusSender, update *model.PdpUpdate, err error) error { + return nil + } + + update := model.PdpUpdate{ + PoliciesToBeDeployed: []model.ToscaPolicy{{Name: "test-policy"}}, + } + + mapJson, err, failures := handlePdpUpdateDeployment(update, mockSender) + assert.NoError(t, err) + assert.Equal(t, "", mapJson) + assert.Contains(t, failures[0], "Internal Map Error") +} diff --git a/pkg/kafkacomm/handler/pdp_update_undeploy_policy.go b/pkg/kafkacomm/handler/pdp_update_undeploy_policy.go index 4e72619..33cbf5b 100644 --- a/pkg/kafkacomm/handler/pdp_update_undeploy_policy.go +++ b/pkg/kafkacomm/handler/pdp_update_undeploy_policy.go @@ -38,8 +38,10 @@ import ( ) type ( - HandlePolicyUndeploymentFunc func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) - opasdkGetDataFunc func(ctx context.Context, dataPath string) (data *oapicodegen.OPADataResponse_Data, err error) + HandlePolicyUndeploymentFunc func(pdpUpdate model.PdpUpdate, p publisher.PdpStatusSender) ([]string, map[string]string) + opasdkGetDataFunc func(ctx context.Context, dataPath string) (data *oapicodegen.OPADataResponse_Data, err error) + policyUndeploymentActionFunc func(policy map[string]interface{}) []string + removeUndeployedPoliciesfromMapFunc func(undeployedPolicy map[string]interface{}) (string, error) ) var ( @@ -57,19 +59,25 @@ var ( removePolicyDirectoryFunc = removePolicyDirectory - policyUndeploymentActionFunc = policyUndeploymentAction + policyUndeploymentActionVar policyUndeploymentActionFunc = policyUndeploymentAction + + removeUndeployedPoliciesfromMapVar removeUndeployedPoliciesfromMapFunc = policymap.RemoveUndeployedPoliciesfromMap removePolicyFromSdkandDirFunc = removePolicyFromSdkandDir removeDataFromSdkandDirFunc = removeDataFromSdkandDir + + analyseEmptyParentNodesFunc = analyseEmptyParentNodes + + processDataDeletionFromSdkAndDirFunc = processDataDeletionFromSdkAndDir ) // processPoliciesTobeUndeployed handles the undeployment of policies func processPoliciesTobeUndeployed(undeployedPolicies map[string]string) ([]string, map[string]string) { var failureMessages []string + hasFailure := false successfullyUndeployedPolicies := make(map[string]string) - // Unmarshal the last known policies deployedPolicies, err := policymap.UnmarshalLastDeployedPolicies(policymap.LastDeployedPolicies) if err != nil { @@ -81,20 +89,23 @@ func processPoliciesTobeUndeployed(undeployedPolicies map[string]string) ([]stri matchedPolicy := findDeployedPolicy(policyID, policyVersion, deployedPolicies) if matchedPolicy != nil { // Handle undeployment for the policy - errs := policyUndeploymentActionFunc(matchedPolicy) + errs := policyUndeploymentActionVar(matchedPolicy) if len(errs) > 0 { + hasFailure = true metrics.IncrementUndeployFailureCount() metrics.IncrementTotalErrorCount() failureMessages = append(failureMessages, errs...) } - deployedPoliciesMap, err := policymap.RemoveUndeployedPoliciesfromMap(matchedPolicy) + deployedPoliciesMap, err := removeUndeployedPoliciesfromMapVar(matchedPolicy) if err != nil { log.Warnf("Policy Name: %s, Version: %s is not removed from LastDeployedPolicies", policyID, policyVersion) failureMessages = append(failureMessages, "Error in removing from LastDeployedPolicies") } log.Debugf("Policies Map After Undeployment : %s", deployedPoliciesMap) - metrics.IncrementUndeploySuccessCount() - successfullyUndeployedPolicies[policyID] = policyVersion + if !hasFailure { + metrics.IncrementUndeploySuccessCount() + successfullyUndeployedPolicies[policyID] = policyVersion + } } else { // Log failure if no match is found log.Debugf("Policy Name: %s, Version: %s is marked for undeployment but was not deployed", policyID, policyVersion) @@ -155,7 +166,7 @@ func removeDataFromSdkandDir(policy map[string]interface{}) []string { if strKey, ok := dataKey.(string); ok { dataKeysSlice = append(dataKeysSlice, strKey) } else { - failureMessages = append(failureMessages, fmt.Sprintf("Invalid Key :%s", dataKey)) + failureMessages = append(failureMessages, fmt.Sprintf("Invalid Key :%v", dataKey)) } } sort.Sort(utils.ByDotCount{Keys: dataKeysSlice, Ascend: false}) @@ -163,28 +174,37 @@ func removeDataFromSdkandDir(policy map[string]interface{}) []string { for _, keyPath := range dataKeysSlice { keyPath = "/" + strings.Replace(keyPath, ".", "/", -1) log.Debugf("Deleting data from OPA : %s", keyPath) - // Prepare to handle any errors - var err error - var dataPath string - // Fetch data first - // Call the function to check and Analyse empty parent nodes - if dataPath, err = analyseEmptyParentNodes(keyPath); err != nil { - failureMessages = append(failureMessages, err.Error()) - } - if err := deleteDataSdkFunc(context.Background(), dataPath); err != nil { - log.Errorf("Error while deleting Data from SDK for path : %s , %v", keyPath, err.Error()) - failureMessages = append(failureMessages, err.Error()) - continue - } - if err := removeDataDirectoryFunc(keyPath); err != nil { - failureMessages = append(failureMessages, err.Error()) - } + errs := processDataDeletionFromSdkAndDirFunc(keyPath) + failureMessages = append(failureMessages, errs...) } } else { - failureMessages = append(failureMessages, fmt.Sprintf("%s:%s Invalid JSON structure: 'data' is missing or not an array", policy["policy-id"], policy["policy-version"])) + policyID, _ := policy[consts.PolicyId].(string) + policyVersion, _ := policy[consts.PolicyVersion].(string) + failureMessages = append(failureMessages, fmt.Sprintf("%v:%v Invalid JSON structure: 'data' is missing or not an array", policyID, policyVersion)) + } + + return failureMessages +} + +func processDataDeletionFromSdkAndDir(keyPath string) []string { + var failureMessages []string + var dataPath string + var err error + // Fetch data first + // Call the function to check and Analyse empty parent nodes + if dataPath, err = analyseEmptyParentNodesFunc(keyPath); err != nil { + failureMessages = append(failureMessages, err.Error()) + } + if err := deleteDataSdkFunc(context.Background(), dataPath); err != nil { + log.Errorf("Error while deleting Data from SDK for path : %s , %v", keyPath, err.Error()) + failureMessages = append(failureMessages, err.Error()) + } + if err := removeDataDirectoryFunc(keyPath); err != nil { + failureMessages = append(failureMessages, err.Error()) } return failureMessages + } // removePolicyFromSdkandDir handles the "policy" directories in the policy @@ -204,7 +224,7 @@ func removePolicyFromSdkandDir(policy map[string]interface{}) []string { } } } else { - failureMessages = append(failureMessages, fmt.Sprintf("%s:%s Invalid JSON structure: 'policy' is missing or not an array", policy["policy-id"], policy["policy-version"])) + failureMessages = append(failureMessages, fmt.Sprintf("%s:%s Invalid JSON structure: 'policy' is missing or not an array", policy[consts.PolicyId], policy[consts.PolicyVersion])) } return failureMessages @@ -234,8 +254,8 @@ func removePolicyDirectory(policyKey string) error { func findDeployedPolicy(policyID, policyVersion string, deployedPolicies []map[string]interface{}) map[string]interface{} { for _, policy := range deployedPolicies { // Extract policy-id and policy-version from the deployed policy - id, idOk := policy["policy-id"].(string) - version, versionOk := policy["policy-version"].(string) + id, idOk := policy[consts.PolicyId].(string) + version, versionOk := policy[consts.PolicyVersion].(string) // Check if the deployed policy matches the undeployed policy if idOk && versionOk && id == policyID && version == policyVersion { diff --git a/pkg/kafkacomm/handler/pdp_update_undeploy_policy_test.go b/pkg/kafkacomm/handler/pdp_update_undeploy_policy_test.go index 08aed34..9936c6c 100644 --- a/pkg/kafkacomm/handler/pdp_update_undeploy_policy_test.go +++ b/pkg/kafkacomm/handler/pdp_update_undeploy_policy_test.go @@ -17,6 +17,7 @@ // ========================LICENSE_END=================================== // will process the update message from pap and send the pdp status response. + package handler import ( @@ -29,6 +30,7 @@ import ( "policy-opa-pdp/pkg/model" "policy-opa-pdp/pkg/model/oapicodegen" "policy-opa-pdp/pkg/policymap" + "reflect" "testing" ) @@ -142,24 +144,6 @@ func TestProcessPoliciesTobeUndeployed_Success(t *testing.T) { assert.Equal(t, "", success["test-policy"]) } -// Failure case: Policy undeployment fails due to missing policy -func TestProcessPoliciesTobeUndeployed_Failure_NoMatch(t *testing.T) { - undeployedPolicies := map[string]string{"non-existent-policy": "v1"} - - // Mock deployed policies (empty list) - deployedPolicies := []map[string]interface{}{} - - mockPolicyMap := new(MockPolicyMap) - mockPolicyMap.On("UnmarshalLastDeployedPolicies", mock.Anything).Return(deployedPolicies, nil) - - policymap.LastDeployedPolicies = `{"test-policy": "v1"}` - - failures, success := processPoliciesTobeUndeployed(undeployedPolicies) - - assert.Empty(t, success, "Expected no policies to be successfully undeployed") - assert.Empty(t, failures, "Expected no failure messages") -} - func TestProcessPoliciesTobeUndeployed_Failure_UnmarshalError(t *testing.T) { undeployedPolicies := map[string]string{"test-policy": "v1"} @@ -174,104 +158,6 @@ func TestProcessPoliciesTobeUndeployed_Failure_UnmarshalError(t *testing.T) { assert.Empty(t, failures, "Expected failure messages due to unmarshal error") } -func TestProcessPoliciesTobeUndeployed_Failure_PolicyNotFound(t *testing.T) { - undeployedPolicies := map[string]string{"non-existent-policy": "v1"} - mockPolicyMap := new(MockPolicyMap) - mockPolicyMap.On("UnmarshalLastDeployedPolicies", mock.Anything).Return([]map[string]interface{}{}, nil) - - failures, success := processPoliciesTobeUndeployed(undeployedPolicies) - - assert.Empty(t, success, "Expected no successful undeployments since policy doesn't exist") - assert.Empty(t, failures, "Failures list should be empty since policy wasn't found") -} - -func TestProcessPoliciesTobeUndeployed_FailureInUndeployment(t *testing.T) { - // Backup original function - originalFunc := policyUndeploymentActionFunc - defer func() { policyUndeploymentActionFunc = originalFunc }() - - // Mock policy undeployment action to fail - policyUndeploymentActionFunc = func(policy map[string]interface{}) []string { - return []string{"Failed to undeploy"} - } - - mockPolicyMap := new(MockPolicyMap) - undeployedPolicies := map[string]string{ - "policy2": "v1", - } - - mockPolicy := map[string]interface{}{ - "policyID": "policy2", - "policyVersion": "v1", - } - - mockPolicyMap.On("UnmarshalLastDeployedPolicies", mock.Anything).Return([]map[string]interface{}{mockPolicy}, nil) - mockPolicyMap.On("RemoveUndeployedPoliciesfromMap", mockPolicy).Return("{}", nil) - - // Run function - failureMessages, successPolicies := processPoliciesTobeUndeployed(undeployedPolicies) - - // Assertions - assert.Empty(t, failureMessages) - assert.Empty(t, successPolicies) -} - -func TestProcessPoliciesTobeUndeployed_PolicyNotDeployed(t *testing.T) { - // Backup original function - originalFunc := policyUndeploymentActionFunc - defer func() { policyUndeploymentActionFunc = originalFunc }() - - // Mock policy undeployment action to succeed - policyUndeploymentActionFunc = func(policy map[string]interface{}) []string { - return nil - } - - mockPolicyMap := new(MockPolicyMap) - undeployedPolicies := map[string]string{ - "policy3": "v1", - } - - mockPolicyMap.On("UnmarshalLastDeployedPolicies", mock.Anything).Return([]map[string]interface{}{}, nil) - - // Run function - failureMessages, successPolicies := processPoliciesTobeUndeployed(undeployedPolicies) - - // Assertions - assert.Empty(t, failureMessages) - assert.Empty(t, successPolicies) -} - -func TestProcessPoliciesTobeUndeployed_ErrorInRemoveFromMap(t *testing.T) { - // Backup original function - originalFunc := policyUndeploymentActionFunc - defer func() { policyUndeploymentActionFunc = originalFunc }() - - // Mock policy undeployment action to succeed - policyUndeploymentActionFunc = func(policy map[string]interface{}) []string { - return nil - } - - mockPolicyMap := new(MockPolicyMap) - undeployedPolicies := map[string]string{ - "policy4": "v1", - } - - mockPolicy := map[string]interface{}{ - "policyID": "policy4", - "policyVersion": "v1", - } - - mockPolicyMap.On("UnmarshalLastDeployedPolicies", mock.Anything).Return([]map[string]interface{}{mockPolicy}, nil) - mockPolicyMap.On("RemoveUndeployedPoliciesfromMap", mockPolicy).Return("", errors.New("removal error")) - - // Run function - failureMessages, successPolicies := processPoliciesTobeUndeployed(undeployedPolicies) - - // Assertions - assert.Empty(t, failureMessages) - assert.Empty(t, successPolicies) -} - func TestRemoveDataDirectory(t *testing.T) { // Backup original values originalDataPath := consts.Data @@ -336,53 +222,6 @@ func TestRemovePolicyDirectory(t *testing.T) { assert.Equal(t, expectedError, err.Error()) } -// Test function for removeDataFromSdkandDir -func TestRemoveDataFromSdkandDir(t *testing.T) { - // Backup original functions - originalRemoveDataDirectory := removeDataDirectoryFunc - originalDeleteData := deleteDataSdkFunc - defer func() { - removeDataDirectoryFunc = originalRemoveDataDirectory // Restore after test - deleteDataSdkFunc = originalDeleteData // Restore after test - }() - - // Mock removeDataDirectoryFunc and deleteDataFunc to return errors for testing - opasdkGetData = func(ctx context.Context, dataPath string) (data *oapicodegen.OPADataResponse_Data, err error) { - // Mock JSON data - mockedData := `{"mocked": {"success": "value", "error": "value"}}` - // Create an instance of OPADataResponse_Data - var response oapicodegen.OPADataResponse_Data - // Unmarshal into the OPADataResponse_Data struct - err = json.Unmarshal([]byte(mockedData), &response) - if err != nil { - return nil, errors.New("Error unmarshalling") - } - return &response, nil // - } - removeDataDirectoryFunc = func(dataKey string) error { - if dataKey == "/mocked/error" { - return errors.New("mocked remove data directory error") - } - return nil - } - - deleteDataSdkFunc = func(ctx context.Context, keyPath string) error { - if keyPath == "/mocked/error" { - return errors.New("mocked delete data error") - } - return nil - } - - policy := map[string]interface{}{ - "data": []interface{}{"mocked.success", "mocked.error"}, - } - - failures := removeDataFromSdkandDir(policy) - - assert.Len(t, failures, 1) // We expect two errors - assert.Contains(t, failures[0], "mocked delete data error") -} - func TestRemovePolicyFromSdkandDir(t *testing.T) { // Backup original functions originalRemovePolicyDirectory := removePolicyDirectoryFunc @@ -481,10 +320,10 @@ func TestPolicyUndeploymentAction(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Set mock behavior removePolicyFromSdkandDirFunc = func(policy map[string]interface{}) []string { - return tt.mockPolicyErrors + return tt.mockPolicyErrors } removeDataFromSdkandDirFunc = func(policy map[string]interface{}) []string { - return tt.mockDataErrors + return tt.mockDataErrors } // Call the function under test @@ -495,3 +334,429 @@ func TestPolicyUndeploymentAction(t *testing.T) { }) } } + +func TestProcessPoliciesTobeUndeployed_FailureInUndeployment(t *testing.T) { + + // Create a mock policy map + mockPolicyMap := new(MockPolicyMap) + + // Define undeployed policies + undeployedPolicies := map[string]string{"policy2": "1.0.0"} + + policymap.LastDeployedPolicies = `{"deployed_policies_dict": [{"policy-id": "policy2","policy-version": "1.0.0"}]}` + + // Override policyUndeploymentActionFunc to return a failure + policyUndeploymentActionVar = func(policy map[string]interface{}) []string { + return []string{"Failed to undeploy"} + } + + // Run the function + failureMessages, successPolicies := processPoliciesTobeUndeployed(undeployedPolicies) + + // Assertions + assert.Equal(t, []string{"Failed to undeploy"}, failureMessages, "Expected failure message for undeployment failure") + assert.Empty(t, successPolicies, "No policies should be successfully undeployed") + + // Ensure all expectations on the mock were met + mockPolicyMap.AssertExpectations(t) +} + +func TestProcessPoliciesTobeUndeployed_ErrorInRemoveFromMap(t *testing.T) { + // Backup original function + policyUndeploymentActionVar = func(policy map[string]interface{}) []string { + return nil + } + + undeployedPolicies := map[string]string{ + "policy4": "v1", + } + + policymap.LastDeployedPolicies = `{"deployed_policies_dict": [{"policy-id": "policy4","policy-version": "v1"}]}` + removeUndeployedPoliciesfromMapVar = func(undeployedPolicies map[string]interface{}) (string, error) { + return "", errors.New("removal error") + } + + // Run function + failureMessages, successPolicies := processPoliciesTobeUndeployed(undeployedPolicies) + + // Assertions + assert.Equal(t, []string{"Error in removing from LastDeployedPolicies"}, failureMessages, "Expected failure message for undeployment failure") + assert.NotEmpty(t, successPolicies) +} + +// Test cases for countChildKeysFromJSON +func TestCountChildKeysFromJSON(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expected map[string]int + }{ + { + name: "Empty JSON", + input: map[string]interface{}{}, + expected: map[string]int{ + // No child nodes + }, + }, + { + name: "Single Level JSON", + input: map[string]interface{}{ + "key1": map[string]interface{}{ + "child1": "value1", + "child2": "value2", + }, + "key2": map[string]interface{}{ + "childA": "valueA", + }, + }, + expected: map[string]int{ + "node/key1": 2, // key1 has 2 children + "node/key2": 1, // key2 has 1 child + }, + }, + { + name: "Nested JSON", + input: map[string]interface{}{ + "root": map[string]interface{}{ + "level1": map[string]interface{}{ + "level2": map[string]interface{}{ + "child1": "value1", + "child2": "value2", + }, + }, + }, + }, + expected: map[string]int{ + "node/root": 1, // root has 1 child (level1) + "node/root/level1": 1, // level1 has 1 child (level2) + "node/root/level1/level2": 2, // level2 has 2 children + }, + }, + { + name: "Mixed Data Types", + input: map[string]interface{}{ + "parent": map[string]interface{}{ + "child1": "string", + "child2": 42, + "child3": map[string]interface{}{ + "subchild1": true, + "subchild2": nil, + }, + }, + }, + expected: map[string]int{ + "node/parent": 3, // parent has 3 children + "node/parent/child3": 2, // child3 has 2 children + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := countChildKeysFromJSON(tt.input) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("countChildKeysFromJSON() = %v, expected %v", got, tt.expected) + } + }) + } +} + +func TestAnalyzeHierarchy(t *testing.T) { + tests := []struct { + name string + parentDataJson json.RawMessage + dataPath string + expectedPath string + expectedErr bool + }{ + { + name: "Valid hierarchy with multiple children", + parentDataJson: json.RawMessage(`{ +"root": { +"parent1": { +"child1": {}, +"child2": {} +}, +"parent2": { +"child3": {} +} +} +}`), + dataPath: "/root/parent1/child1", + expectedPath: "/root/parent1/child1", + expectedErr: false, + }, + { + name: "Hierarchy with only one child, eligible parent", + parentDataJson: json.RawMessage(`{ +"root": { +"parent1": { +"child1": {} +} +} +}`), + dataPath: "/root/parent1/child1", + expectedPath: "/root/parent1/child1", + expectedErr: false, + }, + { + name: "Invalid JSON structure", + parentDataJson: json.RawMessage(`{ +"root": { "parent1": "child1" `), // Malformed JSON + dataPath: "/root/parent1/child1", + expectedPath: "", + expectedErr: true, + }, + { + name: "Path does not exist in JSON", + parentDataJson: json.RawMessage(`{ +"root": { +"parent1": { +"child1": {} +} +} +}`), + dataPath: "/root/parent2/child3", + expectedPath: "/root/parent2/child3", + expectedErr: false, + }, + { + name: "Root path case", + parentDataJson: json.RawMessage(`{ +"root": {} +}`), + dataPath: "/root", + expectedPath: "/root", + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := analyzeHierarchy(tt.parentDataJson, tt.dataPath) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedPath, result) + } + }) + } +} + +// Mock function variable for opasdkGetData +var mockGetData func(ctx context.Context, dataPath string) (*oapicodegen.OPADataResponse_Data, error) + +func TestAnalyseEmptyParentNodes(t *testing.T) { + // Backup the original function + originalOpaSDKGetData := opasdkGetData + opasdkGetData = func(ctx context.Context, dataPath string) (*oapicodegen.OPADataResponse_Data, error) { + return mockGetData(ctx, dataPath) + } + defer func() { + opasdkGetData = originalOpaSDKGetData // Restore after test + }() + + tests := []struct { + name string + inputPath string + mockResponse interface{} + mockError error + expectedOutput string + expectError bool + }{ + // Case 1: Leaf node (no parent hierarchy) + { + name: "Leaf Node - No Parent", + inputPath: "/singleSegment", + mockResponse: nil, + mockError: nil, + expectedOutput: "/singleSegment", + expectError: false, + }, + // Case 2: Parent exists with valid data + { + name: "Success - Valid Parent Data Exists", + inputPath: "/parent/child", + mockResponse: map[string]interface{}{ + "child": "data", + }, + mockError: nil, + expectedOutput: "/parent/child", + expectError: false, + }, + // Case 3: Parent exists but is empty + { + name: "Parent Exists but is Empty", + inputPath: "/parent/child", + mockResponse: map[string]interface{}{}, + mockError: nil, + expectedOutput: "/parent/child", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock function behavior + opasdkGetData = func(ctx context.Context, dataPath string) (*oapicodegen.OPADataResponse_Data, error) { + if tt.mockResponse != nil { + jsonData, _ := json.Marshal(tt.mockResponse) + var resData oapicodegen.OPADataResponse_Data + _ = json.Unmarshal(jsonData, &resData) + return &resData, tt.mockError + } + return nil, tt.mockError + } + + // Call function + output, err := analyseEmptyParentNodes(tt.inputPath) + + // Validate results + if tt.expectError { + assert.Error(t, err, "Expected error but got none") + } else { + assert.NoError(t, err, "Expected no error but got one") + assert.Equal(t, tt.expectedOutput, output, "Unexpected output") + } + }) + } +} + +func TestProcessDataDeletionFromSdkAndDir(t *testing.T) { + tests := []struct { + name string + inputKeyPath string + mockAnalyseErr error + mockDeleteErr error + mockRemoveErr error + expectedFailures []string + }{ + { + name: "Success - No errors", + inputKeyPath: "/valid/path", + mockAnalyseErr: nil, + mockDeleteErr: nil, + mockRemoveErr: nil, + expectedFailures: []string{}, + }, + { + name: "Failure - analyseEmptyParentNodes error", + inputKeyPath: "/error/path", + mockAnalyseErr: errors.New("failed to analyze"), + mockDeleteErr: nil, + mockRemoveErr: nil, + expectedFailures: []string{"failed to analyze"}, + }, + { + name: "Failure - deleteDataSdkFunc error", + inputKeyPath: "/delete/error", + mockAnalyseErr: nil, + mockDeleteErr: errors.New("delete failed"), + mockRemoveErr: nil, + expectedFailures: []string{"delete failed"}, + }, + { + name: "Failure - removeDataDirectoryFunc error", + inputKeyPath: "/remove/error", + mockAnalyseErr: nil, + mockDeleteErr: nil, + mockRemoveErr: errors.New("remove failed"), + expectedFailures: []string{"remove failed"}, + }, + { + name: "Failure - Multiple errors", + inputKeyPath: "/multiple/errors", + mockAnalyseErr: errors.New("analyse failed"), + mockDeleteErr: errors.New("delete failed"), + mockRemoveErr: errors.New("remove failed"), + expectedFailures: []string{"analyse failed", "delete failed", "remove failed"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock function variables + analyseEmptyParentNodesFunc = func(dataPath string) (string, error) { + return dataPath, tt.mockAnalyseErr + } + deleteDataSdkFunc = func(ctx context.Context, dataPath string) error { + return tt.mockDeleteErr + } + removeDataDirectoryFunc = func(keyPath string) error { + return tt.mockRemoveErr + } + + // Call function + failureMessages := processDataDeletionFromSdkAndDir(tt.inputKeyPath) + + // Normalize nil vs empty slice + if failureMessages == nil { + failureMessages = []string{} + } + + // Validate results + assert.Equal(t, tt.expectedFailures, failureMessages, "Unexpected failure messages") + }) + } +} + +// Mock function for testing +func mockProcessDataDeletion(keyPath string) []string { + if keyPath == "/invalid/path" { + return []string{"Failed to delete: " + keyPath} + } + return nil // Simulate successful deletion +} + +func TestRemoveDataFromSdkandDir(t *testing.T) { + // Mock the function globally + originalProcessDataDeletion := processDataDeletionFromSdkAndDirFunc + processDataDeletionFromSdkAndDirFunc = mockProcessDataDeletion + defer func() { processDataDeletionFromSdkAndDirFunc = originalProcessDataDeletion }() // Restore after test + + tests := []struct { + name string + policy map[string]interface{} + expectedFailures []string + }{ + { + name: "Valid data keys", + policy: map[string]interface{}{ + "data": []interface{}{"policy.rule1", "policy.rule2"}, + }, + expectedFailures: nil, + }, + { + name: "Invalid data key type", + policy: map[string]interface{}{ + "data": []interface{}{"policy.rule1", 123}, // Invalid integer key + }, + expectedFailures: []string{"Invalid Key :123"}, + }, + { + name: "Invalid JSON structure", + policy: map[string]interface{}{ + "policyId": "test-policy", + "policyVersion": "1.0", + }, + expectedFailures: []string{": Invalid JSON structure: 'data' is missing or not an array"}, + }, + + { + name: "Deletion failure", + policy: map[string]interface{}{ + "data": []interface{}{"invalid.path"}, + }, + expectedFailures: []string{"Failed to delete: /invalid/path"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := removeDataFromSdkandDir(tt.policy) + assert.ElementsMatch(t, tt.expectedFailures, result) + }) + } +} diff --git a/pkg/kafkacomm/pdp_topic_producer.go b/pkg/kafkacomm/pdp_topic_producer.go index 341cd01..8685a34 100644 --- a/pkg/kafkacomm/pdp_topic_producer.go +++ b/pkg/kafkacomm/pdp_topic_producer.go @@ -50,6 +50,8 @@ var ( // It configures the Kafka producer with the given bootstrap servers and topic. // If SASL authentication is enabled via the configuration, the necessary credentials // are set in the producer configuration. +// +//nolint:gosec func GetKafkaProducer(bootstrapServers, topic string) (*KafkaProducer, error) { var err error once.Do(func() { diff --git a/pkg/kafkacomm/pdp_topic_producer_test.go b/pkg/kafkacomm/pdp_topic_producer_test.go index e106772..c66ab38 100644 --- a/pkg/kafkacomm/pdp_topic_producer_test.go +++ b/pkg/kafkacomm/pdp_topic_producer_test.go @@ -106,6 +106,31 @@ func TestKafkaProducer_Produce_Error(t *testing.T) { mockProducer.AssertExpectations(t) } +func TestKafkaProducer_Produce_NilTopic(t *testing.T) { + // Arrange + mockProducer := new(mocks.KafkaProducerInterface) + topic := "test-topic" + kp := &KafkaProducer{ + producer: mockProducer, + topic: topic, + } + + msg := &kafka.Message{ + TopicPartition: kafka.TopicPartition{ + Topic: nil, + Partition: kafka.PartitionAny, + }, + } + + mockProducer.On("Produce", mock.Anything, mock.Anything).Return(nil) + + err := kp.Produce(msg, nil) + assert.NoError(t, err) + assert.NotNil(t, msg.TopicPartition.Topic) + assert.Equal(t, topic, *msg.TopicPartition.Topic) + mockProducer.AssertExpectations(t) +} + func TestKafkaProducer_Close(t *testing.T) { // Arrange mockProducer := new(mocks.KafkaProducerInterface) @@ -158,6 +183,12 @@ func (m *MockKafkaProducer) Close() { m.Called() } +func (m *MockKafkaProducer) Flush(timeout int) int { + + args := m.Called(timeout) + return args.Int(0) +} + func mockKafkaNewProducer(conf *kafka.ConfigMap) (*kafka.Producer, error) { // Return a mock *kafka.Producer (it doesn't have to be functional) mockProducer := new(MockKafkaProducer) @@ -204,3 +235,20 @@ func TestKafkaProducer_Close_NilProducer(t *testing.T) { logOutput := buf.String() assert.Contains(t, logOutput, "KafkaProducer or producer is nil, skipping Close.") } + +func TestKafkaProducer_Flush(t *testing.T) { + // Arrange + mockProducer := new(mocks.KafkaProducerInterface) + kp := &KafkaProducer{ + producer: mockProducer, + } + + mockProducer.On("Flush", 15*1000).Return(1) + + // Act + result := kp.Flush(15 * 1000) + + // Assert + assert.Equal(t, 1, result, "Flush should return expected value") + mockProducer.AssertExpectations(t) +} diff --git a/pkg/kafkacomm/publisher/pdp-heartbeat_test.go b/pkg/kafkacomm/publisher/pdp-heartbeat_test.go index ac0ad02..7a8fe55 100644 --- a/pkg/kafkacomm/publisher/pdp-heartbeat_test.go +++ b/pkg/kafkacomm/publisher/pdp-heartbeat_test.go @@ -16,9 +16,7 @@ // SPDX-License-Identifier: Apache-2.0 // ========================LICENSE_END=================================== // - package publisher - import ( "errors" "policy-opa-pdp/pkg/kafkacomm/publisher/mocks" @@ -27,7 +25,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) - /* Success Case 1 TestStartHeartbeatIntervalTimer_ValidInterval @@ -36,11 +33,9 @@ Input: intervalMs = 1000 Expected Output: The ticker starts with an interval of 1000 milliseconds, and heartbeat messages are sent at this interval. */ func TestStartHeartbeatIntervalTimer_ValidInterval(t *testing.T) { - intervalMs := int64(1000) mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - StartHeartbeatIntervalTimer(intervalMs, mockSender) mu.Lock() defer mu.Unlock() @@ -51,7 +46,6 @@ func TestStartHeartbeatIntervalTimer_ValidInterval(t *testing.T) { t.Errorf("Expected currentInterval to be %d, got %d", intervalMs, currentInterval) } } - /* Failure Case 1 TestStartHeartbeatIntervalTimer_InvalidInterval @@ -63,16 +57,13 @@ func TestStartHeartbeatIntervalTimer_InvalidInterval(t *testing.T) { intervalMs := int64(-1000) mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - StartHeartbeatIntervalTimer(intervalMs, mockSender) mu.Lock() defer mu.Unlock() - if ticker != nil { t.Log("Expected ticker to be nil for invalid interval") } } - /* TestSendPDPHeartBeat_Success 2 Description: Test sending a heartbeat successfully. @@ -80,13 +71,11 @@ Input: Valid pdpStatus object Expected Output: Heartbeat message is sent successfully, and a debug log "Message sent successfully" is generated. */ func TestSendPDPHeartBeat_Success(t *testing.T) { - mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) err := sendPDPHeartBeat(mockSender) assert.NoError(t, err) } - /* TestSendPDPHeartBeat_Failure 2 Description: Test failing to send a heartbeat. @@ -100,7 +89,6 @@ func TestSendPDPHeartBeat_Failure(t *testing.T) { err := sendPDPHeartBeat(mockSender) assert.Error(t, err) } - /* TestsendPDPHeartBeat_Success 3 Description: Test sending a heartbeat successfully with some deployed policies. @@ -110,17 +98,14 @@ Expected Output: Heartbeat message is sent successfully, and a debug log "Messag func TestSendPDPHeartBeat_SuccessSomeDeployedPolicies(t *testing.T) { // Setup mock Policymap mockPolicymap := new(MockPolicymap) - mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - policymap.LastDeployedPolicies = "some-policies" // Set mock behavior for policymap mockPolicymap.On("ExtractDeployedPolicies", mock.Anything).Return(nil) err := sendPDPHeartBeat(mockSender) assert.NoError(t, err) } - /* TestsendPDPHeartBeat_Success 4 Description: Test sending a heartbeat successfully with no deployed policies. @@ -130,17 +115,14 @@ Expected Output: Heartbeat message is sent successfully, and a debug log "Messag func TestSendPDPHeartBeat_SuccessNoDeployedPolicies(t *testing.T) { // Setup mock Policymap mockPolicymap := new(MockPolicymap) - mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - policymap.LastDeployedPolicies = "" // Set mock behavior for policymap mockPolicymap.On("ExtractDeployedPolicies", mock.Anything).Return(nil) err := sendPDPHeartBeat(mockSender) assert.NoError(t, err) } - /* TestStopTicker_Success 3 Description: Test stopping the ticker. @@ -159,7 +141,6 @@ func TestStopTicker_Success(t *testing.T) { t.Errorf("Expected ticker to be nil") } } - /* TestStopTicker_NotRunning 3 Description: Test stopping the ticker when it is not running. @@ -171,36 +152,25 @@ func TestStopTicker_NotRunning(t *testing.T) { mu.Lock() defer mu.Unlock() } - func TestStartHeartbeatIntervalTimer_TickerAlreadyRunning(t *testing.T) { intervalMs := int64(1000) - mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - // Start the ticker for the first time StartHeartbeatIntervalTimer(intervalMs, mockSender) - StartHeartbeatIntervalTimer(intervalMs, mockSender) - if currentInterval != intervalMs { t.Errorf("Expected ticker to not restart, currentInterval is %d, expected %d", currentInterval, intervalMs) } - assert.NotNil(t, ticker, "Expected ticker to be running but it is nil") } - func TestStartHeartbeatIntervalTimer_TickerAlreadyRunning_Case2(t *testing.T) { intervalMs := int64(1000) - mockSender := new(mocks.PdpStatusSender) mockSender.On("SendPdpStatus", mock.Anything).Return(nil) - // Start the ticker for the first time StartHeartbeatIntervalTimer(intervalMs, mockSender) - // Start it again StartHeartbeatIntervalTimer(int64(201), mockSender) - assert.NotNil(t, ticker, "Expected ticker to be running but it is nil") } diff --git a/pkg/kafkacomm/publisher/pdp-pap-registration.go b/pkg/kafkacomm/publisher/pdp-pap-registration.go index 471bfdb..b3463d1 100644 --- a/pkg/kafkacomm/publisher/pdp-pap-registration.go +++ b/pkg/kafkacomm/publisher/pdp-pap-registration.go @@ -25,11 +25,9 @@ import ( "github.com/confluentinc/confluent-kafka-go/v2/kafka" "github.com/google/uuid" "policy-opa-pdp/cfg" - "policy-opa-pdp/consts" "policy-opa-pdp/pkg/kafkacomm" "policy-opa-pdp/pkg/log" "policy-opa-pdp/pkg/model" - "policy-opa-pdp/pkg/pdpattributes" "time" ) @@ -73,29 +71,3 @@ func (s *RealPdpStatusSender) SendPdpStatus(pdpStatus model.PdpStatus) error { return nil } - -// sends the registartion message to topic using SendPdpStatus(pdpStatus) -func SendPdpPapRegistration(s PdpStatusSender) error { - - var pdpStatus = model.PdpStatus{ - MessageType: model.PDP_STATUS, - PdpType: consts.PdpType, - State: model.Passive, - Healthy: model.Healthy, - Policies: nil, - PdpResponse: nil, - Name: pdpattributes.PdpName, - Description: "Pdp Status Registration Message", - PdpGroup: consts.PdpGroup, - } - - log.Debugf("Sending PDP PAP Registration Message") - - err := s.SendPdpStatus(pdpStatus) - if err != nil { - log.Warnf("Error producing message: %v\n", err) - return err - } - return nil - -} diff --git a/pkg/kafkacomm/publisher/pdp-pap-registration_test.go b/pkg/kafkacomm/publisher/pdp-pap-registration_test.go index 9855b28..67e2f1b 100644 --- a/pkg/kafkacomm/publisher/pdp-pap-registration_test.go +++ b/pkg/kafkacomm/publisher/pdp-pap-registration_test.go @@ -24,9 +24,7 @@ import ( "fmt" "github.com/confluentinc/confluent-kafka-go/v2/kafka" "github.com/google/uuid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "policy-opa-pdp/pkg/kafkacomm/publisher/mocks" "policy-opa-pdp/pkg/model" "testing" "time" @@ -41,27 +39,6 @@ func (m *MockPdpStatusSender) SendPdpStatus(pdpStatus model.PdpStatus) error { } -func TestSendPdpPapRegistration_Success(t *testing.T) { - mockSender := new(mocks.PdpStatusSender) - - mockSender.On("SendPdpStatus", mock.AnythingOfType("model.PdpStatus")).Return(nil) - - err := SendPdpPapRegistration(mockSender) - assert.NoError(t, err) - mockSender.AssertCalled(t, "SendPdpStatus", mock.AnythingOfType("model.PdpStatus")) -} - -func TestSendPdpPapRegistration_Failure(t *testing.T) { - mockSender := new(mocks.PdpStatusSender) - - mockSender.On("SendPdpStatus", mock.AnythingOfType("model.PdpStatus")).Return(errors.New("failed To Send")) - - err := SendPdpPapRegistration(mockSender) - assert.Error(t, err, "Expected an error for failure") - assert.EqualError(t, err, "failed To Send", "Error messages should match") - mockSender.AssertCalled(t, "SendPdpStatus", mock.AnythingOfType("model.PdpStatus")) -} - // New type MockKafkaProducer struct { diff --git a/pkg/kafkacomm/publisher/pdp-status-publisher.go b/pkg/kafkacomm/publisher/pdp-status-publisher.go index 3e25780..a9aa6e0 100644 --- a/pkg/kafkacomm/publisher/pdp-status-publisher.go +++ b/pkg/kafkacomm/publisher/pdp-status-publisher.go @@ -35,6 +35,14 @@ import ( "github.com/google/uuid" ) +type( + SendPdpUpdateResponseFunc func(s PdpStatusSender, pdpUpdate *model.PdpUpdate, resMessage string) error +) + +var ( + SendPdpUpdateResponseVar SendPdpUpdateResponseFunc = SendPdpUpdateResponse +) + // Sends a PDP_STATUS message to indicate the successful processing of a PDP_UPDATE request // received from the Policy Administration Point (PAP). func SendPdpUpdateResponse(s PdpStatusSender, pdpUpdate *model.PdpUpdate, resMessage string) error { @@ -123,7 +131,7 @@ func SendPdpUpdateErrorResponse(s PdpStatusSender, pdpUpdate *model.PdpUpdate, e err = s.SendPdpStatus(pdpStatus) if err != nil { - log.Warnf("Failed to send PDP Update Message : %v", err) + log.Warnf("Failed to send PDP Update Error Message : %v", err) return err } @@ -160,7 +168,7 @@ func SendStateChangeResponse(s PdpStatusSender, pdpStateChange *model.PdpStateCh err := s.SendPdpStatus(pdpStatus) if err != nil { - log.Warnf("Failed to send PDP Update Message : %v", err) + log.Warnf("Failed to send PDP State Change Message : %v", err) return err } diff --git a/pkg/metrics/statistics-provider.go b/pkg/metrics/statistics-provider.go index 381ba5c..9776511 100644 --- a/pkg/metrics/statistics-provider.go +++ b/pkg/metrics/statistics-provider.go @@ -27,14 +27,14 @@ import ( "policy-opa-pdp/pkg/log" "policy-opa-pdp/pkg/model/oapicodegen" "policy-opa-pdp/pkg/utils" - + "policy-opa-pdp/consts" "github.com/google/uuid" openapi_types "github.com/oapi-codegen/runtime/types" ) func FetchCurrentStatistics(res http.ResponseWriter, req *http.Request) { - requestId := req.Header.Get("X-ONAP-RequestID") + requestId := req.Header.Get(consts.RequestId) var parsedUUID *uuid.UUID var statisticsParams *oapicodegen.StatisticsParams @@ -47,12 +47,12 @@ func FetchCurrentStatistics(res http.ResponseWriter, req *http.Request) { statisticsParams = &oapicodegen.StatisticsParams{ XONAPRequestID: (*openapi_types.UUID)(parsedUUID), } - res.Header().Set("X-ONAP-RequestID", statisticsParams.XONAPRequestID.String()) + res.Header().Set(consts.RequestId, statisticsParams.XONAPRequestID.String()) } } else { log.Warnf("Invalid or Missing Request ID") requestId = "000000000000" - res.Header().Set("X-ONAP-RequestID", requestId) + res.Header().Set(consts.RequestId, requestId) } var statReport oapicodegen.StatisticsReport diff --git a/pkg/metrics/statistics-provider_test.go b/pkg/metrics/statistics-provider_test.go index 400430e..6767e65 100644 --- a/pkg/metrics/statistics-provider_test.go +++ b/pkg/metrics/statistics-provider_test.go @@ -21,6 +21,7 @@ package metrics import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "policy-opa-pdp/pkg/model/oapicodegen" @@ -98,3 +99,25 @@ func TestFetchCurrentStatistics_ValidRequestID(t *testing.T) { assert.Equal(t, http.StatusOK, res.Code) } + +type FailingResponseWriter struct { + http.ResponseWriter +} + +func (w *FailingResponseWriter) Write(b []byte) (int, error) { + return 0, fmt.Errorf("forced encoding failure") +} + +func TestFetchCurrentStatistics_JSONEncodingFailure(t *testing.T) { + req := httptest.NewRequest("GET", "/statistics", nil) + w := httptest.NewRecorder() + + // Wrap the ResponseWriter to force an error + failingWriter := &FailingResponseWriter{w} + + FetchCurrentStatistics(failingWriter, req) + resp := w.Result() + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/pkg/opasdk/opasdk.go b/pkg/opasdk/opasdk.go index 8fd1708..8776e78 100644 --- a/pkg/opasdk/opasdk.go +++ b/pkg/opasdk/opasdk.go @@ -124,7 +124,7 @@ func GetOPASingletonInstance() (*sdk.OPA, error) { func UpsertPolicy(ctx context.Context, policyID string, policyContent []byte) error { txn, err := memStore.NewTransaction(ctx, storage.WriteParams) if err != nil { - log.Warnf("Error creating transaction: %s", err) + log.Warnf("Error creating transaction While Upsert Policy: %s", err) memStore.Abort(ctx, txn) return err } @@ -136,7 +136,7 @@ func UpsertPolicy(ctx context.Context, policyID string, policyContent []byte) er } err = memStore.Commit(ctx, txn) if err != nil { - log.Warnf("Error commiting the transaction: %s", err) + log.Warnf("Error commiting the transaction while upsert policy: %s", err) memStore.Abort(ctx, txn) return err } @@ -146,7 +146,7 @@ func UpsertPolicy(ctx context.Context, policyID string, policyContent []byte) er func DeletePolicy(ctx context.Context, policyID string) error { txn, err := memStore.NewTransaction(ctx, storage.WriteParams) if err != nil { - log.Warnf("Error creating transaction: %s", err) + log.Warnf("Error creating transaction while delete policy: %s", err) memStore.Abort(ctx, txn) return err } @@ -158,7 +158,7 @@ func DeletePolicy(ctx context.Context, policyID string) error { } err = memStore.Commit(ctx, txn) if err != nil { - log.Warnf("Error commiting the transaction: %s", err) + log.Warnf("Error commiting the transaction while delete policy: %s", err) memStore.Abort(ctx, txn) return err } @@ -168,7 +168,7 @@ func DeletePolicy(ctx context.Context, policyID string) error { func WriteData(ctx context.Context, dataPath string, data interface{}) error { txn, err := memStore.NewTransaction(ctx, storage.WriteParams) if err != nil { - log.Warnf("Error creating transaction: %s", err) + log.Warnf("Error creating transaction while write data: %s", err) memStore.Abort(ctx, txn) return err } @@ -190,7 +190,7 @@ func WriteData(ctx context.Context, dataPath string, data interface{}) error { } err = memStore.Commit(ctx, txn) if err != nil { - log.Warnf("Error commiting the transaction: %s", err) + log.Warnf("Error commiting the transaction while write data: %s", err) memStore.Abort(ctx, txn) return err } @@ -201,7 +201,7 @@ func WriteData(ctx context.Context, dataPath string, data interface{}) error { func DeleteData(ctx context.Context, dataPath string) error { txn, err := memStore.NewTransaction(ctx, storage.WriteParams) if err != nil { - log.Warnf("Error creating transaction: %s", err) + log.Warnf("Error creating transaction while Delete Data: %s", err) memStore.Abort(ctx, txn) return err } @@ -213,7 +213,7 @@ func DeleteData(ctx context.Context, dataPath string) error { } err = memStore.Commit(ctx, txn) if err != nil { - log.Warnf("Error commiting the transaction: %s", err) + log.Warnf("Error commiting the transaction while Delete Data: %s", err) memStore.Abort(ctx, txn) return err } @@ -225,7 +225,7 @@ func ListPolicies(res http.ResponseWriter, req *http.Request) { ctx := context.Background() rtxn, err := memStore.NewTransaction(ctx, storage.TransactionParams{Write: false}) if err != nil { - log.Warnf("Error creating transaction %s", err) + log.Warnf("Error creating transaction while listing policies: %s", err) memStore.Abort(ctx, rtxn) http.Error(res, err.Error(), http.StatusInternalServerError) return @@ -280,7 +280,7 @@ func initializePath(ctx context.Context, txn storage.Transaction, path string) e func PatchData(ctx context.Context, patches []PatchImpl) error { txn, err := memStore.NewTransaction(ctx, storage.WriteParams) if err != nil { - log.Warnf("Error in creating transaction: %s", err) + log.Warnf("Error in creating transaction while Patch Data: %s", err) memStore.Abort(ctx, txn) return err } diff --git a/pkg/opasdk/opasdk_test.go b/pkg/opasdk/opasdk_test.go index 48ed65e..313160f 100644 --- a/pkg/opasdk/opasdk_test.go +++ b/pkg/opasdk/opasdk_test.go @@ -534,7 +534,8 @@ func TestListPolicies_GetPolicyError(t *testing.T) { txn := new(storage.Transaction) mockMemStore.On("NewTransaction", ctx, mock.Anything).Return(txn, nil) - mockMemStore.On("ListPolicies", ctx, mock.AnythingOfType("*opasdk.MockTransaction")).Return([]string{}, errors.New("GetPolicy error")) + mockMemStore.On("ListPolicies", ctx, mock.AnythingOfType("*opasdk.MockTransaction")).Return([]string{"policyId"}, nil) + mockMemStore.On("GetPolicy", ctx, mock.AnythingOfType("*opasdk.MockTransaction"), "policyId").Return([]byte{}, errors.New("GetPolicy error")) mockMemStore.On("Abort", ctx, mock.AnythingOfType("*opasdk.MockTransaction")).Return(nil) req := httptest.NewRequest("GET", "/opa/listpolicies", nil) diff --git a/pkg/policymap/policy_and_data_map.go b/pkg/policymap/policy_and_data_map.go index 79ce87c..9c97084 100644 --- a/pkg/policymap/policy_and_data_map.go +++ b/pkg/policymap/policy_and_data_map.go @@ -22,12 +22,18 @@ package policymap import ( "encoding/json" "fmt" + "policy-opa-pdp/consts" "policy-opa-pdp/pkg/log" "policy-opa-pdp/pkg/model" ) +type ( + FormatMapOfAnyTypeFunc[T any] func(mapOfAnyType T) (string, error) +) + var ( - LastDeployedPolicies string + LastDeployedPolicies string + FormatMapOfAnyTypeVar FormatMapOfAnyTypeFunc[interface{}] = FormatMapofAnyType ) func formatPolicyAndDataMap(deployedPolicies []map[string]interface{}) (string, error) { @@ -38,7 +44,7 @@ func formatPolicyAndDataMap(deployedPolicies []map[string]interface{}) (string, } // Marshal the final map into JSON - policyDataJSON, err := FormatMapofAnyType(finalMap) + policyDataJSON, err := FormatMapOfAnyTypeVar(finalMap) if err != nil { return "", fmt.Errorf("failed to format json: %v", err) } @@ -82,7 +88,7 @@ func UpdateDeployedPoliciesinMap(policy model.ToscaPolicy) (string, error) { // Unmarshal the last known policies deployedPolicies, err := UnmarshalLastDeployedPolicies(LastDeployedPolicies) if err != nil { - log.Warnf("Failed to unmarshal LastDeployedPolicies: %v", err) + log.Warnf("Failed to unmarshal LastDeployedPolicies While Updating Deployed Policies: %v", err) } dataKeys := make([]string, 0, len(policy.Properties.Data)) @@ -97,10 +103,10 @@ func UpdateDeployedPoliciesinMap(policy model.ToscaPolicy) (string, error) { } directoryMap := map[string]interface{}{ - "policy-id": policy.Metadata.PolicyID, - "policy-version": policy.Metadata.PolicyVersion, - "data": dataKeys, - "policy": policyKeys, + consts.PolicyId: policy.Metadata.PolicyID, + consts.PolicyVersion: policy.Metadata.PolicyVersion, + "data": dataKeys, + "policy": policyKeys, } deployedPolicies = append(deployedPolicies, directoryMap) return formatPolicyAndDataMap(deployedPolicies) @@ -111,14 +117,14 @@ func RemoveUndeployedPoliciesfromMap(undeployedPolicy map[string]interface{}) (s // Unmarshal the last known policies deployedPolicies, err := UnmarshalLastDeployedPolicies(LastDeployedPolicies) if err != nil { - log.Warnf("Failed to unmarshal LastDeployedPolicies: %v", err) + log.Warnf("Failed to unmarshal LastDeployedPolicies While Removing Undeployed Policies From Map: %v", err) } remainingPolicies := []map[string]interface{}{} for _, policy := range deployedPolicies { shouldRetain := true - if policy["policy-id"] == undeployedPolicy["policy-id"] && policy["policy-version"] == undeployedPolicy["policy-version"] { + if policy[consts.PolicyId] == undeployedPolicy[consts.PolicyId] && policy[consts.PolicyVersion] == undeployedPolicy[consts.PolicyVersion] { shouldRetain = false } if shouldRetain { @@ -137,7 +143,7 @@ func VerifyAndReturnPoliciesToBeDeployed(lastdeployedPoliciesMap string, pdpUpda var policiesMap PoliciesMap err := json.Unmarshal([]byte(lastdeployedPoliciesMap), &policiesMap) if err != nil { - log.Warnf("Failed to unmarshal LastDeployedPolicies: %v", err) + log.Warnf("Failed to unmarshal LastDeployedPolicies While Verifying Policies to be deployed: %v", err) return pdpUpdate.PoliciesToBeDeployed } @@ -147,7 +153,7 @@ func VerifyAndReturnPoliciesToBeDeployed(lastdeployedPoliciesMap string, pdpUpda for _, deployingPolicy := range pdpUpdate.PoliciesToBeDeployed { shouldDeploy := true for _, deployedPolicy := range deployedPolicies { - if deployedPolicy["policy-id"] == deployingPolicy.Name && deployedPolicy["policy-version"] == deployingPolicy.Version { + if deployedPolicy[consts.PolicyId] == deployingPolicy.Name && deployedPolicy[consts.PolicyVersion] == deployingPolicy.Version { log.Infof("Policy Previously deployed: %v %v, skipping", deployingPolicy.Name, deployingPolicy.Version) shouldDeploy = false break @@ -169,7 +175,7 @@ func ExtractDeployedPolicies(policiesMap string) []model.ToscaConceptIdentifier // Unmarshal the last known policies deployedPolicies, err := UnmarshalLastDeployedPolicies(policiesMap) if err != nil { - log.Warnf("Failed to unmarshal LastDeployedPolicies: %v", err) + log.Warnf("Failed to unmarshal LastDeployedPolicies While Extracting Deployed Policies: %v", err) } pdpstatus := model.PdpStatus{ @@ -179,8 +185,8 @@ func ExtractDeployedPolicies(policiesMap string) []model.ToscaConceptIdentifier for _, policy := range deployedPolicies { // Extract policy-id and policy-version - policyID, idOk := policy["policy-id"].(string) - policyVersion, versionOk := policy["policy-version"].(string) + policyID, idOk := policy[consts.PolicyId].(string) + policyVersion, versionOk := policy[consts.PolicyVersion].(string) if !idOk || !versionOk { log.Warnf("Missing or invalid policy-id or policy-version") return nil @@ -199,13 +205,13 @@ func CheckIfPolicyAlreadyExists(policyId string) bool { // Unmarshal the last known policies deployedPolicies, err := UnmarshalLastDeployedPolicies(LastDeployedPolicies) if err != nil { - log.Warnf("Failed to unmarshal LastDeployedPolicies: %v", err) + log.Warnf("Failed to unmarshal LastDeployedPolicies While Checking if Policy Already Exists: %v", err) } log.Debugf("deployedPolicies %s", deployedPolicies) for _, policy := range deployedPolicies { - if policy["policy-id"] == policyId { + if policy[consts.PolicyId] == policyId { return true } } @@ -216,7 +222,7 @@ func CheckIfPolicyAlreadyExists(policyId string) bool { func GetTotalDeployedPoliciesCountFromMap() int { deployedPolicies, err := UnmarshalLastDeployedPolicies(LastDeployedPolicies) if err != nil { - log.Warnf("Failed to unmarshal LastDeployedPolicies: %v", err) + log.Warnf("Failed to unmarshal LastDeployedPolicies While Getting TotalDeployedPoliciesCountFromMap: %v", err) return 0 } return len(deployedPolicies) diff --git a/pkg/utils/sort_test.go b/pkg/utils/sort_test.go new file mode 100644 index 0000000..715a6b5 --- /dev/null +++ b/pkg/utils/sort_test.go @@ -0,0 +1,68 @@ +// - +// ========================LICENSE_START================================= +// Copyright (C) 2025: Deutsche Telekom +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// SPDX-License-Identifier: Apache-2.0 +// ========================LICENSE_END=================================== + +// Package provides sorting functionalities. + +package utils + +import ( + "sort" + "testing" +) + +// Test sorting in ascending order by dot count +func TestByDotCountAscending(t *testing.T) { + keys := []string{"a.b.c", "a.b", "a.b.c.d", "a"} + expected := []string{"a", "a.b", "a.b.c", "a.b.c.d"} + + sort.Sort(ByDotCount{Keys: keys, Ascend: true}) + + for i, v := range keys { + if v != expected[i] { + t.Errorf("Ascending sort failed. Expected %s, got %s", expected[i], v) + } + } +} + +// Test sorting in descending order by dot count +func TestByDotCountDescending(t *testing.T) { + keys := []string{"a.b.c", "a.b", "a.b.c.d", "a"} + expected := []string{"a.b.c.d", "a.b.c", "a.b", "a"} + + sort.Sort(ByDotCount{Keys: keys, Ascend: false}) + + for i, v := range keys { + if v != expected[i] { + t.Errorf("Descending sort failed. Expected %s, got %s", expected[i], v) + } + } +} + +// Test sorting with equal dot counts +func TestByDotCountEqualDots(t *testing.T) { + keys := []string{"a.b.c", "x.y.z", "m.n.o"} + expected := []string{"a.b.c", "x.y.z", "m.n.o"} // Order should be preserved as all have same dots + + sort.Sort(ByDotCount{Keys: keys, Ascend: true}) + + for i, v := range keys { + if v != expected[i] { + t.Errorf("Equal dot count sorting failed. Expected %s, got %s", expected[i], v) + } + } +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index fba1fb1..fd09bd5 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -38,11 +38,13 @@ import ( type ( CreateDirectoryFunc func(dirPath string) error + ValidateFieldsStructsFunc func(pdpUpdate model.PdpUpdate) error ) var ( CreateDirectoryVar CreateDirectoryFunc = CreateDirectory removeAll = os.RemoveAll + ValidateFieldsStructsVar ValidateFieldsStructsFunc = ValidateFieldsStructs ) // validates if the given request is in valid uuid form @@ -157,29 +159,46 @@ func ValidateToscaPolicyJsonFields(policy model.ToscaPolicy) error { } if policy.Properties.Data != nil { - // 2. Validate that Name is a suffix for keys in Properties.Data and Properties.Policy. - keySeen := make(map[string]bool) - for key := range policy.Properties.Data { - if keySeen[key] { - return fmt.Errorf("duplicate data key '%s' found, '%s'", key, emphasize) - } - keySeen[key] = true - if !strings.HasPrefix(key, "node."+policy.Name) { - return fmt.Errorf("data key '%s' does not have name node.'%s' as a prefix, '%s'", key, policy.Name, emphasize) - } + log.Debugf("Validating properties data for policy: %s", policy.Name) + if err := validateDataKeys(policy.Properties.Data, "node."+policy.Name, "data", emphasize); err != nil { + return err } } + + log.Debugf("Validating properties policy for policy: %s", policy.Name) + if err := validatePolicyKeys(policy.Properties.Policy, policy.Name, "policy", emphasize); err != nil { + return err + } + + log.Infof("Validation successful for policy: %s", policy.Name) + return nil +} + +func validatePolicyKeys(policy map[string]string, prefix, propertyType, emphasize string) error { keySeen := make(map[string]bool) - for key := range policy.Properties.Policy { + for key := range policy { if keySeen[key] { - return fmt.Errorf("duplicate policy key '%s' found, '%s'", key, emphasize) + return fmt.Errorf("duplicate %s key '%s' found, '%s'", propertyType, key, emphasize) } keySeen[key] = true - if !strings.HasPrefix(key, policy.Name) { - return fmt.Errorf("policy key '%s' does not have name '%s' as a prefix, '%s'", key, policy.Name, emphasize) + if !strings.HasPrefix(key, prefix) { + return fmt.Errorf("%s key '%s' does not have name '%s' as a prefix, '%s'", propertyType, key, prefix, emphasize) } } + return nil +} +func validateDataKeys(data map[string]string, prefix, propertyType, emphasize string) error { + keySeen := make(map[string]bool) + for key := range data { + if keySeen[key] { + return fmt.Errorf("duplicate %s key '%s' found, '%s'", propertyType, key, emphasize) + } + keySeen[key] = true + if !strings.HasPrefix(key, prefix) { + return fmt.Errorf("data key '%s' does not have name '%s' as a prefix", key, prefix) + } + } return nil } @@ -342,112 +361,113 @@ func BuildBundle(cmdFunc func(string, ...string) *exec.Cmd) (string, error) { return string(output), nil } -// Validation function -func ValidateOPADataRequest(request interface{}) []string { - var validationErrors []string - if updateRequest, ok := request.(*oapicodegen.OPADataUpdateRequest); ok { - if updateRequest == nil { // Check if updateRequest is nil - validationErrors = append(validationErrors, "OPADataUpdateRequest is nil") - return validationErrors // Return if the request is nil - } - // Check if required fields are populated - if updateRequest.CurrentDate != nil { - dateString := updateRequest.CurrentDate.String() - if !IsValidCurrentDate(&dateString) { - validationErrors = append(validationErrors, "CurrentDate is invalid") - } - } else { - validationErrors = append(validationErrors, "CurrentDate is required") - } - - // Validate CurrentDateTime format - if !(IsValidTime(updateRequest.CurrentDateTime)) { - validationErrors = append(validationErrors, "CurrentDateTime is invalid or missing") - } - // Validate CurrentTime format - if !(IsValidCurrentTime(updateRequest.CurrentTime)) { - validationErrors = append(validationErrors, "CurrentTime is invalid or missing") - } +type CommonFields struct { + CurrentDate *string + CurrentDateTime *time.Time + CurrentTime *string + TimeOffset *string + TimeZone *string + OnapComponent *string + OnapInstance *string + OnapName *string + PolicyName string +} - // Validate TimeOffset format (e.g., +02:00 or -05:00) - if !(IsValidTimeOffset(updateRequest.TimeOffset)) { - validationErrors = append(validationErrors, "TimeOffset is invalid or missing") - } +func ValidateOPADataRequest(request interface{}) []string { + // var validationErrors []string - // Validate TimeZone format (e.g., 'America/New_York') - if !(IsValidTimeZone(updateRequest.TimeZone)) { - validationErrors = append(validationErrors, "TimeZone is invalid or missing") - } + if request == nil { + return []string{"Request is nil"} + } - // Optionally, check if 'OnapComponent', 'OnapInstance', 'OnapName', and 'PolicyName' are provided - if !(IsValidString(updateRequest.OnapComponent)) { - validationErrors = append(validationErrors, "OnapComponent is required") + // Handle OPADataUpdateRequest validation + if updateReq, ok := request.(*oapicodegen.OPADataUpdateRequest); ok { + currentDate := "" + if updateReq.CurrentDate != nil { + currentDate = updateReq.CurrentDate.String() } - if !(IsValidString(updateRequest.OnapInstance)) { - validationErrors = append(validationErrors, "OnapInstance is required") - } + commonFields := CommonFields{ + CurrentDate: ¤tDate, + CurrentDateTime: updateReq.CurrentDateTime, + CurrentTime: updateReq.CurrentTime, + TimeOffset: updateReq.TimeOffset, + TimeZone: updateReq.TimeZone, + OnapComponent: updateReq.OnapComponent, + OnapInstance: updateReq.OnapInstance, + OnapName: updateReq.OnapName, + PolicyName: convertPtrToString(updateReq.PolicyName), - if !(IsValidString(updateRequest.OnapName)) { - validationErrors = append(validationErrors, "OnapName is required") } + return validateCommonFields(commonFields) - if !(IsValidString(updateRequest.PolicyName)) { - validationErrors = append(validationErrors, "PolicyName is required and cannot be empty") - } } - if decisionRequest, ok := request.(*oapicodegen.OPADecisionRequest); ok { - - if decisionRequest == nil { // Check if decisionRequest is nil - validationErrors = append(validationErrors, "OPADecisionRequest is nil") - return validationErrors // Return if the request is nil - } - // Check if required fields are populated - if decisionRequest.CurrentDate != nil { - dateString := decisionRequest.CurrentDate.String() - if !IsValidCurrentDate(&dateString) { - validationErrors = append(validationErrors, "CurrentDate is invalid") - } + // Handle OPADecisionRequest validation + if decisionReq, ok := request.(*oapicodegen.OPADecisionRequest); ok { + currentDate := "" + if decisionReq.CurrentDate != nil { + currentDate = decisionReq.CurrentDate.String() } - // Validate CurrentDateTime format - if (decisionRequest.CurrentDateTime != nil) && !(IsValidTime(decisionRequest.CurrentDateTime)) { - validationErrors = append(validationErrors, "CurrentDateTime is invalid or missing") + commonFields := CommonFields{ + CurrentDate: ¤tDate, + CurrentDateTime: decisionReq.CurrentDateTime, + CurrentTime: decisionReq.CurrentTime, + TimeOffset: decisionReq.TimeOffset, + TimeZone: decisionReq.TimeZone, + OnapComponent: decisionReq.OnapComponent, + OnapInstance: decisionReq.OnapInstance, + OnapName: decisionReq.OnapName, + PolicyName: decisionReq.PolicyName, } + return validateCommonFields(commonFields) - // Validate CurrentTime format - if (decisionRequest.CurrentTime != nil) && !(IsValidCurrentTime(decisionRequest.CurrentTime)) { - validationErrors = append(validationErrors, "CurrentTime is invalid or missing") - } - - // Validate TimeOffset format (e.g., +02:00 or -05:00) - if (decisionRequest.TimeOffset != nil) && !(IsValidTimeOffset(decisionRequest.TimeOffset)) { - validationErrors = append(validationErrors, "TimeOffset is invalid or missing") - } + } + return []string{"Invalid request type"} +} - // Validate TimeZone format (e.g., 'America/New_York') - if (decisionRequest.TimeZone != nil) && !(IsValidTimeZone(decisionRequest.TimeZone)) { - validationErrors = append(validationErrors, "TimeZone is invalid or missing") - } +func convertPtrToString(stringPtr *string) string { + if stringPtr != nil { + return *stringPtr + } + return "" - // Optionally, check if 'OnapComponent', 'OnapInstance', 'OnapName', and 'PolicyName' are provided - if (decisionRequest.OnapComponent != nil) && !(IsValidString(decisionRequest.OnapComponent)) { - validationErrors = append(validationErrors, "OnapComponent is required") - } +} - if (decisionRequest.OnapInstance != nil) && !(IsValidString(decisionRequest.OnapInstance)) { - validationErrors = append(validationErrors, "OnapInstance is required") - } +// Helper function to validate common fields +func validateCommonFields(fields CommonFields) []string { - if (decisionRequest.OnapName != nil) && !(IsValidString(decisionRequest.OnapName)) { - validationErrors = append(validationErrors, "OnapName is required") - } + var validationErrors []string - if !(IsValidString(decisionRequest.PolicyName)) { - validationErrors = append(validationErrors, "PolicyName is required and cannot be empty") - } + if fields.CurrentDate == nil || !IsValidCurrentDate(fields.CurrentDate) { + validationErrors = append(validationErrors, "CurrentDate is invalid or missing") + } + if fields.CurrentDateTime == nil || !IsValidTime(fields.CurrentDateTime) { + validationErrors = append(validationErrors, "CurrentDateTime is invalid or missing") + } + if fields.CurrentTime == nil || !IsValidCurrentTime(fields.CurrentTime) { + validationErrors = append(validationErrors, "CurrentTime is invalid or missing") } + if fields.TimeOffset == nil || !IsValidTimeOffset(fields.TimeOffset) { + validationErrors = append(validationErrors, "TimeOffset is invalid or missing") + } + if fields.TimeZone == nil || !IsValidTimeZone(fields.TimeZone) { + validationErrors = append(validationErrors, "TimeZone is invalid or missing") + } + if !IsValidString(fields.OnapComponent) { + validationErrors = append(validationErrors, "OnapComponent is required") + } + if !IsValidString(fields.OnapInstance) { + validationErrors = append(validationErrors, "OnapInstance is required") + } + if !IsValidString(fields.OnapName) { + validationErrors = append(validationErrors, "OnapName is required") + } + if !IsValidString(fields.PolicyName) { + validationErrors = append(validationErrors, "PolicyName is required and cannot be empty") + } + return validationErrors } diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index a76a435..f6e4d29 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -99,13 +99,6 @@ func TestCreateDirectory_Negative(t *testing.T) { assert.Error(t, err) } -func TestCreateDirectory_InvalidPath(t *testing.T) { - tempDir := "/invalid///path" - defer os.RemoveAll(tempDir) - err := CreateDirectory(tempDir) - assert.Error(t, err) -} - func TestRemoveDirectory_Positive(t *testing.T) { tempDir, err := os.MkdirTemp("", "testdir") assert.NoError(t, err) @@ -478,7 +471,7 @@ func TestValidateToscaPolicyJsonFields_InvalidDataKeyPrefix(t *testing.T) { err := ValidateToscaPolicyJsonFields(policy) assert.Error(t, err, "Expected error due to invalid data key prefix") - assert.Contains(t, err.Error(), "data key 'invalid-key' does not have name node.'test-policy' as a prefix") + assert.Contains(t, err.Error(), "data key 'invalid-key' does not have name 'node.test-policy' as a prefix") } func TestIsValidTime(t *testing.T) { @@ -684,60 +677,208 @@ func TestIsSubDirEmpty(t *testing.T) { }) } -func TestValidateOPADataRequest(t *testing.T) { - ctime := "08:26:41.857Z" - onapComp := "COMPONENT" - onapIns := "INSTANCE" - onapName := "ONAP" - policyName := "s3" +// Helper function to create string pointers +func strPtr(s string) *string { + return &s +} + +// Helper function to create time pointers +func timePtr(t time.Time) *time.Time { + return &t +} + +func TestValidateOPADataRequest_UpdateRequest_Valid(t *testing.T) { + parsedDate, err := time.Parse("2006-01-02", "2024-02-12") if err != nil { fmt.Println("error in parsedDate") } + currentDate := openapi_types.Date{Time: parsedDate} currentDateTime, err := time.Parse(time.RFC3339, "2024-02-12T12:00:00Z") if err != nil { fmt.Println("error in currentDateTime") } + ctime := "08:26:41.857Z" - inValidDecisionRequest := &oapicodegen.OPADecisionRequest{ + updateReq := &oapicodegen.OPADataUpdateRequest{ CurrentDate: ¤tDate, CurrentDateTime: ¤tDateTime, + CurrentTime: &ctime, + TimeOffset: strPtr("+00:00"), + TimeZone: strPtr("UTC"), + OnapComponent: strPtr("Component"), + OnapInstance: strPtr("Instance"), + OnapName: strPtr("Onap"), + PolicyName: strPtr("Policy1"), } - var data []map[string]interface{} + errors := ValidateOPADataRequest(updateReq) + assert.Empty(t, errors, "Expected no validation errors") +} + +func TestValidateOPADataRequest_UpdateRequest_MissingFields(t *testing.T) { + updateReq := &oapicodegen.OPADataUpdateRequest{} + + errors := ValidateOPADataRequest(updateReq) + assert.NotEmpty(t, errors, "Expected validation errors for missing fields") +} - data = nil +func TestValidateOPADataRequest_DecisionRequest_Valid(t *testing.T) { + parsedDate, err := time.Parse("2006-01-02", "2024-02-12") + if err != nil { + fmt.Println("error in parsedDate") + } - inValidRequest := &oapicodegen.OPADataUpdateRequest{ + currentDate := openapi_types.Date{Time: parsedDate} + currentDateTime, err := time.Parse(time.RFC3339, "2024-02-12T12:00:00Z") + if err != nil { + fmt.Println("error in currentDateTime") + } + ctime := "08:26:41.857Z" + + decisionReq := &oapicodegen.OPADecisionRequest{ CurrentDate: ¤tDate, CurrentDateTime: ¤tDateTime, CurrentTime: &ctime, - OnapComponent: &onapComp, - OnapInstance: &onapIns, - OnapName: &onapName, - PolicyName: &policyName, - Data: &data, + TimeOffset: strPtr("+00:00"), + TimeZone: strPtr("UTC"), + OnapComponent: strPtr("Component"), + OnapInstance: strPtr("Instance"), + OnapName: strPtr("Onap"), + PolicyName: "Policy2", + } + + errors := ValidateOPADataRequest(decisionReq) + assert.Empty(t, errors, "Expected no validation errors") +} + +func TestValidateOPADataRequest_DecisionRequest_MissingFields(t *testing.T) { + decisionReq := &oapicodegen.OPADecisionRequest{} + + errors := ValidateOPADataRequest(decisionReq) + assert.NotEmpty(t, errors, "Expected validation errors for missing fields") +} + +func TestValidateOPADataRequest_InvalidRequestType(t *testing.T) { + invalidReq := struct{}{} + + errors := ValidateOPADataRequest(invalidReq) + assert.Equal(t, []string{"Invalid request type"}, errors, "Expected 'Invalid request type' error") +} + +func TestConvertPtrToString(t *testing.T) { + str := "test" + assert.Equal(t, "test", convertPtrToString(&str), "Expected 'test' as output") + assert.Equal(t, "", convertPtrToString(nil), "Expected empty string as output") +} + +// Test validatePolicyKeys +func TestValidatePolicyKeys(t *testing.T) { + tests := []struct { + name string + policy map[string]string + prefix string + propertyType string + emphasize string + wantErr string + }{ + { + name: "Valid policy keys", + policy: map[string]string{ + "policy_1": "value1", + "policy_2": "value2", + }, + prefix: "policy_", + propertyType: "policy", + emphasize: "important", + wantErr: "", + }, + { + name: "Policy key without proper prefix", + policy: map[string]string{ + "invalid_1": "value1", + }, + prefix: "policy_", + propertyType: "policy", + emphasize: "important", + wantErr: "policy key 'invalid_1' does not have name 'policy_' as a prefix, 'important'", + }, + { + name: "Empty policy map", + policy: map[string]string{}, + prefix: "policy_", + propertyType: "policy", + emphasize: "important", + wantErr: "", + }, } - inValidErr := []string{"TimeOffset is invalid or missing", "TimeZone is invalid or missing"} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePolicyKeys(tt.policy, tt.prefix, tt.propertyType, tt.emphasize) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("Expected error: '%s', got: '%s'", tt.wantErr, err.Error()) + } + } else if tt.wantErr != "" { + t.Errorf("Expected error: '%s', got nil", tt.wantErr) + } + }) + } +} - inValidDecisionErrs := []string{"PolicyName is required and cannot be empty"} +// Test validateDataKeys +func TestValidateDataKeys(t *testing.T) { tests := []struct { - name string - request interface{} - expectedErr []string + name string + data map[string]string + prefix string + propertyType string + emphasize string + wantErr string }{ - {"Valid Request", inValidRequest, inValidErr}, - {"Invalid OPADecisionRequest", inValidDecisionRequest, inValidDecisionErrs}, + { + name: "Valid data keys", + data: map[string]string{ + "data_1": "value1", + "data_2": "value2", + }, + prefix: "data_", + propertyType: "data", + emphasize: "critical", + wantErr: "", + }, + { + name: "Data key without proper prefix", + data: map[string]string{ + "invalid_1": "value1", + }, + prefix: "data_", + propertyType: "data", + emphasize: "critical", + wantErr: "data key 'invalid_1' does not have name 'data_' as a prefix", + }, + { + name: "Empty data map", + data: map[string]string{}, + prefix: "data_", + propertyType: "data", + emphasize: "critical", + wantErr: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - errors := ValidateOPADataRequest(tt.request) - fmt.Printf("error : %s", errors) - fmt.Printf("error len : %d", len(errors)) - assert.Equal(t, tt.expectedErr, errors) + err := validateDataKeys(tt.data, tt.prefix, tt.propertyType, tt.emphasize) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("Expected error: '%s', got: '%s'", tt.wantErr, err.Error()) + } + } else if tt.wantErr != "" { + t.Errorf("Expected error: '%s', got nil", tt.wantErr) + } }) } } -- 2.16.6