// - // ========================LICENSE_START================================= // O-RAN-SC // %% // Copyright (C) 2021: Nordix Foundation // %% // 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. // ========================LICENSE_END=================================== // package main import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "sync" "syscall" "testing" "time" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "oransc.org/usecase/oruclosedloop/internal/config" "oransc.org/usecase/oruclosedloop/internal/linkfailure" repomock "oransc.org/usecase/oruclosedloop/internal/repository/mocks" "oransc.org/usecase/oruclosedloop/internal/restclient/mocks" ) func Test_init(t *testing.T) { assertions := require.New(t) os.Setenv("CONSUMER_HOST", "consumerHost") os.Setenv("CONSUMER_PORT", "8095") t.Cleanup(func() { os.Clearenv() }) doInit() wantedConfiguration := &config.Config{ ConsumerHost: "consumerHost", ConsumerPort: 8095, InfoCoordinatorAddress: "http://enrichmentservice:8083", SDNRAddress: "http://localhost:3904", SDNRUser: "admin", SDNPassword: "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U", ORUToODUMapFile: "o-ru-to-o-du-map.csv", ConsumerCertPath: "security/consumer.crt", ConsumerKeyPath: "security/consumer.key", LogLevel: log.InfoLevel, } assertions.Equal(wantedConfiguration, configuration) assertions.Equal(fmt.Sprint(wantedConfiguration.ConsumerPort), consumerPort) assertions.Equal(wantedConfiguration.ConsumerHost+":"+fmt.Sprint(wantedConfiguration.ConsumerPort), jobRegistrationInfo.JobResultURI) wantedLinkFailureConfig := linkfailure.Configuration{ SDNRAddress: wantedConfiguration.SDNRAddress, SDNRUser: wantedConfiguration.SDNRUser, SDNRPassword: wantedConfiguration.SDNPassword, } assertions.Equal(wantedLinkFailureConfig, linkfailureConfig) } func Test_validateConfiguration(t *testing.T) { assertions := require.New(t) type args struct { configuration *config.Config } tests := []struct { name string args args wantErr error }{ { name: "Valid config, should return nil", args: args{ configuration: &config.Config{ ConsumerHost: "host", ConsumerPort: 80, ConsumerCertPath: "security/consumer.crt", ConsumerKeyPath: "security/consumer.key", }, }, }, { name: "Invalid config, should return error", args: args{ configuration: &config.Config{}, }, wantErr: fmt.Errorf("consumer host and port must be provided"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateConfiguration(tt.args.configuration) assertions.Equal(tt.wantErr, err) }) } } func Test_initializeLookupService(t *testing.T) { assertions := require.New(t) type args struct { csvFile string oRuId string mockReturn [][]string mockReturnError error } tests := []struct { name string args args wantODuId string wantInitErr error }{ { name: "Successful initialization, should return nil and lookup service should be initiated with data", args: args{ csvFile: "file", oRuId: "1", mockReturn: [][]string{{"1", "2"}}, }, wantODuId: "2", }, { name: "Unsuccessful initialization, should return error and lookup service should not be initiated with data", args: args{ csvFile: "file", mockReturnError: errors.New("Error"), }, wantInitErr: errors.New("Error"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockCsvFileHelper := &repomock.CsvFileHelper{} mockCsvFileHelper.On("GetCsvFromFile", mock.Anything).Return(tt.args.mockReturn, tt.args.mockReturnError) err := initializeLookupService(mockCsvFileHelper, tt.args.csvFile) oDuId, _ := lookupService.GetODuID(tt.args.oRuId) assertions.Equal(tt.wantODuId, oDuId) assertions.Equal(tt.wantInitErr, err) mockCsvFileHelper.AssertCalled(t, "GetCsvFromFile", tt.args.csvFile) }) } } func Test_getRouter_shouldContainAllPathsWithHandlers(t *testing.T) { assertions := require.New(t) r := getRouter() messageHandlerRoute := r.Get("messageHandler") assertions.NotNil(messageHandlerRoute) supportedMethods, err := messageHandlerRoute.GetMethods() assertions.Equal([]string{http.MethodPost}, supportedMethods) assertions.Nil(err) path, _ := messageHandlerRoute.GetPathTemplate() assertions.Equal("/", path) startHandlerRoute := r.Get("start") assertions.NotNil(messageHandlerRoute) supportedMethods, err = startHandlerRoute.GetMethods() assertions.Equal([]string{http.MethodPost}, supportedMethods) assertions.Nil(err) path, _ = startHandlerRoute.GetPathTemplate() assertions.Equal("/admin/start", path) stopHandlerRoute := r.Get("stop") assertions.NotNil(stopHandlerRoute) supportedMethods, err = stopHandlerRoute.GetMethods() assertions.Equal([]string{http.MethodPost}, supportedMethods) assertions.Nil(err) path, _ = stopHandlerRoute.GetPathTemplate() assertions.Equal("/admin/stop", path) statusHandlerRoute := r.Get("status") assertions.NotNil(statusHandlerRoute) supportedMethods, err = statusHandlerRoute.GetMethods() assertions.Equal([]string{http.MethodGet}, supportedMethods) assertions.Nil(err) path, _ = statusHandlerRoute.GetPathTemplate() assertions.Equal("/status", path) } func Test_startHandler(t *testing.T) { assertions := require.New(t) jobRegistrationInfo.JobResultURI = "host:80" var job_definition interface{} type args struct { mockReturnBody []byte mockReturnStatus int } tests := []struct { name string args args wantedStatus int wantedBody string }{ { name: "Start with successful registration, should return ok", args: args{ mockReturnBody: []byte(""), mockReturnStatus: http.StatusOK, }, wantedStatus: http.StatusOK, }, { name: "Start with error response at registration, should return error", args: args{ mockReturnBody: []byte("error"), mockReturnStatus: http.StatusBadRequest, }, wantedStatus: http.StatusBadRequest, wantedBody: "Unable to register consumer job due to:", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clientMock := setUpClientMock(tt.args.mockReturnBody, tt.args.mockReturnStatus) handler := http.HandlerFunc(startHandler) responseRecorder := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodPost, "/start", nil) handler.ServeHTTP(responseRecorder, r) assertions.Equal(tt.wantedStatus, responseRecorder.Code, tt.name) assertions.Contains(responseRecorder.Body.String(), tt.wantedBody, tt.name) var wantedJobRegistrationInfo = struct { InfoTypeId string `json:"info_type_id"` JobResultUri string `json:"job_result_uri"` JobOwner string `json:"job_owner"` JobDefinition interface{} `json:"job_definition"` }{ InfoTypeId: "STD_Fault_Messages", JobResultUri: "host:80", JobOwner: "O-RU Closed Loop Usecase", JobDefinition: job_definition, } wantedBody, _ := json.Marshal(wantedJobRegistrationInfo) var actualRequest *http.Request clientMock.AssertCalled(t, "Do", mock.MatchedBy(func(req *http.Request) bool { actualRequest = req return true })) assertions.Equal(http.MethodPut, actualRequest.Method) assertions.Equal("http", actualRequest.URL.Scheme) assertions.Equal("enrichmentservice:8083", actualRequest.URL.Host) assertions.Equal("/data-consumer/v1/info-jobs/14e7bb84-a44d-44c1-90b7-6995a92ad43c", actualRequest.URL.Path) assertions.Equal("application/json; charset=utf-8", actualRequest.Header.Get("Content-Type")) body, _ := ioutil.ReadAll(actualRequest.Body) expectedBody := wantedBody assertions.Equal(expectedBody, body) clientMock.AssertNumberOfCalls(t, "Do", 1) // Check that the running status is "started" statusHandler := http.HandlerFunc(statusHandler) statusResponseRecorder := httptest.NewRecorder() statusRequest, _ := http.NewRequest(http.MethodGet, "/status", nil) statusHandler.ServeHTTP(statusResponseRecorder, statusRequest) assertions.Equal(http.StatusOK, statusResponseRecorder.Code) assertions.Equal(`{"status": "started"}`, statusResponseRecorder.Body.String()) }) } } func Test_stopHandler(t *testing.T) { assertions := require.New(t) jobRegistrationInfo.JobResultURI = "host:80" type args struct { mockReturnBody []byte mockReturnStatus int } tests := []struct { name string args args wantedStatus int wantedBody string }{ { name: "Stop with successful job deletion, should return ok", args: args{ mockReturnBody: []byte(""), mockReturnStatus: http.StatusOK, }, wantedStatus: http.StatusOK, }, { name: "Stop with error response at job deletion, should return error", args: args{ mockReturnBody: []byte("error"), mockReturnStatus: http.StatusBadRequest, }, wantedStatus: http.StatusBadRequest, wantedBody: "Please remove job 14e7bb84-a44d-44c1-90b7-6995a92ad43c manually", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clientMock := setUpClientMock(tt.args.mockReturnBody, tt.args.mockReturnStatus) handler := http.HandlerFunc(stopHandler) responseRecorder := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodPost, "/stop", nil) handler.ServeHTTP(responseRecorder, r) assertions.Equal(tt.wantedStatus, responseRecorder.Code, tt.name) assertions.Contains(responseRecorder.Body.String(), tt.wantedBody, tt.name) var actualRequest *http.Request clientMock.AssertCalled(t, "Do", mock.MatchedBy(func(req *http.Request) bool { actualRequest = req return true })) assertions.Equal(http.MethodDelete, actualRequest.Method) assertions.Equal("http", actualRequest.URL.Scheme) assertions.Equal("enrichmentservice:8083", actualRequest.URL.Host) assertions.Equal("/data-consumer/v1/info-jobs/14e7bb84-a44d-44c1-90b7-6995a92ad43c", actualRequest.URL.Path) clientMock.AssertNumberOfCalls(t, "Do", 1) // Check that the running status is "stopped" statusHandler := http.HandlerFunc(statusHandler) statusResponseRecorder := httptest.NewRecorder() statusRequest, _ := http.NewRequest(http.MethodGet, "/status", nil) statusHandler.ServeHTTP(statusResponseRecorder, statusRequest) assertions.Equal(http.StatusOK, statusResponseRecorder.Code) assertions.Equal(`{"status": "stopped"}`, statusResponseRecorder.Body.String()) }) } } func Test_deleteOnShutdown(t *testing.T) { assertions := require.New(t) var buf bytes.Buffer log.SetOutput(&buf) t.Cleanup(func() { log.SetOutput(os.Stderr) }) type args struct { mockReturnBody []byte mockReturnStatus int } tests := []struct { name string args args wantedLog string }{ { name: "Delete with successful job deletion, should return ok", args: args{ mockReturnBody: []byte(""), mockReturnStatus: http.StatusOK, }, }, { name: "Stop with error response at job deletion, should return error", args: args{ mockReturnBody: []byte("error"), mockReturnStatus: http.StatusBadRequest, }, wantedLog: "Please remove job 14e7bb84-a44d-44c1-90b7-6995a92ad43c manually", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { setUpClientMock(tt.args.mockReturnBody, tt.args.mockReturnStatus) c := make(chan os.Signal, 1) go deleteOnShutdown(c) c <- syscall.SIGTERM waitForLogToBeWritten(&buf) log := buf.String() if tt.wantedLog != "" { assertions.Contains(log, "level=error") assertions.Contains(log, "Unable to delete job on shutdown due to:") assertions.Contains(log, tt.wantedLog) } }) } } func waitForLogToBeWritten(logBuf *bytes.Buffer) { wg := sync.WaitGroup{} wg.Add(1) for { if waitTimeout(&wg, 10*time.Millisecond) && logBuf.Len() != 0 { wg.Done() break } } } // waitTimeout waits for the waitgroup for the specified max timeout. // Returns true if waiting timed out. func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { c := make(chan struct{}) go func() { defer close(c) wg.Wait() }() select { case <-c: return false // completed normally case <-time.After(timeout): return true // timed out } } func setUpClientMock(body []byte, status int) *mocks.HTTPClient { clientMock := mocks.HTTPClient{} clientMock.On("Do", mock.Anything).Return(&http.Response{ Body: ioutil.NopCloser(bytes.NewReader(body)), StatusCode: status, }, nil) client = &clientMock return &clientMock }