b46e925a2bea3479726ccfc90c7609c48277eb89
[nonrtric/plt/sme.git] / capifcore / internal / invokermanagement / invokermanagement_test.go
1 // -
2 //
3 //      ========================LICENSE_START=================================
4 //      O-RAN-SC
5 //      %%
6 //      Copyright (C) 2022: Nordix Foundation
7 //      %%
8 //      Licensed under the Apache License, Version 2.0 (the "License");
9 //      you may not use this file except in compliance with the License.
10 //      You may obtain a copy of the License at
11 //
12 //           http://www.apache.org/licenses/LICENSE-2.0
13 //
14 //      Unless required by applicable law or agreed to in writing, software
15 //      distributed under the License is distributed on an "AS IS" BASIS,
16 //      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 //      See the License for the specific language governing permissions and
18 //      limitations under the License.
19 //      ========================LICENSE_END===================================
20 package invokermanagement
21
22 import (
23         "fmt"
24         "net/http"
25         "os"
26         "strings"
27         "testing"
28         "time"
29
30         "oransc.org/nonrtric/capifcore/internal/eventsapi"
31         "oransc.org/nonrtric/capifcore/internal/invokermanagementapi"
32         "oransc.org/nonrtric/capifcore/internal/keycloak"
33
34         "github.com/labstack/echo/v4"
35
36         "oransc.org/nonrtric/capifcore/internal/common29122"
37         "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
38
39         keycloackmocks "oransc.org/nonrtric/capifcore/internal/keycloak/mocks"
40         "oransc.org/nonrtric/capifcore/internal/publishservice"
41         publishmocks "oransc.org/nonrtric/capifcore/internal/publishservice/mocks"
42
43         "github.com/deepmap/oapi-codegen/pkg/middleware"
44         "github.com/deepmap/oapi-codegen/pkg/testutil"
45         echomiddleware "github.com/labstack/echo/v4/middleware"
46         "github.com/stretchr/testify/assert"
47         "github.com/stretchr/testify/mock"
48 )
49
50 func TestOnboardInvoker(t *testing.T) {
51         aefProfiles := []publishserviceapi.AefProfile{
52                 getAefProfile("aefId"),
53         }
54         apiId := "apiId"
55         publishedServices := []publishserviceapi.ServiceAPIDescription{
56                 {
57                         ApiId:       &apiId,
58                         AefProfiles: &aefProfiles,
59                 },
60         }
61
62         invokerInfo := "invoker a"
63         wantedInvokerSecret := "onboarding_secret_" + strings.Replace(invokerInfo, " ", "_", 1)
64         var client keycloak.Client
65         client.Secret = &wantedInvokerSecret
66         publishRegisterMock := publishmocks.PublishRegister{}
67         publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices)
68
69         accessMgmMock := keycloackmocks.AccessManagement{}
70         accessMgmMock.On("AddClient", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil)
71         accessMgmMock.On("GetClientRepresentation", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&client, nil)
72
73         invokerUnderTest, eventChannel, requestHandler := getEcho(&publishRegisterMock, &accessMgmMock)
74
75         newInvoker := getInvoker(invokerInfo)
76
77         // Onboard a valid invoker
78         result := testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
79
80         assert.Equal(t, http.StatusCreated, result.Code())
81         var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
82         err := result.UnmarshalBodyToObject(&resultInvoker)
83         assert.NoError(t, err, "error unmarshaling response")
84         wantedInvokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
85         assert.Equal(t, wantedInvokerId, *resultInvoker.ApiInvokerId)
86         assert.Equal(t, newInvoker.NotificationDestination, resultInvoker.NotificationDestination)
87         assert.Equal(t, newInvoker.OnboardingInformation.ApiInvokerPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
88
89         assert.Equal(t, wantedInvokerSecret, *resultInvoker.OnboardingInformation.OnboardingSecret)
90         assert.Equal(t, "http://example.com/onboardedInvokers/"+*resultInvoker.ApiInvokerId, result.Recorder.Header().Get(echo.HeaderLocation))
91         assert.True(t, invokerUnderTest.IsInvokerRegistered(wantedInvokerId))
92         assert.True(t, invokerUnderTest.VerifyInvokerSecret(wantedInvokerId, wantedInvokerSecret))
93         publishRegisterMock.AssertCalled(t, "GetAllPublishedServices")
94         assert.Equal(t, invokermanagementapi.APIList(publishedServices), *resultInvoker.ApiList)
95         if invokerEvent, timeout := waitForEvent(eventChannel, 1*time.Second); timeout {
96                 assert.Fail(t, "No event sent")
97         } else {
98                 assert.Equal(t, *resultInvoker.ApiInvokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
99                 assert.Equal(t, eventsapi.CAPIFEventAPIINVOKERONBOARDED, invokerEvent.Events)
100         }
101
102         // Onboarding the same invoker should result in Forbidden
103         result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
104
105         assert.Equal(t, http.StatusForbidden, result.Code())
106         var problemDetails common29122.ProblemDetails
107         err = result.UnmarshalBodyToObject(&problemDetails)
108         assert.NoError(t, err, "error unmarshaling response")
109         assert.Equal(t, http.StatusForbidden, *problemDetails.Status)
110         assert.Contains(t, *problemDetails.Cause, "already onboarded")
111
112         // Onboard an invoker missing required NotificationDestination, should get 400 with problem details
113         invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
114                 OnboardingInformation: invokermanagementapi.OnboardingInformation{
115                         ApiInvokerPublicKey: "newKey",
116                 },
117         }
118         result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
119
120         assert.Equal(t, http.StatusBadRequest, result.Code())
121         err = result.UnmarshalBodyToObject(&problemDetails)
122         assert.NoError(t, err, "error unmarshaling response")
123         assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
124         assert.Contains(t, *problemDetails.Cause, "missing")
125         assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
126
127         // Onboard an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
128         invalidInvoker = invokermanagementapi.APIInvokerEnrolmentDetails{
129                 NotificationDestination: "http://golang.cafe/",
130         }
131
132         result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
133
134         assert.Equal(t, http.StatusBadRequest, result.Code())
135         err = result.UnmarshalBodyToObject(&problemDetails)
136         assert.NoError(t, err, "error unmarshaling response")
137         assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
138         assert.Contains(t, *problemDetails.Cause, "missing")
139         assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
140 }
141
142 func TestDeleteInvoker(t *testing.T) {
143         invokerUnderTest, eventChannel, requestHandler := getEcho(nil, nil)
144
145         invokerId := "invokerId"
146         newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
147                 ApiInvokerId:            &invokerId,
148                 NotificationDestination: "url",
149                 OnboardingInformation: invokermanagementapi.OnboardingInformation{
150                         ApiInvokerPublicKey: "key",
151                 },
152         }
153         invokerUnderTest.onboardedInvokers[invokerId] = newInvoker
154         assert.True(t, invokerUnderTest.IsInvokerRegistered(invokerId))
155
156         // Delete the invoker
157         result := testutil.NewRequest().Delete("/onboardedInvokers/"+invokerId).Go(t, requestHandler)
158
159         assert.Equal(t, http.StatusNoContent, result.Code())
160         assert.False(t, invokerUnderTest.IsInvokerRegistered(invokerId))
161         if invokerEvent, timeout := waitForEvent(eventChannel, 1*time.Second); timeout {
162                 assert.Fail(t, "No event sent")
163         } else {
164                 assert.Equal(t, invokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
165                 assert.Equal(t, eventsapi.CAPIFEventAPIINVOKEROFFBOARDED, invokerEvent.Events)
166         }
167 }
168
169 func TestFailedUpdateInvoker(t *testing.T) {
170         publishRegisterMock := publishmocks.PublishRegister{}
171         publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
172         serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
173
174         invokerInfo := "invoker a"
175         invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
176
177         // Attempt to update with an invoker without the ApiInvokerId provided in the parameter body. We should get 400 with problem details.
178         invalidInvoker := getInvoker(invokerInfo)
179         serviceUnderTest.onboardedInvokers[invokerId] = invalidInvoker
180
181         result := testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
182         assert.Equal(t, http.StatusBadRequest, result.Code())
183
184         var problemDetails common29122.ProblemDetails
185         err := result.UnmarshalBodyToObject(&problemDetails)
186         assert.NoError(t, err, "error unmarshaling response")
187         assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
188
189         assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
190 }
191
192 func TestUpdateInvoker(t *testing.T) {
193         publishRegisterMock := publishmocks.PublishRegister{}
194         publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
195         serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
196
197         invokerId := "invokerId"
198         invoker := invokermanagementapi.APIInvokerEnrolmentDetails{
199                 ApiInvokerId:            &invokerId,
200                 NotificationDestination: "http://golang.cafe/",
201                 OnboardingInformation: invokermanagementapi.OnboardingInformation{
202                         ApiInvokerPublicKey: "key",
203                 },
204         }
205         serviceUnderTest.onboardedInvokers[invokerId] = invoker
206
207         // Update the invoker with valid invoker, should return 200 with updated invoker details
208         newNotifURL := "http://golang.org/"
209         invoker.NotificationDestination = common29122.Uri(newNotifURL)
210         newPublicKey := "newPublicKey"
211         invoker.OnboardingInformation.ApiInvokerPublicKey = newPublicKey
212         result := testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invoker).Go(t, requestHandler)
213
214         var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
215         assert.Equal(t, http.StatusOK, result.Code())
216         err := result.UnmarshalBodyToObject(&resultInvoker)
217         assert.NoError(t, err, "error unmarshaling response")
218         assert.Equal(t, invokerId, *resultInvoker.ApiInvokerId)
219         assert.Equal(t, newNotifURL, string(resultInvoker.NotificationDestination))
220         assert.Equal(t, newPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
221
222         // Update with an invoker missing required NotificationDestination, should get 400 with problem details
223         validOnboardingInfo := invokermanagementapi.OnboardingInformation{
224                 ApiInvokerPublicKey: "key",
225         }
226         invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
227                 ApiInvokerId:          &invokerId,
228                 OnboardingInformation: validOnboardingInfo,
229         }
230         result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
231
232         assert.Equal(t, http.StatusBadRequest, result.Code())
233         var problemDetails common29122.ProblemDetails
234         err = result.UnmarshalBodyToObject(&problemDetails)
235         assert.NoError(t, err, "error unmarshaling response")
236         assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
237         assert.Contains(t, *problemDetails.Cause, "missing")
238         assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
239
240         // Update with an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
241         invalidInvoker.NotificationDestination = "http://golang.org/"
242         invalidInvoker.OnboardingInformation = invokermanagementapi.OnboardingInformation{}
243         result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
244
245         assert.Equal(t, http.StatusBadRequest, result.Code())
246         err = result.UnmarshalBodyToObject(&problemDetails)
247         assert.NoError(t, err, "error unmarshaling response")
248         assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
249         assert.Contains(t, *problemDetails.Cause, "missing")
250         assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
251
252         // Update with an invoker with other ApiInvokerId than the one provided in the URL, should get 400 with problem details
253         invalidId := "1"
254         invalidInvoker.ApiInvokerId = &invalidId
255         invalidInvoker.OnboardingInformation = validOnboardingInfo
256         result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
257
258         assert.Equal(t, http.StatusBadRequest, result.Code())
259         err = result.UnmarshalBodyToObject(&problemDetails)
260         assert.NoError(t, err, "error unmarshaling response")
261         assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
262         assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
263
264         // Update an invoker that has not been onboarded, should get 404 with problem details
265         missingId := "1"
266         invoker.ApiInvokerId = &missingId
267         result = testutil.NewRequest().Put("/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, requestHandler)
268
269         assert.Equal(t, http.StatusNotFound, result.Code())
270         err = result.UnmarshalBodyToObject(&problemDetails)
271         assert.NoError(t, err, "error unmarshaling response")
272         assert.Equal(t, http.StatusNotFound, *problemDetails.Status)
273         assert.Contains(t, *problemDetails.Cause, "not been onboarded")
274         assert.Contains(t, *problemDetails.Cause, "invoker")
275 }
276
277 func TestGetInvokerApiList(t *testing.T) {
278         aefProfiles1 := []publishserviceapi.AefProfile{
279                 getAefProfile("aefId"),
280         }
281         apiId := "apiId"
282         apiList := []publishserviceapi.ServiceAPIDescription{
283                 {
284                         ApiId:       &apiId,
285                         AefProfiles: &aefProfiles1,
286                 },
287         }
288         aefProfiles2 := []publishserviceapi.AefProfile{
289                 getAefProfile("aefId2"),
290         }
291         apiId2 := "apiId2"
292         apiList = append(apiList, publishserviceapi.ServiceAPIDescription{
293                 ApiId:       &apiId2,
294                 AefProfiles: &aefProfiles2,
295         })
296         publishRegisterMock := publishmocks.PublishRegister{}
297         publishRegisterMock.On("GetAllPublishedServices").Return(apiList)
298         invokerUnderTest, _, _ := getEcho(&publishRegisterMock, nil)
299
300         invokerInfo := "invoker a"
301         newInvoker := getInvoker(invokerInfo)
302         invokerAId := "api_invoker_id_" + strings.ReplaceAll(invokerInfo, " ", "_")
303         newInvoker.ApiInvokerId = &invokerAId
304         invokerUnderTest.onboardedInvokers[invokerAId] = newInvoker
305         invokerInfo = "invoker b"
306         newInvoker = getInvoker(invokerInfo)
307         invokerId := "api_invoker_id_" + strings.ReplaceAll(invokerInfo, " ", "_")
308         newInvoker.ApiInvokerId = &invokerId
309         invokerUnderTest.onboardedInvokers[invokerId] = newInvoker
310
311         wantedApiList := invokerUnderTest.GetInvokerApiList(invokerAId)
312         assert.NotNil(t, wantedApiList)
313         assert.Len(t, *wantedApiList, 2)
314         assert.Equal(t, apiId, *(*wantedApiList)[0].ApiId)
315 }
316
317 func getEcho(publishRegister publishservice.PublishRegister, keycloakMgm keycloak.AccessManagement) (*InvokerManager, chan eventsapi.EventNotification, *echo.Echo) {
318         swagger, err := invokermanagementapi.GetSwagger()
319         if err != nil {
320                 fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
321                 os.Exit(1)
322         }
323
324         swagger.Servers = nil
325
326         eventChannel := make(chan eventsapi.EventNotification)
327         im := NewInvokerManager(publishRegister, keycloakMgm, eventChannel)
328
329         e := echo.New()
330         e.Use(echomiddleware.Logger())
331         e.Use(middleware.OapiRequestValidator(swagger))
332
333         invokermanagementapi.RegisterHandlers(e, im)
334         return im, eventChannel, e
335 }
336
337 func getAefProfile(aefId string) publishserviceapi.AefProfile {
338         return publishserviceapi.AefProfile{
339                 AefId: aefId,
340                 Versions: []publishserviceapi.Version{
341                         {
342                                 Resources: &[]publishserviceapi.Resource{
343                                         {
344                                                 CommType: "REQUEST_RESPONSE",
345                                         },
346                                 },
347                         },
348                 },
349         }
350 }
351
352 func getInvoker(invokerInfo string) invokermanagementapi.APIInvokerEnrolmentDetails {
353         newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
354                 ApiInvokerInformation:   &invokerInfo,
355                 NotificationDestination: "http://golang.cafe/",
356                 OnboardingInformation: invokermanagementapi.OnboardingInformation{
357                         ApiInvokerPublicKey: "key",
358                 },
359                 ApiList: nil,
360         }
361         return newInvoker
362 }
363
364 // waitForEvent waits for the channel to receive an event for the specified max timeout.
365 // Returns true if waiting timed out.
366 func waitForEvent(ch chan eventsapi.EventNotification, timeout time.Duration) (*eventsapi.EventNotification, bool) {
367         select {
368         case event := <-ch:
369                 return &event, false // completed normally
370         case <-time.After(timeout):
371                 return nil, true // timed out
372         }
373 }