3 // ========================LICENSE_START=================================
6 // Copyright (C) 2023-2024: OpenInfra Foundation Europe
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
31 "oransc.org/nonrtric/servicemanager/internal/common29122"
32 "oransc.org/nonrtric/servicemanager/internal/envreader"
33 "oransc.org/nonrtric/servicemanager/internal/invokermanagementapi"
34 "oransc.org/nonrtric/servicemanager/internal/kongclear"
36 "oransc.org/nonrtric/servicemanager/internal/providermanagement"
37 provapi "oransc.org/nonrtric/servicemanager/internal/providermanagementapi"
38 "oransc.org/nonrtric/servicemanager/internal/publishservice"
39 publishapi "oransc.org/nonrtric/servicemanager/internal/publishserviceapi"
41 "github.com/deepmap/oapi-codegen/pkg/middleware"
42 "github.com/deepmap/oapi-codegen/pkg/testutil"
43 "github.com/labstack/echo/v4"
44 echomiddleware "github.com/labstack/echo/v4/middleware"
45 log "github.com/sirupsen/logrus"
46 "github.com/stretchr/testify/assert"
48 "oransc.org/nonrtric/capifcore"
49 mockKong "oransc.org/nonrtric/servicemanager/mockkong"
53 eServiceManager *echo.Echo
56 mockConfigReader *envreader.MockConfigReader
57 serviceManagerServer *httptest.Server
58 capifServer *httptest.Server
59 mockKongServer *httptest.Server
62 func TestMain(m *testing.M) {
75 func setupTest() error {
76 // Start the mock Kong server
78 mockKong.RegisterHandlers(eKong)
79 mockKongServer = httptest.NewServer(eKong)
81 // Parse the server URL
82 parsedMockKongURL, err := url.Parse(mockKongServer.URL)
84 log.Fatalf("error parsing mock Kong URL: %v", err)
88 // Extract the host and port
89 mockKongHost := parsedMockKongURL.Hostname()
90 mockKongControlPlanePort := parsedMockKongURL.Port()
92 eCapifWeb = echo.New()
93 capifcore.RegisterHandlers(eCapifWeb, nil, nil)
94 capifServer = httptest.NewServer(eCapifWeb)
96 // Parse the server URL
97 parsedCapifURL, err := url.Parse(capifServer.URL)
99 log.Fatalf("error parsing mock Kong URL: %v", err)
103 // Extract the host and port
104 capifHost := parsedCapifURL.Hostname()
105 capifPort := parsedCapifURL.Port()
107 // Set up the mock config reader with the desired configuration for testing
108 mockConfigReader = &envreader.MockConfigReader{
109 MockedConfig: map[string]string{
110 "KONG_DOMAIN": "kong",
111 "KONG_PROTOCOL": "http",
112 "KONG_CONTROL_PLANE_IPV4": mockKongHost,
113 "KONG_CONTROL_PLANE_PORT": mockKongControlPlanePort,
114 "KONG_DATA_PLANE_IPV4": "10.101.1.101",
115 "KONG_DATA_PLANE_PORT": "32080",
116 "CAPIF_PROTOCOL": "http",
117 "CAPIF_IPV4": capifHost,
118 "CAPIF_PORT": capifPort,
120 "SERVICE_MANAGER_PORT": "8095",
121 "TEST_SERVICE_IPV4": "10.101.1.101",
122 "TEST_SERVICE_PORT": "30951",
126 myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
128 log.Fatal("error loading environment file on setupTest")
132 eServiceManager = echo.New()
133 err = registerHandlers(eServiceManager, myEnv, myPorts)
135 log.Fatal("registerHandlers fatal error on setupTest")
138 serviceManagerServer = httptest.NewServer(eServiceManager)
144 func getProvider() provapi.APIProviderEnrolmentDetails {
147 funcInfoAPF = "rApp Kong as APF"
148 funcInfoAEF = "rApp Kong as AEF"
151 testFuncs := []provapi.APIProviderFunctionDetails{
153 ApiProvFuncInfo: &funcInfoAPF,
154 ApiProvFuncRole: provapi.ApiProviderFuncRoleAPF,
155 RegInfo: provapi.RegistrationInformation{
156 ApiProvPubKey: "APF-PublicKey",
160 ApiProvFuncInfo: &funcInfoAEF,
161 ApiProvFuncRole: provapi.ApiProviderFuncRoleAEF,
162 RegInfo: provapi.RegistrationInformation{
163 ApiProvPubKey: "AEF-PublicKey",
167 return provapi.APIProviderEnrolmentDetails{
169 ApiProvDomInfo: &domainInfo,
170 ApiProvFuncs: &testFuncs,
174 func capifCleanUp() {
175 t := new(testing.T) // Create a new testing.T instance for capifCleanUp
177 // Delete the invoker
178 invokerInfo := "invoker a"
179 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
181 result := testutil.NewRequest().Delete("/api-invoker-management/v1/onboardedInvokers/"+invokerId).Go(t, eServiceManager)
182 assert.Equal(t, http.StatusNoContent, result.Code())
184 // Delete the original published service
185 apfId := "APF_id_rApp_Kong_as_APF"
187 apiId := "api_id_" + apiName
189 result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
190 assert.Equal(t, http.StatusNoContent, result.Code())
192 // Delete the first published service
193 apfId = "APF_id_rApp_Kong_as_APF"
195 apiId = "api_id_" + apiName
197 result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
198 assert.Equal(t, http.StatusNoContent, result.Code())
200 // Delete the second published service
202 apiId = "api_id_" + apiName
204 result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
205 assert.Equal(t, http.StatusNoContent, result.Code())
207 // Delete the provider
208 domainID := "domain_id_Kong"
209 result = testutil.NewRequest().Delete("/api-provider-management/v1/registrations/"+domainID).Go(t, eServiceManager)
210 assert.Equal(t, http.StatusNoContent, result.Code())
213 func teardown() error {
214 log.Trace("entering teardown")
216 myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
218 log.Fatal("error loading environment file")
222 err = kongclear.KongClear(myEnv, myPorts)
224 log.Fatal("error clearing Kong on teardown")
227 mockKongServer.Close()
229 serviceManagerServer.Close()
234 func TestRegisterValidProvider(t *testing.T) {
237 newProvider := getProvider()
239 // Register a valid provider
240 result := testutil.NewRequest().Post("/api-provider-management/v1/registrations").WithJsonBody(newProvider).Go(t, eServiceManager)
241 assert.Equal(t, http.StatusCreated, result.Code())
243 var resultProvider provapi.APIProviderEnrolmentDetails
244 err := result.UnmarshalBodyToObject(&resultProvider)
245 assert.NoError(t, err, "error unmarshaling response")
248 func TestPublishUnpublishService(t *testing.T) {
249 apfId := "APF_id_rApp_Kong_as_APF"
251 newApiId := "api_id_" + apiName
253 myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
254 assert.Nil(t, err, "error reading env file")
256 testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
257 testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
259 assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
260 assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
262 // Check no services published
263 result := testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager)
264 assert.Equal(t, http.StatusOK, result.Code())
266 // Parse JSON from the response body
267 var resultServices []publishapi.ServiceAPIDescription
268 err = result.UnmarshalJsonToObject(&resultServices)
269 assert.NoError(t, err, "error unmarshaling response")
271 // Check if the parsed array is empty
272 assert.Zero(t, len(resultServices))
274 aefId := "AEF_id_rApp_Kong_as_AEF"
275 namespace := "namespace"
276 repoName := "repoName"
277 chartName := "chartName"
278 releaseName := "releaseName"
279 description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
281 newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
283 // Publish a service for provider
284 result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
285 assert.Equal(t, http.StatusCreated, result.Code())
287 if result.Code() != http.StatusCreated {
288 log.Fatalf("failed to publish the service with HTTP result code %d", result.Code())
292 var resultService publishapi.ServiceAPIDescription
293 err = result.UnmarshalJsonToObject(&resultService)
294 assert.NoError(t, err, "error unmarshaling response")
295 assert.Equal(t, newApiId, *resultService.ApiId)
297 assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation))
299 // Check that the service is published for the provider
300 result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
301 assert.Equal(t, http.StatusOK, result.Code())
303 err = result.UnmarshalJsonToObject(&resultService)
304 assert.NoError(t, err, "error unmarshaling response")
305 assert.Equal(t, newApiId, *resultService.ApiId)
307 aefProfile := (*resultService.AefProfiles)[0]
308 interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
310 resultServiceIpv4 := *interfaceDescription.Ipv4Addr
311 resultServicePort := *interfaceDescription.Port
313 kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"])
314 kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
316 assert.NotEmpty(t, kongDataPlaneIPv4, "KONG_DATA_PLANE_IPV4 is required in .env file for unit testing")
317 assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing")
319 assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4)
320 assert.Equal(t, kongDataPlanePort, resultServicePort)
323 func TestOnboardInvoker(t *testing.T) {
324 invokerInfo := "invoker a"
325 newInvoker := getInvoker(invokerInfo)
327 // Onboard a valid invoker
328 result := testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(newInvoker).Go(t, eServiceManager)
329 assert.Equal(t, http.StatusCreated, result.Code())
331 var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
333 err := result.UnmarshalBodyToObject(&resultInvoker)
334 assert.NoError(t, err, "error unmarshaling response")
336 wantedInvokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
338 assert.Equal(t, wantedInvokerId, *resultInvoker.ApiInvokerId)
339 assert.Equal(t, newInvoker.NotificationDestination, resultInvoker.NotificationDestination)
340 assert.Equal(t, newInvoker.OnboardingInformation.ApiInvokerPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
342 assert.Equal(t, "http://example.com/api-invoker-management/v1/onboardedInvokers/"+*resultInvoker.ApiInvokerId, result.Recorder.Header().Get(echo.HeaderLocation))
344 // Onboarding the same invoker should result in Forbidden
345 result = testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(newInvoker).Go(t, eServiceManager)
347 assert.Equal(t, http.StatusForbidden, result.Code())
349 var problemDetails common29122.ProblemDetails
350 err = result.UnmarshalBodyToObject(&problemDetails)
351 assert.NoError(t, err, "error unmarshaling response")
353 assert.Equal(t, http.StatusForbidden, *problemDetails.Status)
354 assert.Contains(t, *problemDetails.Cause, "already onboarded")
356 // Onboard an invoker missing required NotificationDestination, should get 400 with problem details
357 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
358 OnboardingInformation: invokermanagementapi.OnboardingInformation{
359 ApiInvokerPublicKey: "newKey",
362 result = testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, eServiceManager)
363 assert.Equal(t, http.StatusBadRequest, result.Code())
365 err = result.UnmarshalBodyToObject(&problemDetails)
366 assert.NoError(t, err, "error unmarshaling response")
368 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
369 assert.Contains(t, *problemDetails.Cause, "missing")
370 assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
372 // Onboard an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
373 invalidInvoker = invokermanagementapi.APIInvokerEnrolmentDetails{
374 NotificationDestination: "http://golang.cafe/",
377 result = testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, eServiceManager)
378 assert.Equal(t, http.StatusBadRequest, result.Code())
380 err = result.UnmarshalBodyToObject(&problemDetails)
381 assert.NoError(t, err, "error unmarshaling response")
383 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
384 assert.Contains(t, *problemDetails.Cause, "missing")
385 assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
388 func TestDeleteInvoker(t *testing.T) {
389 invokerInfo := "invoker a"
390 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
392 // Delete the invoker
393 result := testutil.NewRequest().Delete("/api-invoker-management/v1/onboardedInvokers/"+invokerId).Go(t, eServiceManager)
394 assert.Equal(t, http.StatusNoContent, result.Code())
397 func TestUpdateInvoker(t *testing.T) {
398 invokerInfo := "invoker a"
399 invoker := getInvoker(invokerInfo)
400 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
402 // Onboard a valid invoker
403 result := testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(invoker).Go(t, eServiceManager)
404 assert.Equal(t, http.StatusCreated, result.Code())
406 // Update the invoker with valid invoker, should return 200 with updated invoker details
407 newNotifURL := "http://golang.org/"
408 invoker.NotificationDestination = common29122.Uri(newNotifURL)
409 newPublicKey := "newPublicKey"
410 invoker.OnboardingInformation.ApiInvokerPublicKey = newPublicKey
412 invoker.ApiInvokerId = &invokerId
414 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invoker).Go(t, eServiceManager)
415 assert.Equal(t, http.StatusOK, result.Code())
417 var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
419 err := result.UnmarshalBodyToObject(&resultInvoker)
420 assert.NoError(t, err, "error unmarshaling response")
422 assert.Equal(t, invokerId, *resultInvoker.ApiInvokerId)
423 assert.Equal(t, newNotifURL, string(resultInvoker.NotificationDestination))
424 assert.Equal(t, newPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
426 // Update with an invoker missing required NotificationDestination, should get 400 with problem details
427 validOnboardingInfo := invokermanagementapi.OnboardingInformation{
428 ApiInvokerPublicKey: "key",
430 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
431 ApiInvokerId: &invokerId,
432 OnboardingInformation: validOnboardingInfo,
434 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, eServiceManager)
435 assert.Equal(t, http.StatusBadRequest, result.Code())
437 var problemDetails common29122.ProblemDetails
438 err = result.UnmarshalBodyToObject(&problemDetails)
439 assert.NoError(t, err, "error unmarshaling response")
441 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
442 assert.Contains(t, *problemDetails.Cause, "missing")
443 assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
445 // Update with an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
446 invalidInvoker.NotificationDestination = "http://golang.org/"
447 invalidInvoker.OnboardingInformation = invokermanagementapi.OnboardingInformation{}
448 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, eServiceManager)
449 assert.Equal(t, http.StatusBadRequest, result.Code())
451 err = result.UnmarshalBodyToObject(&problemDetails)
452 assert.NoError(t, err, "error unmarshaling response")
454 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
455 assert.Contains(t, *problemDetails.Cause, "missing")
456 assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
458 // Update with an invoker with other ApiInvokerId than the one provided in the URL, should get 400 with problem details
460 invalidInvoker.ApiInvokerId = &invalidId
461 invalidInvoker.OnboardingInformation = validOnboardingInfo
462 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, eServiceManager)
464 assert.Equal(t, http.StatusBadRequest, result.Code())
466 err = result.UnmarshalBodyToObject(&problemDetails)
467 assert.NoError(t, err, "error unmarshaling response")
469 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
470 assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
472 // Update an invoker that has not been onboarded, should get 404 with problem details
474 invoker.ApiInvokerId = &missingId
475 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, eServiceManager)
476 assert.Equal(t, http.StatusNotFound, result.Code())
478 err = result.UnmarshalBodyToObject(&problemDetails)
479 assert.NoError(t, err, "error unmarshaling response")
481 assert.Equal(t, http.StatusNotFound, *problemDetails.Status)
482 assert.Contains(t, *problemDetails.Cause, "not been onboarded")
483 assert.Contains(t, *problemDetails.Cause, "invoker")
487 func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string]int) (err error) {
488 capifProtocol := myEnv["CAPIF_PROTOCOL"]
489 capifIPv4 := common29122.Ipv4Addr(myEnv["CAPIF_IPV4"])
490 capifPort := common29122.Port(myPorts["CAPIF_PORT"])
491 kongDomain := myEnv["KONG_DOMAIN"]
492 kongProtocol := myEnv["KONG_PROTOCOL"]
493 kongControlPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_CONTROL_PLANE_IPV4"])
494 kongControlPlanePort := common29122.Port(myPorts["KONG_CONTROL_PLANE_PORT"])
495 kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"])
496 kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
498 // Register ProviderManagement
499 providerManagerSwagger, err := provapi.GetSwagger()
501 log.Fatalf("error loading ProviderManagement swagger spec\n: %s", err)
504 providerManagerSwagger.Servers = nil
505 providerManager := providermanagement.NewProviderManager(capifProtocol, capifIPv4, capifPort)
507 var group *echo.Group
509 group = e.Group("/api-provider-management/v1")
510 group.Use(middleware.OapiRequestValidator(providerManagerSwagger))
511 provapi.RegisterHandlersWithBaseURL(e, providerManager, "/api-provider-management/v1")
513 publishServiceSwagger, err := publishapi.GetSwagger()
515 fmt.Fprintf(os.Stderr, "Error loading PublishService swagger spec\n: %s", err)
519 publishServiceSwagger.Servers = nil
521 ps := publishservice.NewPublishService(
522 kongDomain, kongProtocol,
523 kongControlPlaneIPv4, kongControlPlanePort,
524 kongDataPlaneIPv4, kongDataPlanePort,
525 capifProtocol, capifIPv4, capifPort)
527 group = e.Group("/published-apis/v1")
528 group.Use(echomiddleware.Logger())
529 group.Use(middleware.OapiRequestValidator(publishServiceSwagger))
530 publishapi.RegisterHandlersWithBaseURL(e, ps, "/published-apis/v1")
532 invokerServiceSwagger, err := invokermanagementapi.GetSwagger()
534 fmt.Fprintf(os.Stderr, "Error loading InvokerManagement swagger spec\n: %s", err)
538 invokerServiceSwagger.Servers = nil
540 im := NewInvokerManager(capifProtocol, capifIPv4, capifPort)
542 group = e.Group("/api-invoker-management/v1")
543 group.Use(echomiddleware.Logger())
544 group.Use(middleware.OapiRequestValidator(invokerServiceSwagger))
545 invokermanagementapi.RegisterHandlersWithBaseURL(e, im, "api-invoker-management/v1")
550 func getServiceAPIDescription(aefId, apiName, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port) publishapi.ServiceAPIDescription {
552 var protocol publishapi.Protocol = "HTTP_1_1"
554 return publishapi.ServiceAPIDescription{
555 AefProfiles: &[]publishapi.AefProfile{
558 InterfaceDescriptions: &[]publishapi.InterfaceDescription{
560 Ipv4Addr: &testServiceIpv4,
561 Port: &testServicePort,
562 SecurityMethods: &[]publishapi.SecurityMethod{
567 DomainName: &domainName,
569 Versions: []publishapi.Version{
572 Resources: &[]publishapi.Resource{
574 CommType: "REQUEST_RESPONSE",
575 Operations: &[]publishapi.Operation{
578 ResourceName: "helloworld",
587 Description: &description,
591 func getInvoker(invokerInfo string) invokermanagementapi.APIInvokerEnrolmentDetails {
592 newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
593 ApiInvokerInformation: &invokerInfo,
594 NotificationDestination: "http://golang.cafe/",
595 OnboardingInformation: invokermanagementapi.OnboardingInformation{
596 ApiInvokerPublicKey: "key",