3 // ========================LICENSE_START=================================
6 // Copyright (C) 2022-2023: Nordix Foundation
7 // Copyright (C) 2024: OpenInfra Foundation Europe
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
13 // http://www.apache.org/licenses/LICENSE-2.0
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
31 "oransc.org/nonrtric/capifcore/internal/eventsapi"
32 "oransc.org/nonrtric/capifcore/internal/invokermanagementapi"
33 "oransc.org/nonrtric/capifcore/internal/keycloak"
35 "github.com/labstack/echo/v4"
37 "oransc.org/nonrtric/capifcore/internal/common29122"
38 "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
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"
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"
51 func TestOnboardInvoker(t *testing.T) {
52 aefProfiles := []publishserviceapi.AefProfile{
53 getAefProfile("aefId"),
56 publishedServices := []publishserviceapi.ServiceAPIDescription{
59 AefProfiles: &aefProfiles,
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)
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)
74 invokerUnderTest, eventChannel, requestHandler := getEcho(&publishRegisterMock, &accessMgmMock)
76 newInvoker := getInvoker(invokerInfo)
78 // Onboard a valid invoker
79 result := testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
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)
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))
95 publishRegisterMock.AssertCalled(t, "GetAllowedPublishedServices", mock.AnythingOfType("[]publishserviceapi.ServiceAPIDescription"))
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")
101 assert.Equal(t, *resultInvoker.ApiInvokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
102 assert.Equal(t, eventsapi.CAPIFEventAPIINVOKERONBOARDED, invokerEvent.Events)
105 // Onboarding the same invoker should result in Forbidden
106 result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
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")
115 // Onboard an invoker missing required NotificationDestination, should get 400 with problem details
116 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
117 OnboardingInformation: invokermanagementapi.OnboardingInformation{
118 ApiInvokerPublicKey: "newKey",
121 result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
130 // Onboard an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
131 invalidInvoker = invokermanagementapi.APIInvokerEnrolmentDetails{
132 NotificationDestination: "http://golang.cafe/",
135 result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
145 func TestDeleteInvoker(t *testing.T) {
146 invokerUnderTest, eventChannel, requestHandler := getEcho(nil, nil)
148 invokerId := "invokerId"
149 newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
150 ApiInvokerId: &invokerId,
151 NotificationDestination: "url",
152 OnboardingInformation: invokermanagementapi.OnboardingInformation{
153 ApiInvokerPublicKey: "key",
156 invokerUnderTest.onboardedInvokers[invokerId] = newInvoker
157 assert.True(t, invokerUnderTest.IsInvokerRegistered(invokerId))
159 // Delete the invoker
160 result := testutil.NewRequest().Delete("/onboardedInvokers/"+invokerId).Go(t, requestHandler)
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")
167 assert.Equal(t, invokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
168 assert.Equal(t, eventsapi.CAPIFEventAPIINVOKEROFFBOARDED, invokerEvent.Events)
172 func TestFailedUpdateInvoker(t *testing.T) {
173 publishRegisterMock := publishmocks.PublishRegister{}
174 publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
175 serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
177 invokerInfo := "invoker a"
178 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
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
184 result := testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
185 assert.Equal(t, http.StatusBadRequest, result.Code())
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)
192 assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
195 func TestUpdateInvoker(t *testing.T) {
196 publishRegisterMock := publishmocks.PublishRegister{}
197 publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
198 serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
200 invokerId := "invokerId"
201 invoker := invokermanagementapi.APIInvokerEnrolmentDetails{
202 ApiInvokerId: &invokerId,
203 NotificationDestination: "http://golang.cafe/",
204 OnboardingInformation: invokermanagementapi.OnboardingInformation{
205 ApiInvokerPublicKey: "key",
208 serviceUnderTest.onboardedInvokers[invokerId] = invoker
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)
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)
225 // Update with an invoker missing required NotificationDestination, should get 400 with problem details
226 validOnboardingInfo := invokermanagementapi.OnboardingInformation{
227 ApiInvokerPublicKey: "key",
229 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
230 ApiInvokerId: &invokerId,
231 OnboardingInformation: validOnboardingInfo,
233 result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
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)
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")
255 // Update with an invoker with other ApiInvokerId than the one provided in the URL, should get 400 with problem details
257 invalidInvoker.ApiInvokerId = &invalidId
258 invalidInvoker.OnboardingInformation = validOnboardingInfo
259 result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
267 // Update an invoker that has not been onboarded, should get 404 with problem details
269 invoker.ApiInvokerId = &missingId
270 result = testutil.NewRequest().Put("/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, requestHandler)
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")
280 func TestGetInvokerApiList(t *testing.T) {
281 aefProfiles1 := []publishserviceapi.AefProfile{
282 getAefProfile("aefId"),
285 apiList := []publishserviceapi.ServiceAPIDescription{
288 AefProfiles: &aefProfiles1,
291 aefProfiles2 := []publishserviceapi.AefProfile{
292 getAefProfile("aefId2"),
295 apiList = append(apiList, publishserviceapi.ServiceAPIDescription{
297 AefProfiles: &aefProfiles2,
299 publishRegisterMock := publishmocks.PublishRegister{}
300 publishRegisterMock.On("GetAllPublishedServices").Return(apiList)
301 invokerUnderTest, _, _ := getEcho(&publishRegisterMock, nil)
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
314 wantedApiList := invokerUnderTest.GetInvokerApiList(invokerAId)
315 assert.NotNil(t, wantedApiList)
316 assert.Len(t, *wantedApiList, 2)
317 assert.Equal(t, apiId, *(*wantedApiList)[0].ApiId)
320 func getEcho(publishRegister publishservice.PublishRegister, keycloakMgm keycloak.AccessManagement) (*InvokerManager, chan eventsapi.EventNotification, *echo.Echo) {
321 swagger, err := invokermanagementapi.GetSwagger()
323 fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
327 swagger.Servers = nil
329 eventChannel := make(chan eventsapi.EventNotification)
330 im := NewInvokerManager(publishRegister, keycloakMgm, eventChannel)
333 e.Use(echomiddleware.Logger())
334 e.Use(middleware.OapiRequestValidator(swagger))
336 invokermanagementapi.RegisterHandlers(e, im)
337 return im, eventChannel, e
340 func getAefProfile(aefId string) publishserviceapi.AefProfile {
341 return publishserviceapi.AefProfile{
343 Versions: []publishserviceapi.Version{
345 Resources: &[]publishserviceapi.Resource{
347 CommType: "REQUEST_RESPONSE",
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",
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) {
372 return &event, false // completed normally
373 case <-time.After(timeout):
374 return nil, true // timed out