3 // ========================LICENSE_START=================================
6 // Copyright (C) 2022: Nordix Foundation
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
12 // http://www.apache.org/licenses/LICENSE-2.0
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
30 "oransc.org/nonrtric/capifcore/internal/eventsapi"
31 "oransc.org/nonrtric/capifcore/internal/invokermanagementapi"
32 "oransc.org/nonrtric/capifcore/internal/keycloak"
34 "github.com/labstack/echo/v4"
36 "oransc.org/nonrtric/capifcore/internal/common29122"
37 "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
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"
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"
50 func TestOnboardInvoker(t *testing.T) {
51 aefProfiles := []publishserviceapi.AefProfile{
52 getAefProfile("aefId"),
55 publishedServices := []publishserviceapi.ServiceAPIDescription{
58 AefProfiles: &aefProfiles,
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)
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)
73 invokerUnderTest, eventChannel, requestHandler := getEcho(&publishRegisterMock, &accessMgmMock)
75 newInvoker := getInvoker(invokerInfo)
77 // Onboard a valid invoker
78 result := testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
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)
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")
98 assert.Equal(t, *resultInvoker.ApiInvokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
99 assert.Equal(t, eventsapi.CAPIFEventAPIINVOKERONBOARDED, invokerEvent.Events)
102 // Onboarding the same invoker should result in Forbidden
103 result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
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")
112 // Onboard an invoker missing required NotificationDestination, should get 400 with problem details
113 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
114 OnboardingInformation: invokermanagementapi.OnboardingInformation{
115 ApiInvokerPublicKey: "newKey",
118 result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
127 // Onboard an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
128 invalidInvoker = invokermanagementapi.APIInvokerEnrolmentDetails{
129 NotificationDestination: "http://golang.cafe/",
132 result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
142 func TestDeleteInvoker(t *testing.T) {
143 invokerUnderTest, eventChannel, requestHandler := getEcho(nil, nil)
145 invokerId := "invokerId"
146 newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
147 ApiInvokerId: &invokerId,
148 NotificationDestination: "url",
149 OnboardingInformation: invokermanagementapi.OnboardingInformation{
150 ApiInvokerPublicKey: "key",
153 invokerUnderTest.onboardedInvokers[invokerId] = newInvoker
154 assert.True(t, invokerUnderTest.IsInvokerRegistered(invokerId))
156 // Delete the invoker
157 result := testutil.NewRequest().Delete("/onboardedInvokers/"+invokerId).Go(t, requestHandler)
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")
164 assert.Equal(t, invokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
165 assert.Equal(t, eventsapi.CAPIFEventAPIINVOKEROFFBOARDED, invokerEvent.Events)
169 func TestFailedUpdateInvoker(t *testing.T) {
170 publishRegisterMock := publishmocks.PublishRegister{}
171 publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
172 serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
174 invokerInfo := "invoker a"
175 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
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
181 result := testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
182 assert.Equal(t, http.StatusBadRequest, result.Code())
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)
189 assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
192 func TestUpdateInvoker(t *testing.T) {
193 publishRegisterMock := publishmocks.PublishRegister{}
194 publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
195 serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
197 invokerId := "invokerId"
198 invoker := invokermanagementapi.APIInvokerEnrolmentDetails{
199 ApiInvokerId: &invokerId,
200 NotificationDestination: "http://golang.cafe/",
201 OnboardingInformation: invokermanagementapi.OnboardingInformation{
202 ApiInvokerPublicKey: "key",
205 serviceUnderTest.onboardedInvokers[invokerId] = invoker
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)
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)
222 // Update with an invoker missing required NotificationDestination, should get 400 with problem details
223 validOnboardingInfo := invokermanagementapi.OnboardingInformation{
224 ApiInvokerPublicKey: "key",
226 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
227 ApiInvokerId: &invokerId,
228 OnboardingInformation: validOnboardingInfo,
230 result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
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)
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")
252 // Update with an invoker with other ApiInvokerId than the one provided in the URL, should get 400 with problem details
254 invalidInvoker.ApiInvokerId = &invalidId
255 invalidInvoker.OnboardingInformation = validOnboardingInfo
256 result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
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")
264 // Update an invoker that has not been onboarded, should get 404 with problem details
266 invoker.ApiInvokerId = &missingId
267 result = testutil.NewRequest().Put("/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, requestHandler)
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")
277 func TestGetInvokerApiList(t *testing.T) {
278 aefProfiles1 := []publishserviceapi.AefProfile{
279 getAefProfile("aefId"),
282 apiList := []publishserviceapi.ServiceAPIDescription{
285 AefProfiles: &aefProfiles1,
288 aefProfiles2 := []publishserviceapi.AefProfile{
289 getAefProfile("aefId2"),
292 apiList = append(apiList, publishserviceapi.ServiceAPIDescription{
294 AefProfiles: &aefProfiles2,
296 publishRegisterMock := publishmocks.PublishRegister{}
297 publishRegisterMock.On("GetAllPublishedServices").Return(apiList)
298 invokerUnderTest, _, _ := getEcho(&publishRegisterMock, nil)
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
311 wantedApiList := invokerUnderTest.GetInvokerApiList(invokerAId)
312 assert.NotNil(t, wantedApiList)
313 assert.Len(t, *wantedApiList, 2)
314 assert.Equal(t, apiId, *(*wantedApiList)[0].ApiId)
317 func getEcho(publishRegister publishservice.PublishRegister, keycloakMgm keycloak.AccessManagement) (*InvokerManager, chan eventsapi.EventNotification, *echo.Echo) {
318 swagger, err := invokermanagementapi.GetSwagger()
320 fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
324 swagger.Servers = nil
326 eventChannel := make(chan eventsapi.EventNotification)
327 im := NewInvokerManager(publishRegister, keycloakMgm, eventChannel)
330 e.Use(echomiddleware.Logger())
331 e.Use(middleware.OapiRequestValidator(swagger))
333 invokermanagementapi.RegisterHandlers(e, im)
334 return im, eventChannel, e
337 func getAefProfile(aefId string) publishserviceapi.AefProfile {
338 return publishserviceapi.AefProfile{
340 Versions: []publishserviceapi.Version{
342 Resources: &[]publishserviceapi.Resource{
344 CommType: "REQUEST_RESPONSE",
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",
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) {
369 return &event, false // completed normally
370 case <-time.After(timeout):
371 return nil, true // timed out