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