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 "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_IPV4": mockKongHost,
113 "KONG_DATA_PLANE_PORT": "32080",
114 "KONG_CONTROL_PLANE_PORT": mockKongControlPlanePort,
115 "CAPIF_PROTOCOL": "http",
116 "CAPIF_IPV4": capifHost,
117 "CAPIF_PORT": capifPort,
119 "SERVICE_MANAGER_PORT": "8095",
120 "TEST_SERVICE_IPV4": "10.101.1.101",
121 "TEST_SERVICE_PORT": "30951",
125 myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
127 log.Fatal("error loading environment file on setupTest")
131 eServiceManager = echo.New()
132 err = registerHandlers(eServiceManager, myEnv, myPorts)
134 log.Fatal("registerHandlers fatal error on setupTest")
137 serviceManagerServer = httptest.NewServer(eServiceManager)
143 func getProvider() provapi.APIProviderEnrolmentDetails {
146 funcInfoAPF = "rApp Kong as APF"
147 funcInfoAEF = "rApp Kong as AEF"
150 testFuncs := []provapi.APIProviderFunctionDetails{
152 ApiProvFuncInfo: &funcInfoAPF,
153 ApiProvFuncRole: provapi.ApiProviderFuncRoleAPF,
154 RegInfo: provapi.RegistrationInformation{
155 ApiProvPubKey: "APF-PublicKey",
159 ApiProvFuncInfo: &funcInfoAEF,
160 ApiProvFuncRole: provapi.ApiProviderFuncRoleAEF,
161 RegInfo: provapi.RegistrationInformation{
162 ApiProvPubKey: "AEF-PublicKey",
166 return provapi.APIProviderEnrolmentDetails{
168 ApiProvDomInfo: &domainInfo,
169 ApiProvFuncs: &testFuncs,
173 func capifCleanUp() {
174 t := new(testing.T) // Create a new testing.T instance for capifCleanUp
176 // Delete the invoker
177 invokerInfo := "invoker a"
178 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
180 result := testutil.NewRequest().Delete("/api-invoker-management/v1/onboardedInvokers/"+invokerId).Go(t, eServiceManager)
181 assert.Equal(t, http.StatusNoContent, result.Code())
183 // Delete the original published service
184 apfId := "APF_id_rApp_Kong_as_APF"
186 apiId := "api_id_" + apiName
188 result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
189 assert.Equal(t, http.StatusNoContent, result.Code())
191 // Delete the first published service
192 apfId = "APF_id_rApp_Kong_as_APF"
194 apiId = "api_id_" + apiName
196 result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
197 assert.Equal(t, http.StatusNoContent, result.Code())
199 // Delete the second published service
201 apiId = "api_id_" + apiName
203 result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
204 assert.Equal(t, http.StatusNoContent, result.Code())
206 // Delete the provider
207 domainID := "domain_id_Kong"
208 result = testutil.NewRequest().Delete("/api-provider-management/v1/registrations/"+domainID).Go(t, eServiceManager)
209 assert.Equal(t, http.StatusNoContent, result.Code())
212 func teardown() error {
213 log.Trace("entering teardown")
215 myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
217 log.Fatal("error loading environment file")
221 err = kongclear.KongClear(myEnv, myPorts)
223 log.Fatal("error clearing Kong on teardown")
226 mockKongServer.Close()
228 serviceManagerServer.Close()
233 func TestRegisterValidProvider(t *testing.T) {
236 newProvider := getProvider()
238 // Register a valid provider
239 result := testutil.NewRequest().Post("/api-provider-management/v1/registrations").WithJsonBody(newProvider).Go(t, eServiceManager)
240 assert.Equal(t, http.StatusCreated, result.Code())
242 var resultProvider provapi.APIProviderEnrolmentDetails
243 err := result.UnmarshalBodyToObject(&resultProvider)
244 assert.NoError(t, err, "error unmarshaling response")
247 func TestPublishUnpublishService(t *testing.T) {
248 apfId := "APF_id_rApp_Kong_as_APF"
250 newApiId := "api_id_" + apiName
252 myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
253 assert.Nil(t, err, "error reading env file")
255 testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
256 testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
258 assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
259 assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
261 // Check no services published
262 result := testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager)
263 assert.Equal(t, http.StatusOK, result.Code())
265 // Parse JSON from the response body
266 var resultServices []publishapi.ServiceAPIDescription
267 err = result.UnmarshalJsonToObject(&resultServices)
268 assert.NoError(t, err, "error unmarshaling response")
270 // Check if the parsed array is empty
271 assert.Zero(t, len(resultServices))
273 aefId := "AEF_id_rApp_Kong_as_AEF"
274 namespace := "namespace"
275 repoName := "repoName"
276 chartName := "chartName"
277 releaseName := "releaseName"
278 description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
280 newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
282 // Publish a service for provider
283 result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
284 assert.Equal(t, http.StatusCreated, result.Code())
286 if result.Code() != http.StatusCreated {
287 log.Fatalf("failed to publish the service with HTTP result code %d", result.Code())
291 var resultService publishapi.ServiceAPIDescription
292 err = result.UnmarshalJsonToObject(&resultService)
293 assert.NoError(t, err, "error unmarshaling response")
294 assert.Equal(t, newApiId, *resultService.ApiId)
296 assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation))
298 // Check that the service is published for the provider
299 result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
300 assert.Equal(t, http.StatusOK, result.Code())
302 err = result.UnmarshalJsonToObject(&resultService)
303 assert.NoError(t, err, "error unmarshaling response")
304 assert.Equal(t, newApiId, *resultService.ApiId)
306 aefProfile := (*resultService.AefProfiles)[0]
307 interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
309 resultServiceIpv4 := *interfaceDescription.Ipv4Addr
310 resultServicePort := *interfaceDescription.Port
312 kongIPv4 := common29122.Ipv4Addr(myEnv["KONG_IPV4"])
313 kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
315 assert.NotEmpty(t, kongIPv4, "KONG_IPV4 is required in .env file for unit testing")
316 assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing")
318 assert.Equal(t, kongIPv4, resultServiceIpv4)
319 assert.Equal(t, kongDataPlanePort, resultServicePort)
322 func TestOnboardInvoker(t *testing.T) {
323 invokerInfo := "invoker a"
324 newInvoker := getInvoker(invokerInfo)
326 // Onboard a valid invoker
327 result := testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(newInvoker).Go(t, eServiceManager)
328 assert.Equal(t, http.StatusCreated, result.Code())
330 var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
332 err := result.UnmarshalBodyToObject(&resultInvoker)
333 assert.NoError(t, err, "error unmarshaling response")
335 wantedInvokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
337 assert.Equal(t, wantedInvokerId, *resultInvoker.ApiInvokerId)
338 assert.Equal(t, newInvoker.NotificationDestination, resultInvoker.NotificationDestination)
339 assert.Equal(t, newInvoker.OnboardingInformation.ApiInvokerPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
341 assert.Equal(t, "http://example.com/api-invoker-management/v1/onboardedInvokers/"+*resultInvoker.ApiInvokerId, result.Recorder.Header().Get(echo.HeaderLocation))
343 // Onboarding the same invoker should result in Forbidden
344 result = testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(newInvoker).Go(t, eServiceManager)
346 assert.Equal(t, http.StatusForbidden, result.Code())
348 var problemDetails common29122.ProblemDetails
349 err = result.UnmarshalBodyToObject(&problemDetails)
350 assert.NoError(t, err, "error unmarshaling response")
352 assert.Equal(t, http.StatusForbidden, *problemDetails.Status)
353 assert.Contains(t, *problemDetails.Cause, "already onboarded")
355 // Onboard an invoker missing required NotificationDestination, should get 400 with problem details
356 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
357 OnboardingInformation: invokermanagementapi.OnboardingInformation{
358 ApiInvokerPublicKey: "newKey",
361 result = testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, eServiceManager)
362 assert.Equal(t, http.StatusBadRequest, result.Code())
364 err = result.UnmarshalBodyToObject(&problemDetails)
365 assert.NoError(t, err, "error unmarshaling response")
367 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
368 assert.Contains(t, *problemDetails.Cause, "missing")
369 assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
371 // Onboard an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
372 invalidInvoker = invokermanagementapi.APIInvokerEnrolmentDetails{
373 NotificationDestination: "http://golang.cafe/",
376 result = testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, eServiceManager)
377 assert.Equal(t, http.StatusBadRequest, result.Code())
379 err = result.UnmarshalBodyToObject(&problemDetails)
380 assert.NoError(t, err, "error unmarshaling response")
382 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
383 assert.Contains(t, *problemDetails.Cause, "missing")
384 assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
387 func TestDeleteInvoker(t *testing.T) {
388 invokerInfo := "invoker a"
389 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
391 // Delete the invoker
392 result := testutil.NewRequest().Delete("/api-invoker-management/v1/onboardedInvokers/"+invokerId).Go(t, eServiceManager)
393 assert.Equal(t, http.StatusNoContent, result.Code())
396 func TestUpdateInvoker(t *testing.T) {
397 invokerInfo := "invoker a"
398 invoker := getInvoker(invokerInfo)
399 invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
401 // Onboard a valid invoker
402 result := testutil.NewRequest().Post("/api-invoker-management/v1/onboardedInvokers").WithJsonBody(invoker).Go(t, eServiceManager)
403 assert.Equal(t, http.StatusCreated, result.Code())
405 // Update the invoker with valid invoker, should return 200 with updated invoker details
406 newNotifURL := "http://golang.org/"
407 invoker.NotificationDestination = common29122.Uri(newNotifURL)
408 newPublicKey := "newPublicKey"
409 invoker.OnboardingInformation.ApiInvokerPublicKey = newPublicKey
411 invoker.ApiInvokerId = &invokerId
413 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invoker).Go(t, eServiceManager)
414 assert.Equal(t, http.StatusOK, result.Code())
416 var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
418 err := result.UnmarshalBodyToObject(&resultInvoker)
419 assert.NoError(t, err, "error unmarshaling response")
421 assert.Equal(t, invokerId, *resultInvoker.ApiInvokerId)
422 assert.Equal(t, newNotifURL, string(resultInvoker.NotificationDestination))
423 assert.Equal(t, newPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
425 // Update with an invoker missing required NotificationDestination, should get 400 with problem details
426 validOnboardingInfo := invokermanagementapi.OnboardingInformation{
427 ApiInvokerPublicKey: "key",
429 invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
430 ApiInvokerId: &invokerId,
431 OnboardingInformation: validOnboardingInfo,
433 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, eServiceManager)
434 assert.Equal(t, http.StatusBadRequest, result.Code())
436 var problemDetails common29122.ProblemDetails
437 err = result.UnmarshalBodyToObject(&problemDetails)
438 assert.NoError(t, err, "error unmarshaling response")
440 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
441 assert.Contains(t, *problemDetails.Cause, "missing")
442 assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
444 // Update with an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
445 invalidInvoker.NotificationDestination = "http://golang.org/"
446 invalidInvoker.OnboardingInformation = invokermanagementapi.OnboardingInformation{}
447 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, eServiceManager)
448 assert.Equal(t, http.StatusBadRequest, result.Code())
450 err = result.UnmarshalBodyToObject(&problemDetails)
451 assert.NoError(t, err, "error unmarshaling response")
453 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
454 assert.Contains(t, *problemDetails.Cause, "missing")
455 assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
457 // Update with an invoker with other ApiInvokerId than the one provided in the URL, should get 400 with problem details
459 invalidInvoker.ApiInvokerId = &invalidId
460 invalidInvoker.OnboardingInformation = validOnboardingInfo
461 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, eServiceManager)
463 assert.Equal(t, http.StatusBadRequest, result.Code())
465 err = result.UnmarshalBodyToObject(&problemDetails)
466 assert.NoError(t, err, "error unmarshaling response")
468 assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
469 assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
471 // Update an invoker that has not been onboarded, should get 404 with problem details
473 invoker.ApiInvokerId = &missingId
474 result = testutil.NewRequest().Put("/api-invoker-management/v1/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, eServiceManager)
475 assert.Equal(t, http.StatusNotFound, result.Code())
477 err = result.UnmarshalBodyToObject(&problemDetails)
478 assert.NoError(t, err, "error unmarshaling response")
480 assert.Equal(t, http.StatusNotFound, *problemDetails.Status)
481 assert.Contains(t, *problemDetails.Cause, "not been onboarded")
482 assert.Contains(t, *problemDetails.Cause, "invoker")
486 func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string]int) (err error) {
487 capifProtocol := myEnv["CAPIF_PROTOCOL"]
488 capifIPv4 := common29122.Ipv4Addr(myEnv["CAPIF_IPV4"])
489 capifPort := common29122.Port(myPorts["CAPIF_PORT"])
490 kongDomain := myEnv["KONG_DOMAIN"]
491 kongProtocol := myEnv["KONG_PROTOCOL"]
492 kongIPv4 := common29122.Ipv4Addr(myEnv["KONG_IPV4"])
493 kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
494 kongControlPlanePort := common29122.Port(myPorts["KONG_CONTROL_PLANE_PORT"])
496 // Register ProviderManagement
497 providerManagerSwagger, err := provapi.GetSwagger()
499 log.Fatalf("error loading ProviderManagement swagger spec\n: %s", err)
502 providerManagerSwagger.Servers = nil
503 providerManager := providermanagement.NewProviderManager(capifProtocol, capifIPv4, capifPort)
505 var group *echo.Group
507 group = e.Group("/api-provider-management/v1")
508 group.Use(middleware.OapiRequestValidator(providerManagerSwagger))
509 provapi.RegisterHandlersWithBaseURL(e, providerManager, "/api-provider-management/v1")
511 publishServiceSwagger, err := publishapi.GetSwagger()
513 fmt.Fprintf(os.Stderr, "Error loading PublishService swagger spec\n: %s", err)
517 publishServiceSwagger.Servers = nil
519 ps := publishservice.NewPublishService(kongDomain, kongProtocol, kongIPv4, kongDataPlanePort, kongControlPlanePort, capifProtocol, capifIPv4, capifPort)
521 group = e.Group("/published-apis/v1")
522 group.Use(echomiddleware.Logger())
523 group.Use(middleware.OapiRequestValidator(publishServiceSwagger))
524 publishapi.RegisterHandlersWithBaseURL(e, ps, "/published-apis/v1")
526 invokerServiceSwagger, err := invokermanagementapi.GetSwagger()
528 fmt.Fprintf(os.Stderr, "Error loading InvokerManagement swagger spec\n: %s", err)
532 invokerServiceSwagger.Servers = nil
534 im := NewInvokerManager(capifProtocol, capifIPv4, capifPort)
536 group = e.Group("/api-invoker-management/v1")
537 group.Use(echomiddleware.Logger())
538 group.Use(middleware.OapiRequestValidator(invokerServiceSwagger))
539 invokermanagementapi.RegisterHandlersWithBaseURL(e, im, "api-invoker-management/v1")
544 func getServiceAPIDescription(aefId, apiName, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port) publishapi.ServiceAPIDescription {
546 var protocol publishapi.Protocol = "HTTP_1_1"
548 return publishapi.ServiceAPIDescription{
549 AefProfiles: &[]publishapi.AefProfile{
552 InterfaceDescriptions: &[]publishapi.InterfaceDescription{
554 Ipv4Addr: &testServiceIpv4,
555 Port: &testServicePort,
556 SecurityMethods: &[]publishapi.SecurityMethod{
561 DomainName: &domainName,
563 Versions: []publishapi.Version{
566 Resources: &[]publishapi.Resource{
568 CommType: "REQUEST_RESPONSE",
569 Operations: &[]publishapi.Operation{
572 ResourceName: "helloworld",
581 Description: &description,
585 func getInvoker(invokerInfo string) invokermanagementapi.APIInvokerEnrolmentDetails {
586 newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
587 ApiInvokerInformation: &invokerInfo,
588 NotificationDestination: "http://golang.cafe/",
589 OnboardingInformation: invokermanagementapi.OnboardingInformation{
590 ApiInvokerPublicKey: "key",