From d3aa5b98789c65107e5ac41fc5be11f08acf11d8 Mon Sep 17 00:00:00 2001 From: Timo Tietavainen Date: Thu, 12 Nov 2020 03:50:01 +0200 Subject: [PATCH] Improve appmgr UT coverage Improve existing unit tests coverage for appmgr's 'cm', 'helm' and 'resthooks' packages. Fix also 'helm install' command's overwrite configuration '---set' flag to be '--set'. Signed-off-by: Timo Tietavainen Change-Id: I2a2f13575fc10191c7ee1fe21e8d3ec344cb71cc --- pkg/cm/cm.go | 28 +-- pkg/cm/cm_test.go | 390 ++++++++++++++++++++++++++++++++++-- pkg/helm/helm.go | 14 +- pkg/helm/helm_test.go | 433 ++++++++++++++++++++++++++++++++++++++-- pkg/resthooks/resthooks.go | 7 +- pkg/resthooks/resthooks_test.go | 256 ++++++++++++++++++++++-- pkg/resthooks/types.go | 10 +- test/dummy-xapp_values.json | 6 + test/faulty_schema.json | 0 9 files changed, 1066 insertions(+), 78 deletions(-) create mode 100644 test/dummy-xapp_values.json create mode 100644 test/faulty_schema.json diff --git a/pkg/cm/cm.go b/pkg/cm/cm.go index 9ff2876..9857e00 100755 --- a/pkg/cm/cm.go +++ b/pkg/cm/cm.go @@ -23,21 +23,24 @@ import ( "encoding/json" "errors" "fmt" + "github.com/spf13/viper" + "github.com/valyala/fastjson" + "github.com/xeipuuv/gojsonschema" "io/ioutil" "os" "path" "regexp" - "strings" "strconv" - "github.com/spf13/viper" - "github.com/valyala/fastjson" - "github.com/xeipuuv/gojsonschema" + "strings" "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/appmgr" "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/models" "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/util" ) +var kubeExec = util.KubectlExec +var helmExec = util.HelmExec + type CM struct{} func NewCM() *CM { @@ -69,7 +72,7 @@ func (cm *CM) UploadConfigElement(Element string) (configList models.AllXappConf c := models.XAppConfig{ Metadata: &models.ConfigMetadata{XappName: &xAppName, Namespace: &namespace}, - Config: activeConfig, + Config: activeConfig, } configList = append(configList, &c) } @@ -104,7 +107,7 @@ func (cm *CM) ReadSchema(name string, desc *interface{}) (err error) { } func (cm *CM) UpdateConfigMap(r models.XAppConfig) (models.ConfigValidationErrors, error) { - fmt.Printf("Configmap update: xappName=%s namespace=%s config: %v", *r.Metadata.XappName, *r.Metadata.Namespace, r.Config) + fmt.Printf("Configmap update: xappName=%s namespace=%s config: %v\n", *r.Metadata.XappName, *r.Metadata.Namespace, r.Config) if validationErrors, err := cm.Validate(r); err != nil { return validationErrors, err } @@ -153,7 +156,6 @@ func (cm *CM) ParseJson(dsContent string) (*fastjson.Value, error) { return v, err } - func (cm *CM) GenerateJSONFile(jsonString string) error { cmJson, err := json.RawMessage(jsonString).MarshalJSON() if err != nil { @@ -188,14 +190,14 @@ func (cm *CM) ReadFile(name string, data interface{}) (err error) { func (cm *CM) ReadConfigmap(name string, ns string) (string, error) { args := fmt.Sprintf("get configmap -o jsonpath='{.data.config-file\\.json}' -n %s %s", ns, cm.GetConfigMapName(name, ns)) - out, err := util.KubectlExec(args) + out, err := kubeExec(args) return string(out), err } -func (cm *CM) ReplaceConfigMap(name, ns string) (error) { +func (cm *CM) ReplaceConfigMap(name, ns string) error { cmd := " create configmap -n %s %s --from-file=%s -o json --dry-run | kubectl replace -f -" args := fmt.Sprintf(cmd, ns, cm.GetConfigMapName(name, ns), viper.GetString("xapp.tmpConfig")) - _, err := util.KubectlExec(args) + _, err := kubeExec(args) return err } @@ -204,7 +206,7 @@ func (cm *CM) FetchChart(name string) (err error) { repo := viper.GetString("helm.repo-name") fetchArgs := fmt.Sprintf("--untar --untardir %s %s/%s", tarDir, repo, name) - _, err = util.HelmExec(strings.Join([]string{"fetch ", fetchArgs}, "")) + _, err = helmExec(strings.Join([]string{"fetch ", fetchArgs}, "")) return } @@ -213,7 +215,7 @@ func (cm *CM) GetRtmData(name string) (msgs appmgr.RtmData) { ns := cm.GetNamespace("") args := fmt.Sprintf("get configmap -o jsonpath='{.data.config-file\\.json}' -n %s %s", ns, cm.GetConfigMapName(name, ns)) - out, err := util.KubectlExec(args) + out, err := kubeExec(args) if err != nil { return } @@ -262,7 +264,7 @@ func (cm *CM) GetNamesFromHelmRepo() (names []string) { rname := viper.GetString("helm.repo-name") cmdArgs := strings.Join([]string{"search ", rname}, "") - out, err := util.HelmExec(cmdArgs) + out, err := helmExec(cmdArgs) if err != nil { return } diff --git a/pkg/cm/cm_test.go b/pkg/cm/cm_test.go index 01f5bc5..b180fc9 100755 --- a/pkg/cm/cm_test.go +++ b/pkg/cm/cm_test.go @@ -24,6 +24,7 @@ import ( "errors" "os" "reflect" + "strings" "testing" "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/appmgr" @@ -31,6 +32,26 @@ import ( "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/util" ) +const ( + expectedHelmSearchCmd = "search helm-repo" + expectedHelmFetchCmd = `fetch --untar --untardir /tmp helm-repo/dummy-xapp` +) + +var caughtKubeExecArgs []string +var kubeExecRetOut string +var kubeExecRetErr error +var caughtHelmExecArgs string +var helmExecRetOut string +var helmExecRetErr error + +var expectedKubectlGetCmd []string = []string{ + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-anr-appconfig`, + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-appmgr-appconfig`, + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-dualco-appconfig`, + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-reporter-appconfig`, + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-uemgr-appconfig`, +} + var helmSearchOutput = ` helm-repo/anr 0.0.1 1.0 Helm Chart for Nokia ANR (Automatic Neighbour Relation) xAPP helm-repo/appmgr 0.0.2 1.0 Helm Chart for xAppManager @@ -62,6 +83,15 @@ var kubectlConfigmapOutput = ` } } ` +var cfgData = `{ + "active":true, + "interfaceId": { + "globalENBId":{ + "plmnId": "1234", + "eNBId":"55" + } + } +}` type ConfigSample struct { Level int @@ -112,20 +142,181 @@ func TestMain(m *testing.M) { os.Exit(code) } -func TestGetRtmData(t *testing.T) { +func TestUploadConfigAllSuccess(t *testing.T) { + var cfg interface{} + var expectedResult models.AllXappConfig + ns := "ricxapp" + xapps := []string{"anr", "appmgr", "dualco", "reporter", "uemgr"} + + if ret := json.Unmarshal([]byte(cfgData), &cfg); ret != nil { + t.Errorf("UploadConfigAll Json unmarshal failed: %v", ret) + } + + for i, _ := range xapps { + expectedResult = append(expectedResult, + &models.XAppConfig{ + Config: cfg, + Metadata: &models.ConfigMetadata{ + Namespace: &ns, + XappName: &xapps[i], + }, + }, + ) + } + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + //Fake helm search success + helmExecRetOut = helmSearchOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' success + kubeExecRetOut = strings.ReplaceAll(cfgData, "\\", "") + + result := NewCM().UploadConfigAll() + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("UploadConfigAll failed: expected: %v, got: %v", expectedResult, result) + } + if caughtHelmExecArgs != expectedHelmSearchCmd { + t.Errorf("UploadConfigAll failed: expected: %v, got: %v", expectedHelmSearchCmd, caughtHelmExecArgs) + } + if !reflect.DeepEqual(caughtKubeExecArgs, expectedKubectlGetCmd) { + t.Errorf("UploadConfigAll failed: expected: %v, got: %v", expectedKubectlGetCmd, caughtKubeExecArgs) + } +} + +func TestUploadConfigAllReturnsEmptyMapIfAllConfigMapReadsFail(t *testing.T) { + var expectedResult models.AllXappConfig + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + //Fake helm search success + helmExecRetOut = helmSearchOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' failure + kubeExecRetErr = errors.New("some error") + + result := NewCM().UploadConfigAll() + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("UploadConfigAll failed: expected: %v, got: %v", expectedResult, result) + } +} + +func TestUploadConfigElementSuccess(t *testing.T) { + var cfg interface{} + var expectedResult models.AllXappConfig + ns := "ricxapp" + xapps := []string{"anr", "appmgr", "dualco", "reporter", "uemgr"} + + if ret := json.Unmarshal([]byte(cfgData), &cfg); ret != nil { + t.Errorf("UploadConfigElement Json unmarshal failed: %v", ret) + } + + for i, _ := range xapps { + expectedResult = append(expectedResult, + &models.XAppConfig{ + Config: cfg.(map[string]interface{})["active"], + Metadata: &models.ConfigMetadata{ + Namespace: &ns, + XappName: &xapps[i], + }, + }, + ) + } + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + //Fake helm search success + helmExecRetOut = helmSearchOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' success + kubeExecRetOut = strings.ReplaceAll(cfgData, "\\", "") + + result := NewCM().UploadConfigElement("active") + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("UploadConfigElement failed: expected: %v, got: %v", expectedResult, result) + } + if caughtHelmExecArgs != expectedHelmSearchCmd { + t.Errorf("UploadConfigElement failed: expected: %v, got: %v", expectedHelmSearchCmd, caughtHelmExecArgs) + } + if !reflect.DeepEqual(caughtKubeExecArgs, expectedKubectlGetCmd) { + t.Errorf("UploadConfigElement failed: expected: %v, got: %v", expectedKubectlGetCmd, caughtKubeExecArgs) + } +} + +func TestUploadConfigElementReturnsEmptyMapIfElementMissingFromConfigMap(t *testing.T) { + var expectedResult models.AllXappConfig + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + //Fake helm search success + helmExecRetOut = helmSearchOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' success + kubeExecRetOut = strings.ReplaceAll(cfgData, "\\", "") + + //Try to upload non-existing configuration element + result := NewCM().UploadConfigElement("some-not-existing-element") + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("UploadConfigElement failed: expected: %v, got: %v", expectedResult, result) + } +} + +func TestGetRtmDataSuccess(t *testing.T) { + expectedKubeCmd := []string{ + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-dummy-xapp-appconfig`, + } expectedMsgs := appmgr.RtmData{ TxMessages: []string{"RIC_X2_LOAD_INFORMATION"}, RxMessages: []string{"RIC_X2_LOAD_INFORMATION"}, Policies: []int64{11, 22, 33}, } - util.KubectlExec = func(args string) (out []byte, err error) { - return []byte(kubectlConfigmapOutput), nil + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' success + kubeExecRetOut = kubectlConfigmapOutput + + result := NewCM().GetRtmData("dummy-xapp") + if !reflect.DeepEqual(result, expectedMsgs) { + t.Errorf("GetRtmData failed: expected: %v, got: %v", expectedMsgs, result) + } + if !reflect.DeepEqual(caughtKubeExecArgs, expectedKubeCmd) { + t.Errorf("GetRtmData failed: expected: '%v', got: '%v'", expectedKubeCmd, caughtKubeExecArgs) + } +} + +func TestGetRtmDataReturnsNoDataIfConfigmapGetFails(t *testing.T) { + var expectedMsgs appmgr.RtmData + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' failure + kubeExecRetErr = errors.New("some error") + + result := NewCM().GetRtmData("dummy-xapp") + if !reflect.DeepEqual(result, expectedMsgs) { + t.Errorf("GetRtmData failed: expected: %v, got: %v", expectedMsgs, result) } +} + +func TestGetRtmDataReturnsNoDataIfJsonParseFails(t *testing.T) { + var expectedMsgs appmgr.RtmData + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' to return nothing what will cause JSON parse failure result := NewCM().GetRtmData("dummy-xapp") if !reflect.DeepEqual(result, expectedMsgs) { - t.Errorf("TestGetRtmData failed: expected: %v, got: %v", expectedMsgs, result) + t.Errorf("GetRtmData failed: expected: %v, got: %v", expectedMsgs, result) } } @@ -146,32 +337,50 @@ func TestFetchChartFails(t *testing.T) { } func TestFetchChartSuccess(t *testing.T) { - util.HelmExec = func(args string) (out []byte, err error) { - return - } + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec if NewCM().FetchChart("dummy-xapp") != nil { t.Errorf("TestFetchChart failed!") } } +func TestGetNamespaceSuccess(t *testing.T) { + if ns := NewCM().GetNamespace("my-ns"); ns != "my-ns" { + t.Errorf("GetNamespace failed: expected: my-ns, got: %s", ns) + } +} + +func TestGetNamespaceReturnsConfiguredNamespaceName(t *testing.T) { + if ns := NewCM().GetNamespace(""); ns != "ricxapp" { + t.Errorf("GetNamespace failed: expected: ricxapp, got: %s", ns) + } +} + func TestGetNamesFromHelmRepoSuccess(t *testing.T) { expectedResult := []string{"anr", "appmgr", "dualco", "reporter", "uemgr"} - util.HelmExec = func(args string) (out []byte, err error) { - return []byte(helmSearchOutput), nil - } + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + //Fake helm search success + helmExecRetOut = helmSearchOutput names := NewCM().GetNamesFromHelmRepo() if !reflect.DeepEqual(names, expectedResult) { t.Errorf("GetNamesFromHelmRepo failed: expected %v, got %v", expectedResult, names) } + if caughtHelmExecArgs != expectedHelmSearchCmd { + t.Errorf("GetNamesFromHelmRepo failed: expected: %v, got: %v", expectedHelmSearchCmd, caughtHelmExecArgs) + } } func TestGetNamesFromHelmRepoFailure(t *testing.T) { expectedResult := []string{} - util.HelmExec = func(args string) (out []byte, err error) { - return []byte(helmSearchOutput), errors.New("Command failed!") - } + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helmSearchOutput + helmExecRetErr = errors.New("Command failed!") names := NewCM().GetNamesFromHelmRepo() if names != nil { @@ -180,26 +389,128 @@ func TestGetNamesFromHelmRepoFailure(t *testing.T) { } func TestBuildConfigMapSuccess(t *testing.T) { + expectedKubeCmd := []string{ + `get configmap -o jsonpath='{.data.config-file\.json}' -n ricxapp configmap-ricxapp-dummy-xapp-appconfig`, + } name := "dummy-xapp" namespace := "ricxapp" m := models.ConfigMetadata{XappName: &name, Namespace: &namespace} - s := `{"Metadata": {"XappName": "ueec", "Namespace": "ricxapp"}, "Config": {"active": true, "interfaceId":{"globalENBId": {"eNBId": 77, "plmnId": "6666"}}}}` + s := `{"Metadata": {"XappName": "ueec", "Namespace": "ricxapp"}, ` + + `"Config": {"active": true, "interfaceId":{"globalENBId": {"eNBId": 77, "plmnId": "6666"}}}}` - util.KubectlExec = func(args string) (out []byte, err error) { - return []byte(`{"logger": {"level": 2}}`), nil - } + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' success + kubeExecRetOut = `{"logger": {"level": 2}}` cmString, err := NewCM().BuildConfigMap(models.XAppConfig{Metadata: &m, Config: s}) if err != nil { t.Errorf("BuildConfigMap failed: %v -> %v", err, cmString) } + if !reflect.DeepEqual(caughtKubeExecArgs, expectedKubeCmd) { + t.Errorf("BuildConfigMap failed: expected: %v, got: %v", expectedKubeCmd, caughtKubeExecArgs) + } } -func TestUpdateConfigMapFails(t *testing.T) { +func TestBuildConfigMapReturnErrorIfJsonMarshalFails(t *testing.T) { + name := "dummy-xapp" + namespace := "ricxapp" + m := models.ConfigMetadata{XappName: &name, Namespace: &namespace} + //Give channel as a configuration input, this will fail JSON marshal + cmString, err := NewCM().BuildConfigMap(models.XAppConfig{Metadata: &m, Config: make(chan int)}) + if err == nil { + t.Errorf("BuildConfigMap failed: %v -> %v", err, cmString) + } +} + +func TestBuildConfigMapReturnErrorIfKubectlGetConfigmapFails(t *testing.T) { + name := "dummy-xapp" + namespace := "ricxapp" + m := models.ConfigMetadata{XappName: &name, Namespace: &namespace} + s := `{"Metadata": {"XappName": "ueec", "Namespace": "ricxapp"}, ` + + `"Config": {"active": true, "interfaceId":{"globalENBId": {"eNBId": 77, "plmnId": "6666"}}}}` + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl get configmap' failure + kubeExecRetErr = errors.New("some error") + + cmString, err := NewCM().BuildConfigMap(models.XAppConfig{Metadata: &m, Config: s}) + if err == nil { + t.Errorf("BuildConfigMap failed: %v -> %v", err, cmString) + } else if err.Error() != "some error" { + t.Errorf("BuildConfigMap failed: expected: 'some error', got: '%s'", err.Error()) + } +} + +func TestBuildConfigMapReturnErrorIfJsonParserFails(t *testing.T) { + name := "dummy-xapp" + namespace := "ricxapp" + m := models.ConfigMetadata{XappName: &name, Namespace: &namespace} + s := `{"Metadata": {"XappName": "ueec", "Namespace": "ricxapp"}, ` + + `"Config": {"active": true, "interfaceId":{"globalENBId": {"eNBId": 77, "plmnId": "6666"}}}}` + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Return empty json that causes JSON parser to fail + kubeExecRetOut = `` + + cmString, err := NewCM().BuildConfigMap(models.XAppConfig{Metadata: &m, Config: s}) + if err == nil { + t.Errorf("BuildConfigMap failed: %v -> %v", err, cmString) + } +} + +func TestGenerateJSONFileSuccess(t *testing.T) { + err := NewCM().GenerateJSONFile("{}") + if err != nil { + t.Errorf("GenerateJSONFile failed: %v", err) + } +} + +func TestReplaceConfigMapSuccess(t *testing.T) { + name := "dummy-xapp" + namespace := "ricxapp" + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + //Fake 'kubectl create configmap' success + kubeExecRetOut = "" + + err := NewCM().ReplaceConfigMap(name, namespace) + if err != nil { + t.Errorf("ReplaceConfigMap failed: %v", err) + } +} + +func TestUpdateConfigMapReturnsErrorIfSchemaFileIsMissing(t *testing.T) { + name := "dummy-xapp" + namespace := "ricxapp" + config := models.XAppConfig{Metadata: &models.ConfigMetadata{XappName: &name, Namespace: &namespace}} + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = `{}` + + //Will fail at schema reading, because schema file is mission + validationErrors, err := NewCM().UpdateConfigMap(config) + if err == nil { + t.Errorf("UpdateConfigMap failed: %v -> %v", err, validationErrors) + } + if caughtHelmExecArgs != expectedHelmFetchCmd { + t.Errorf("UpdateConfigMap failed: expected: %v, got: %v", expectedHelmFetchCmd, caughtHelmExecArgs) + } +} + +func TestUpdateConfigMapReturnsErrorIfHelmFetchChartFails(t *testing.T) { name := "dummy-xapp" namespace := "ricxapp" config := models.XAppConfig{Metadata: &models.ConfigMetadata{XappName: &name, Namespace: &namespace}} + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some error") + validationErrors, err := NewCM().UpdateConfigMap(config) if err == nil { t.Errorf("UpdateConfigMap failed: %v -> %v", err, validationErrors) @@ -225,6 +536,7 @@ func TestValidationSuccess(t *testing.T) { func TestValidationFails(t *testing.T) { var d interface{} var cfg map[string]interface{} + err := json.Unmarshal([]byte(`{"active": "INVALID", "interfaceId":{"globalENBId": {"eNBId": 77, "plmnId": "6666"}}}`), &cfg) err = NewCM().ReadFile("../../test/schema.json", &d) @@ -234,7 +546,47 @@ func TestValidationFails(t *testing.T) { feedback, err := NewCM().doValidate(d, cfg) if err == nil { - t.Errorf("doValidate should faile but didn't: %v -> %v", err, feedback) + t.Errorf("doValidate should fail but didn't: %v -> %v", err, feedback) } appmgr.Logger.Debug("Feedbacks: %v", feedback) } + +func TestReadFileReturnsErrorIfFileReadFails(t *testing.T) { + var d interface{} + + if err := NewCM().ReadFile("not/existing/test/schema.json", &d); err == nil { + t.Errorf("ReadFile should fail but it didn't") + } +} + +func TestReadFileReturnsErrorIfJsonUnmarshalFails(t *testing.T) { + var d interface{} + + if err := NewCM().ReadFile("../../test/faulty_schema.json", &d); err == nil { + t.Errorf("ReadFile should fail but it didn't") + } +} + +func mockedKubeExec(args string) (out []byte, err error) { + caughtKubeExecArgs = append(caughtKubeExecArgs, args) + return []byte(kubeExecRetOut), kubeExecRetErr +} + +func resetKubeExecMock() { + kubeExec = util.KubectlExec + caughtKubeExecArgs = nil + kubeExecRetOut = "" + kubeExecRetErr = nil +} + +func mockedHelmExec(args string) (out []byte, err error) { + caughtHelmExecArgs = args + return []byte(helmExecRetOut), helmExecRetErr +} + +func resetHelmExecMock() { + helmExec = util.HelmExec + caughtHelmExecArgs = "" + helmExecRetOut = "" + helmExecRetErr = nil +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index ed00b9e..e777403 100755 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -36,6 +36,9 @@ import ( "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/util" ) +var kubeExec = util.KubectlExec +var helmExec = util.HelmExec + type Helm struct { initDone bool cm *cm.CM @@ -71,7 +74,7 @@ func (h *Helm) Initialize() { } func (h *Helm) Run(args string) (out []byte, err error) { - return util.HelmExec(args) + return helmExec(args) } // API functions @@ -80,7 +83,7 @@ func (h *Helm) Init() (out []byte, err error) { return out, err } - return util.HelmExec(strings.Join([]string{"init -c --skip-refresh"}, "")) + return helmExec(strings.Join([]string{"init -c --skip-refresh"}, "")) } func (h *Helm) AddRepo() (out []byte, err error) { @@ -100,7 +103,7 @@ func (h *Helm) AddRepo() (out []byte, err error) { repoArgs := fmt.Sprintf(" %s %s ", viper.GetString("helm.repo-name"), viper.GetString("helm.repo")) credentials := fmt.Sprintf(" --username %s --password %s", string(username), string(password)) - return util.HelmExec(strings.Join([]string{"repo add ", repoArgs, credentials}, "")) + return helmExec(strings.Join([]string{"repo add ", repoArgs, credentials}, "")) } func (h *Helm) Install(m models.XappDescriptor) (xapp models.Xapp, err error) { @@ -124,7 +127,6 @@ func (h *Helm) Status(name string) (xapp models.Xapp, err error) { appmgr.Logger.Info("Getting xapps status: %v", err.Error()) return } - return h.ParseStatus(name, string(out)) } @@ -218,7 +220,7 @@ func (h *Helm) GetEndpointInfo(name string) (svc string, port int) { port = 4560 // Default ns := h.cm.GetNamespace("") args := fmt.Sprintf(" get service -n %s service-%s-%s-rmr -o json", ns, ns, name) - out, err := util.KubectlExec(args) + out, err := kubeExec(args) if err != nil { return fmt.Sprintf("service-%s-%s-rmr.%s", ns, name, ns), 4560 } @@ -339,7 +341,7 @@ func (h *Helm) GetInstallArgs(x models.XappDescriptor, cmOverride bool) (args st } if cmOverride == true { - args = fmt.Sprintf("%s ---set ricapp.appconfig.override=%s-appconfig", args, *x.XappName) + args = fmt.Sprintf("%s --set ricapp.appconfig.override=%s-appconfig", args, *x.XappName) } if x.OverrideFile != nil { diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index 109103c..1bfdb22 100755 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -20,9 +20,12 @@ package helm import ( + "errors" + "github.com/spf13/viper" "os" "reflect" "strconv" + "strings" "testing" "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/appmgr" @@ -30,6 +33,12 @@ import ( "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/util" ) +var caughtKubeExecArgs string +var kubeExecRetOut string +var kubeExecRetErr error +var caughtHelmExecArgs string +var helmExecRetOut string +var helmExecRetErr error var helmStatusOutput = ` LAST DEPLOYED: Sat Mar 9 06:50:45 2019 NAMESPACE: default @@ -51,7 +60,7 @@ NAME READY UP-TO-DATE AVAILABLE AGE dummy-xapp 3/3 3 3 55m ` -var helListOutput = `Next: "" +var helListAllOutput = `Next: "" Releases: - AppVersion: "1.0" Chart: dummy-xapp-chart-0.1.0 @@ -76,7 +85,18 @@ Releases: Updated: Sun Mar 24 07:17:00 2019 ` -var helmServiceOutput = `{ +var helListOutput = `Next: "" +Releases: +- AppVersion: "1.0" + Chart: dummy-xapp-chart-0.1.0 + Name: dummy-xapp + Namespace: default + Revision: 1 + Status: DEPLOYED + Updated: Mon Mar 11 06:55:05 2019 + ` + +var kubeServiceOutput = `{ "apiVersion": "v1", "kind": "Service", "metadata": { @@ -130,44 +150,332 @@ func TestMain(m *testing.M) { os.Exit(code) } -func TestHelmStatus(t *testing.T) { - util.KubectlExec = func(args string) (out []byte, err error) { - return []byte(helmServiceOutput), nil +func TestInit(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + + NewHelm().Init() + + expectedHelmCommand := "init -c --skip-refresh" + if caughtHelmExecArgs != expectedHelmCommand { + t.Errorf("Init failed: expected %v, got %v", expectedHelmCommand, caughtHelmExecArgs) + } +} + +func TestAddRepoSuccess(t *testing.T) { + defer func() { + resetHelmExecMock() + removeTestUsernameFile() + removeTestPasswordFile() + }() + helmExec = mockedHelmExec + + if err := writeTestUsernameFile(); err != nil { + t.Errorf("AddRepo username file create failed: %s", err) + return + } + if err := writeTestPasswordFile(); err != nil { + t.Errorf("AddRepo password file create failed: %s", err) + return + } + + if _, err := NewHelm().AddRepo(); err != nil { + t.Errorf("AddRepo failed: %v", err) + } + + if !strings.Contains(caughtHelmExecArgs, "repo add") { + t.Errorf("AddRepo failed: expected %v, got %v", "repo add", caughtHelmExecArgs) + } +} + +func TestAddRepoReturnsErrorIfNoUsernameFile(t *testing.T) { + if _, err := NewHelm().AddRepo(); err == nil { + t.Errorf("AddRepo expected to fail but it didn't") } +} + +func TestAddRepoReturnsErrorIfNoPasswordFile(t *testing.T) { + defer func() { resetHelmExecMock(); removeTestUsernameFile() }() + helmExec = mockedHelmExec + + if err := writeTestUsernameFile(); err != nil { + t.Errorf("AddRepo username file create failed: %s", err) + return + } + if _, err := NewHelm().AddRepo(); err == nil { + t.Errorf("AddRepo expected to fail but it didn't") + } +} + +func TestInstallSuccess(t *testing.T) { + name := "dummy-xapp" + xappDesc := models.XappDescriptor{XappName: &name, Namespace: "ricxapp"} + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helmStatusOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + kubeExecRetOut = kubeServiceOutput + + xapp, err := NewHelm().Install(xappDesc) + if err != nil { + t.Errorf("Install failed: %v", err) + } + validateXappModel(t, xapp) +} + +func TestInstallReturnsErrorIfHelmRepoUpdateFails(t *testing.T) { + name := "dummy-xapp" + xappDesc := models.XappDescriptor{XappName: &name, Namespace: "ricxapp"} + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some helm command error") + + if _, err := NewHelm().Install(xappDesc); err == nil { + t.Errorf("Install expected to fail but it didn't") + } +} + +func TestStatusSuccess(t *testing.T) { + name := "dummy-xapp" + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helmStatusOutput + + xapp, err := NewHelm().Status(name) + if err != nil { + t.Errorf("Status failed: %v", err) + } + validateXappModel(t, xapp) +} + +func TestStatusReturnsErrorIfHelmStatusFails(t *testing.T) { + name := "dummy-xapp" + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some helm command error") + + if _, err := NewHelm().Status(name); err == nil { + t.Errorf("Status expected to fail but it didn't") + } +} + +func TestParseStatusSuccess(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helListOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + kubeExecRetOut = kubeServiceOutput + xapp, err := NewHelm().ParseStatus("dummy-xapp", helmStatusOutput) if err != nil { - t.Errorf("Helm install failed: %v", err) + t.Errorf("ParseStatus failed: %v", err) } - x := getXappData() - xapp.Version = "1.0" - if *x.Name != *xapp.Name || x.Status != xapp.Status || x.Version != xapp.Version { - t.Errorf("\n%v \n%v", *xapp.Name, *x.Name) + validateXappModel(t, xapp) + + expectedHelmCommand := "list --deployed --output yaml --namespace=ricxapp dummy-xapp" + if caughtHelmExecArgs != expectedHelmCommand { + t.Errorf("ParseStatus failed: expected %v, got %v", expectedHelmCommand, caughtHelmExecArgs) + } +} + +func TestListSuccess(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helListAllOutput + + names, err := NewHelm().List() + if err != nil { + t.Errorf("List failed: %v", err) } - if *x.Instances[0].Name != *xapp.Instances[0].Name || x.Instances[0].Status != xapp.Instances[0].Status { - t.Errorf("\n1:%v 2:%v", *x.Instances[0].Name, *xapp.Instances[0].Name) + if !reflect.DeepEqual(names, []string{"dummy-xapp", "dummy-xapp2"}) { + t.Errorf("List failed: %v", err) + } + expectedHelmCommand := "list --all --deployed --output yaml --namespace=ricxapp" + if caughtHelmExecArgs != expectedHelmCommand { + t.Errorf("List: expected %v, got %v", expectedHelmCommand, caughtHelmExecArgs) } +} + +func TestListReturnsErrorIfHelmListFails(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some helm command error") - if x.Instances[0].IP != xapp.Instances[0].IP || x.Instances[0].Port != xapp.Instances[0].Port { - t.Errorf("\n%v - %v, %v - %v", x.Instances[0].IP, xapp.Instances[0].IP, x.Instances[0].Port, xapp.Instances[0].Port) + if _, err := NewHelm().List(); err == nil { + t.Errorf("List expected to fail but it didn't") } } -func TestHelmLists(t *testing.T) { - names, err := NewHelm().GetNames(helListOutput) +func TestDeleteSuccess(t *testing.T) { + name := "dummy-xapp" + + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helmStatusOutput + + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + kubeExecRetOut = kubeServiceOutput + + xapp, err := NewHelm().Delete(name) if err != nil { - t.Errorf("Helm status failed: %v", err) + t.Errorf("Delete failed: %v", err) + } + + validateXappModel(t, xapp) + + expectedHelmCommand := "del --purge dummy-xapp" + if caughtHelmExecArgs != expectedHelmCommand { + t.Errorf("Delete failed: expected %v, got %v", expectedHelmCommand, caughtHelmExecArgs) } +} + +func TestDeleteReturnsErrorIfHelmStatusFails(t *testing.T) { + name := "dummy-xapp" + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some helm command error") + + if _, err := NewHelm().Delete(name); err == nil { + t.Errorf("Delete expected to fail but it didn't") + } +} + +func TestFetchSuccessIfCmdArgHasTestSuffix(t *testing.T) { + if err := NewHelm().Fetch("kissa", "koira"); err != nil { + t.Errorf("Fetch failed: %v", err) + } +} + +func TestGetVersionSuccess(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helListOutput + + if version := NewHelm().GetVersion("dummy-xapp"); version != "1.0" { + t.Errorf("GetVersion failed: expected 1.0, got %v", version) + } + + expectedHelmCommand := "list --deployed --output yaml --namespace=ricxapp dummy-xapp" + if caughtHelmExecArgs != expectedHelmCommand { + t.Errorf("GetVersion failed: expected %v, got %v", expectedHelmCommand, caughtHelmExecArgs) + } +} + +func TestGetVersionReturnsEmptyStringIfHelmListFails(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some helm command error") + + if version := NewHelm().GetVersion("dummy-xapp"); version != "" { + t.Errorf("GetVersion expected to return empty string, got %v", version) + } +} + +func TestGetAddressSuccess(t *testing.T) { + ip, port := NewHelm().GetAddress(helmStatusOutput) + if ip != "10.102.184.212" { + t.Errorf("GetAddress failed: expected 10.102.184.212, got %v", ip) + } + if port != "80/TCP" { + t.Errorf("GetAddress failed: expected 80/TCP, got %v", port) + } +} + +func TestGetEndpointInfoSuccess(t *testing.T) { + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + kubeExecRetOut = kubeServiceOutput + + svc, port := NewHelm().GetEndpointInfo("dummy-xapp") + expectedSvc := "service-ricxapp-dummy-xapp-rmr.ricxapp" + if svc != expectedSvc { + t.Errorf("GetEndpointInfo failed: expected %v, got %v", expectedSvc, svc) + } + if port != 4560 { + t.Errorf("GetEndpointInfo failed: expected port 4560, got %v", port) + } + expectedKubeCommand := " get service -n ricxapp service-ricxapp-dummy-xapp-rmr -o json" + if caughtKubeExecArgs != expectedKubeCommand { + t.Errorf("GetEndpointInfo failed: expected %v, got %v", expectedKubeCommand, caughtKubeExecArgs) + } +} + +func TestGetEndpointInfoReturnsDefaultPortIfJsonParseFails(t *testing.T) { + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + kubeExecRetOut = "not-json-syntax" + + svc, port := NewHelm().GetEndpointInfo("dummy-xapp") + expectedSvc := "service-ricxapp-dummy-xapp-rmr.ricxapp" + if svc != expectedSvc { + t.Errorf("GetEndpointInfo failed: expected %v, got %v", expectedSvc, svc) + } + if port != 4560 { + t.Errorf("GetEndpointInfo failed: expected port 4560, got %v", port) + } +} + +func TestGetEndpointInfoReturnsDefaultPortIfKubeGetServiceFails(t *testing.T) { + defer func() { resetKubeExecMock() }() + kubeExec = mockedKubeExec + kubeExecRetErr = errors.New("some helm command error") + + svc, port := NewHelm().GetEndpointInfo("dummy-xapp") + expectedSvc := "service-ricxapp-dummy-xapp-rmr.ricxapp" + if svc != expectedSvc { + t.Errorf("GetEndpointInfo failed: expected %v, got %v", expectedSvc, svc) + } + if port != 4560 { + t.Errorf("GetEndpointInfo failed: expected port 4560, got %v", port) + } +} + +func TestHelmStatusAllSuccess(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetOut = helListAllOutput + + if _, err := NewHelm().StatusAll(); err != nil { + t.Errorf("StatusAll failed: %v", err) + } + // Todo: check StatusAll response content +} + +func TestStatusAllReturnsErrorIfHelmListFails(t *testing.T) { + defer func() { resetHelmExecMock() }() + helmExec = mockedHelmExec + helmExecRetErr = errors.New("some helm command error") + + if _, err := NewHelm().StatusAll(); err == nil { + t.Errorf("StatusAll expected to fail but it didn't") + } +} + +func TestGetNamesSuccess(t *testing.T) { + names, err := NewHelm().GetNames(helListAllOutput) + if err != nil { + t.Errorf("GetNames failed: %v", err) + } if !reflect.DeepEqual(names, []string{"dummy-xapp", "dummy-xapp2"}) { - t.Errorf("Helm status failed: %v", err) + t.Errorf("GetNames failed: %v", err) } } func TestAddTillerEnv(t *testing.T) { if NewHelm().AddTillerEnv() != nil { - t.Errorf("TestAddTillerEnv failed!") + t.Errorf("AddTillerEnv failed!") } } @@ -177,22 +485,62 @@ func TestGetInstallArgs(t *testing.T) { expectedArgs := "install helm-repo/dummy-xapp --namespace=ricxapp --name=dummy-xapp" if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { - t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) + t.Errorf("GetInstallArgs failed: expected %v, got %v", expectedArgs, args) + } + + expectedArgs += " --set ricapp.appconfig.override=dummy-xapp-appconfig" + if args := NewHelm().GetInstallArgs(x, true); args != expectedArgs { + t.Errorf("GetInstallArgs failed: expected %v, got %v", expectedArgs, args) } x.HelmVersion = "1.2.3" expectedArgs = "install helm-repo/dummy-xapp --namespace=ricxapp --version=1.2.3 --name=dummy-xapp" if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { - t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) + t.Errorf("GetInstallArgs failed: expected %v, got %v", expectedArgs, args) } x.ReleaseName = "ueec-xapp" expectedArgs = "install helm-repo/dummy-xapp --namespace=ricxapp --version=1.2.3 --name=ueec-xapp" if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { - t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) + t.Errorf("GetInstallArgs failed: expected %v, got %v", expectedArgs, args) + } + + x.OverrideFile = "../../test/dummy-xapp_values.json" + expectedArgs += " -f=/tmp/appmgr_override.yaml" + if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { + t.Errorf("GetInstallArgs failed: expected %v, got %v", expectedArgs, args) } } +func writeTestUsernameFile() error { + f, err := os.Create(viper.GetString("helm.helm-username-file")) + if err != nil { + return err + } + _, err = f.WriteString("some-username") + f.Close() + return err +} + +func removeTestUsernameFile() error { + return os.Remove(viper.GetString("helm.helm-username-file")) +} + +func writeTestPasswordFile() (err error) { + f, err := os.Create(viper.GetString("helm.helm-password-file")) + if err != nil { + return err + } + + _, err = f.WriteString("some-password") + f.Close() + return err +} + +func removeTestPasswordFile() error { + return os.Remove(viper.GetString("helm.helm-password-file")) +} + func getXappData() (x models.Xapp) { //name1 := "dummy-xapp-8984fc9fd-l6xch" //name2 := "dummy-xapp-8984fc9fd-pp4hg" @@ -224,3 +572,44 @@ func generateXapp(name, status, ver, iname, istatus, ip, port string) (x models. return } + +func mockedKubeExec(args string) (out []byte, err error) { + caughtKubeExecArgs = args + return []byte(kubeExecRetOut), kubeExecRetErr +} + +func resetKubeExecMock() { + kubeExec = util.KubectlExec + caughtKubeExecArgs = "" + kubeExecRetOut = "" + kubeExecRetErr = nil +} + +func mockedHelmExec(args string) (out []byte, err error) { + caughtHelmExecArgs = args + return []byte(helmExecRetOut), helmExecRetErr +} + +func resetHelmExecMock() { + helmExec = util.HelmExec + caughtHelmExecArgs = "" + helmExecRetOut = "" + helmExecRetErr = nil +} + +func validateXappModel(t *testing.T, xapp models.Xapp) { + expXapp := getXappData() + xapp.Version = "1.0" + + if *expXapp.Name != *xapp.Name || expXapp.Status != xapp.Status || expXapp.Version != xapp.Version { + t.Errorf("\n%v \n%v", *xapp.Name, *expXapp.Name) + } + + if *expXapp.Instances[0].Name != *xapp.Instances[0].Name || expXapp.Instances[0].Status != xapp.Instances[0].Status { + t.Errorf("\n1:%v 2:%v", *expXapp.Instances[0].Name, *xapp.Instances[0].Name) + } + + if expXapp.Instances[0].IP != xapp.Instances[0].IP || expXapp.Instances[0].Port != xapp.Instances[0].Port { + t.Errorf("\n%v - %v, %v - %v", expXapp.Instances[0].IP, xapp.Instances[0].IP, expXapp.Instances[0].Port, xapp.Instances[0].Port) + } +} diff --git a/pkg/resthooks/resthooks.go b/pkg/resthooks/resthooks.go index dea70c9..9ac49f9 100755 --- a/pkg/resthooks/resthooks.go +++ b/pkg/resthooks/resthooks.go @@ -33,9 +33,13 @@ import ( ) func NewResthook(restoreData bool) *Resthook { + return createResthook(restoreData, sdl.NewSdlInstance("appmgr", sdl.NewDatabase())) +} + +func createResthook(restoreData bool, sdlInst iSdl) *Resthook { rh := &Resthook{ client: &http.Client{}, - db: sdl.NewSdlInstance("appmgr", sdl.NewDatabase()), + db: sdlInst, } if restoreData { @@ -176,7 +180,6 @@ func (rh *Resthook) retry(s SubscriptionInfo, fn func() error) error { func (rh *Resthook) StoreSubscriptions(m cmap.ConcurrentMap) { for v := range m.Iter() { s := v.Val.(SubscriptionInfo) - data, err := json.Marshal(s.req) if err != nil { appmgr.Logger.Error("json.marshal failed: %v ", err.Error()) diff --git a/pkg/resthooks/resthooks_test.go b/pkg/resthooks/resthooks_test.go index d1ac790..e89383f 100755 --- a/pkg/resthooks/resthooks_test.go +++ b/pkg/resthooks/resthooks_test.go @@ -20,8 +20,18 @@ package resthooks import ( + "encoding/json" + "errors" + "fmt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "net" + "net/http" + "net/http/httptest" "os" + "time" + + "strconv" "testing" "gerrit.o-ran-sc.org/r/ric-plt/appmgr/pkg/appmgr" @@ -30,31 +40,40 @@ import ( var rh *Resthook var resp models.SubscriptionResponse +var mockedSdl *SdlMock // Test cases func TestMain(m *testing.M) { appmgr.Init() appmgr.Logger.SetLevel(0) - rh = NewResthook(false) + mockedSdl = new(SdlMock) + rh = createResthook(false, mockedSdl) code := m.Run() os.Exit(code) } func TestAddSubscriptionSuccess(t *testing.T) { - resp := rh.AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) + var mockSdlRetOk error + subsReq := createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + + mockedSdl.expectDbSet(t, subsReq, mockSdlRetOk) + resp := rh.AddSubscription(subsReq) assert.Equal(t, resp.Version, int64(0)) assert.Equal(t, resp.EventType, models.EventTypeCreated) } func TestAddSubscriptionExists(t *testing.T) { - resp := rh.AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) + resp := rh.AddSubscription(createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) assert.Equal(t, resp.Version, int64(0)) assert.Equal(t, resp.EventType, models.EventTypeCreated) } func TestDeletesubscriptionSuccess(t *testing.T) { - resp := rh.AddSubscription(CreateSubscription(models.EventTypeDeleted, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + var mockSdlRetOk error + + mockedSdl.On("Set", mock.Anything).Return(mockSdlRetOk) + resp := rh.AddSubscription(createSubscription(models.EventTypeDeleted, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) assert.Equal(t, resp.Version, int64(0)) assert.Equal(t, resp.EventType, models.EventTypeDeleted) @@ -72,17 +91,23 @@ func TestDeletesubscriptionInvalid(t *testing.T) { } func TestModifySubscriptionSuccess(t *testing.T) { - resp := rh.AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + resp := rh.AddSubscription(createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) assert.Equal(t, resp.Version, int64(0)) assert.Equal(t, resp.EventType, models.EventTypeCreated) - resp, ok := rh.ModifySubscription(resp.ID, CreateSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + resp, ok := rh.ModifySubscription(resp.ID, createSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) assert.Equal(t, ok, true) assert.Equal(t, resp.Version, int64(0)) assert.Equal(t, resp.EventType, models.EventTypeModified) } -func TestModifysubscriptionInvalid(t *testing.T) { +func TestModifySubscriptionForNonExistingSubscription(t *testing.T) { + resp, ok := rh.ModifySubscription("Non-existent-ID", createSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + assert.Equal(t, ok, false) + assert.Equal(t, *resp, models.SubscriptionResponse{}) +} + +func TestDeleteSubscriptionForNonExistingSubscription(t *testing.T) { resp, ok := rh.DeleteSubscription("Non-existent-ID") assert.Equal(t, ok, false) assert.Equal(t, resp.Version, int64(0)) @@ -90,21 +115,22 @@ func TestModifysubscriptionInvalid(t *testing.T) { } func TestGetAllSubscriptionSuccess(t *testing.T) { - rh.FlushSubscriptions() + flushExistingSubscriptions() subscriptions := rh.GetAllSubscriptions() assert.Equal(t, len(subscriptions), 0) - rh.AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) - rh.AddSubscription(CreateSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + rh.AddSubscription(createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) + rh.AddSubscription(createSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) subscriptions = rh.GetAllSubscriptions() assert.Equal(t, len(subscriptions), 2) } func TestGetSubscriptionByIdSuccess(t *testing.T) { - rh.FlushSubscriptions() - sub1 := CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") - sub2 := CreateSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2") + flushExistingSubscriptions() + + sub1 := createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + sub2 := createSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2") r1 := rh.AddSubscription(sub1) r2 := rh.AddSubscription(sub2) @@ -117,10 +143,212 @@ func TestGetSubscriptionByIdSuccess(t *testing.T) { assert.Equal(t, resp2.Data, sub2.Data) } +func TestGetSubscriptionByIdForNonExistingSubscription(t *testing.T) { + resp, ok := rh.GetSubscriptionById("Non-existent-ID") + assert.Equal(t, ok, false) + assert.Equal(t, resp, models.Subscription{}) +} + +func TestNotifyClientsNoXapp(t *testing.T) { + rh.NotifyClients(models.AllDeployedXapps{}, models.EventTypeUndeployed) +} + +func TestNotifySuccess(t *testing.T) { + flushExistingSubscriptions() + + sub := createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + resp := rh.AddSubscription(sub) + + xapp := getDummyXapp() + ts := createHTTPServer(t, "POST", "/xapps_hook", 8087, http.StatusOK, nil) + defer ts.Close() + + v, ok := rh.subscriptions.Get(resp.ID) + assert.True(t, ok) + err := rh.notify(models.AllDeployedXapps{&xapp}, models.EventTypeUndeployed, v.(SubscriptionInfo), 1) + assert.Nil(t, err) +} + +func TestNotifySuccessIfHttpErrorResponse(t *testing.T) { + flushExistingSubscriptions() + + sub := createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + resp := rh.AddSubscription(sub) + + xapp := getDummyXapp() + ts := createHTTPServer(t, "POST", "/xapps_hook", 8087, http.StatusInternalServerError, nil) + defer ts.Close() + + v, ok := rh.subscriptions.Get(resp.ID) + assert.True(t, ok) + err := rh.notify(models.AllDeployedXapps{&xapp}, models.EventTypeUndeployed, v.(SubscriptionInfo), 1) + assert.Nil(t, err) +} + +func TestNotifyReturnsErrorAfterRetriesIfNoHttpServer(t *testing.T) { + flushExistingSubscriptions() + + sub := createSubscription(models.EventTypeCreated, int64(2), int64(1), "http://localhost:8087/xapps_hook") + resp := rh.AddSubscription(sub) + + xapp := getDummyXapp() + + v, ok := rh.subscriptions.Get(resp.ID) + assert.True(t, ok) + err := rh.notify(models.AllDeployedXapps{&xapp}, models.EventTypeUndeployed, v.(SubscriptionInfo), 1) + assert.NotNil(t, err) + assert.Equal(t, 0, len(rh.subscriptions.Items())) +} + +func TestRestoreSubscriptionsSuccess(t *testing.T) { + var mockSdlRetOk error + mSdl := new(SdlMock) + key := "key-1" + + subsReq := createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + serializedSubsReq, err := json.Marshal(subsReq) + assert.Nil(t, err) + + mockSdlGetRetVal := make(map[string]interface{}) + //Cast data to string to act like a real SDL/Redis client + mockSdlGetRetVal[key] = string(serializedSubsReq) + mSdl.On("GetAll").Return([]string{key}, mockSdlRetOk).Twice() + mSdl.On("Get", []string{key}).Return(mockSdlGetRetVal, mockSdlRetOk).Once() + restHook := createResthook(true, mSdl) + + val, found := restHook.subscriptions.Get(key) + assert.True(t, found) + assert.Equal(t, subsReq, val.(SubscriptionInfo).req) +} + +func TestRestoreSubscriptionsFailsIfSdlGetAllFails(t *testing.T) { + var mockSdlRetStatus error + mSdl := new(SdlMock) + getCalled := 0 + mGetAllCall := mSdl.On("GetAll") + mGetAllCall.RunFn = func(args mock.Arguments) { + if getCalled > 0 { + mockSdlRetStatus = errors.New("some SDL error") + } + getCalled++ + mGetAllCall.ReturnArguments = mock.Arguments{[]string{}, mockSdlRetStatus} + } + + restHook := createResthook(true, mSdl) + assert.Equal(t, 0, len(restHook.subscriptions.Items())) +} + +func TestRestoreSubscriptionsFailsIfSdlGetFails(t *testing.T) { + var mockSdlRetOk error + mSdl := new(SdlMock) + mockSdlRetNok := errors.New("some SDL error") + key := "key-1" + subsReq := createSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + serializedSubsReq, err := json.Marshal(subsReq) + assert.Nil(t, err) + + mockSdlGetRetVal := make(map[string]interface{}) + mockSdlGetRetVal[key] = serializedSubsReq + + mSdl.On("GetAll").Return([]string{key}, mockSdlRetOk).Twice() + mSdl.On("Get", []string{key}).Return(mockSdlGetRetVal, mockSdlRetNok).Once() + + restHook := createResthook(true, mSdl) + assert.Equal(t, 0, len(restHook.subscriptions.Items())) +} + func TestTeardown(t *testing.T) { + var mockSdlRetOk error + mockedSdl.On("RemoveAll").Return(mockSdlRetOk).Once() + rh.FlushSubscriptions() } -func CreateSubscription(et models.EventType, maxRetries, retryTimer int64, targetUrl string) models.SubscriptionRequest { +func createSubscription(et models.EventType, maxRetries, retryTimer int64, targetUrl string) models.SubscriptionRequest { return models.SubscriptionRequest{&models.SubscriptionData{et, &maxRetries, &retryTimer, &targetUrl}} } + +func getDummyXapp() models.Xapp { + return generateXapp("dummy-xapp", "deployed", "1.0", "dummy-xapp-8984fc9fd-bkcbp", "running", "service-ricxapp-dummy-xapp-rmr.ricxapp", "4560") +} + +func generateXapp(name, status, ver, iname, istatus, ip, port string) (x models.Xapp) { + x.Name = &name + x.Status = status + x.Version = ver + p, _ := strconv.Atoi(port) + var msgs appmgr.RtmData + + instance := &models.XappInstance{ + Name: &iname, + Status: istatus, + IP: ip, + Port: int64(p), + TxMessages: msgs.TxMessages, + RxMessages: msgs.RxMessages, + } + x.Instances = append(x.Instances, instance) + return +} + +func flushExistingSubscriptions() { + var mockSdlRetOk error + mockedSdl.On("RemoveAll").Return(mockSdlRetOk).Once() + rh.FlushSubscriptions() +} + +func createHTTPServer(t *testing.T, method, url string, port, status int, respData interface{}) *httptest.Server { + l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + t.Error("Failed to create listener: " + err.Error()) + } + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, method) + assert.Equal(t, r.URL.String(), url) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(status) + b, _ := json.Marshal(respData) + w.Write(b) + })) + ts.Listener.Close() + ts.Listener = l + + ts.Start() + time.Sleep(time.Duration(1 * time.Second)) + return ts +} + +func (m *SdlMock) expectDbSet(t *testing.T, subsReq models.SubscriptionRequest, mockRet error) { + serializedSubReq, _ := json.Marshal(subsReq) + m.On("Set", mock.Anything).Run( + func(args mock.Arguments) { + sdlKVs := args.Get(0).([]interface{}) + assert.Equal(t, 2, len(sdlKVs)) + //Validate that subscription request is set to SDL + assert.Equal(t, serializedSubReq, sdlKVs[1]) + }).Return(mockRet).Once() +} + +type SdlMock struct { + mock.Mock +} + +func (m *SdlMock) Set(pairs ...interface{}) error { + a := m.Called(pairs) + return a.Error(0) +} + +func (m *SdlMock) Get(keys []string) (map[string]interface{}, error) { + a := m.Called(keys) + return a.Get(0).(map[string]interface{}), a.Error(1) +} + +func (m *SdlMock) GetAll() ([]string, error) { + a := m.Called() + return a.Get(0).([]string), a.Error(1) +} + +func (m *SdlMock) RemoveAll() error { + a := m.Called() + return a.Error(0) +} diff --git a/pkg/resthooks/types.go b/pkg/resthooks/types.go index 2229abe..f2af972 100755 --- a/pkg/resthooks/types.go +++ b/pkg/resthooks/types.go @@ -20,7 +20,6 @@ package resthooks import ( - sdl "gerrit.o-ran-sc.org/r/ric-plt/sdlgo" cmap "github.com/orcaman/concurrent-map" "net/http" @@ -36,7 +35,7 @@ type SubscriptionInfo struct { type Resthook struct { client *http.Client subscriptions cmap.ConcurrentMap - db *sdl.SdlInstance + db iSdl Seq int64 } @@ -47,3 +46,10 @@ type SubscriptionNotification struct { Version int64 `json:"version,omitempty"` XApps string `json:"xApps,omitempty"` } + +type iSdl interface { + Set(pairs ...interface{}) error + Get(keys []string) (map[string]interface{}, error) + GetAll() ([]string, error) + RemoveAll() error +} diff --git a/test/dummy-xapp_values.json b/test/dummy-xapp_values.json new file mode 100644 index 0000000..7687f63 --- /dev/null +++ b/test/dummy-xapp_values.json @@ -0,0 +1,6 @@ +{ + "XappName": "dummy-xapp", + "ReleaseName": "dummy-xapp", + "HelmVersion": "1.0.0", + "Namespace": "ricxapp" +} diff --git a/test/faulty_schema.json b/test/faulty_schema.json new file mode 100644 index 0000000..e69de29 -- 2.16.6