| // - |
| // ========================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" |
| "oransc.org/usecase/oruclosedloop/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 := &mocks.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 |
| } |