NONRTRIC-946: Servicemanager - add Kong data plane and control plane
[nonrtric/plt/sme.git] / servicemanager / internal / publishservice / publishservice_test.go
1 // -
2 //   ========================LICENSE_START=================================
3 //   O-RAN-SC
4 //   %%
5 //   Copyright (C) 2023-2024: OpenInfra Foundation Europe
6 //   %%
7 //   Licensed under the Apache License, Version 2.0 (the "License");
8 //   you may not use this file except in compliance with the License.
9 //   You may obtain a copy of the License at
10 //
11 //        http://www.apache.org/licenses/LICENSE-2.0
12 //
13 //   Unless required by applicable law or agreed to in writing, software
14 //   distributed under the License is distributed on an "AS IS" BASIS,
15 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 //   See the License for the specific language governing permissions and
17 //   limitations under the License.
18 //   ========================LICENSE_END===================================
19 //
20
21 package publishservice
22
23 import (
24         "fmt"
25         "net/http"
26         "net/http/httptest"
27         "net/url"
28         "os"
29         "strings"
30         "testing"
31
32         "github.com/deepmap/oapi-codegen/pkg/middleware"
33         "github.com/deepmap/oapi-codegen/pkg/testutil"
34         echo "github.com/labstack/echo/v4"
35         echomiddleware "github.com/labstack/echo/v4/middleware"
36         log "github.com/sirupsen/logrus"
37         "github.com/stretchr/testify/assert"
38
39         "oransc.org/nonrtric/servicemanager/internal/envreader"
40         "oransc.org/nonrtric/servicemanager/internal/kongclear"
41
42         "oransc.org/nonrtric/servicemanager/internal/common29122"
43         "oransc.org/nonrtric/servicemanager/internal/providermanagement"
44         provapi "oransc.org/nonrtric/servicemanager/internal/providermanagementapi"
45         publishapi "oransc.org/nonrtric/servicemanager/internal/publishserviceapi"
46
47         "oransc.org/nonrtric/capifcore"
48         mockKong "oransc.org/nonrtric/servicemanager/mockkong"
49 )
50
51 var (
52         eServiceManager      *echo.Echo
53         eCapifWeb            *echo.Echo
54         eKong                *echo.Echo
55         mockConfigReader     *envreader.MockConfigReader
56         serviceManagerServer *httptest.Server
57         capifServer          *httptest.Server
58         mockKongServer       *httptest.Server
59 )
60
61 func TestMain(m *testing.M) {
62         err := setupTest()
63         if err != nil {
64                 return
65         }
66
67         ret := m.Run()
68         if ret == 0 {
69                 teardown()
70         }
71         os.Exit(ret)
72 }
73
74 func setupTest() error {
75         // Start the mock Kong server
76         eKong = echo.New()
77         mockKong.RegisterHandlers(eKong)
78         mockKongServer = httptest.NewServer(eKong)
79
80         // Parse the server URL
81         parsedMockKongURL, err := url.Parse(mockKongServer.URL)
82         if err != nil {
83                 log.Fatalf("error parsing mock Kong URL: %v", err)
84                 return err
85         }
86
87         // Extract the host and port
88         mockKongHost := parsedMockKongURL.Hostname()
89         mockKongControlPlanePort := parsedMockKongURL.Port()
90
91         eCapifWeb = echo.New()
92         capifcore.RegisterHandlers(eCapifWeb, nil, nil)
93         capifServer = httptest.NewServer(eCapifWeb)
94
95         // Parse the server URL
96         parsedCapifURL, err := url.Parse(capifServer.URL)
97         if err != nil {
98                 log.Fatalf("error parsing mock Kong URL: %v", err)
99                 return err
100         }
101
102         // Extract the host and port
103         capifHost := parsedCapifURL.Hostname()
104         capifPort := parsedCapifURL.Port()
105
106         // Set up the mock config reader with the desired configuration for testing
107         mockConfigReader = &envreader.MockConfigReader{
108                 MockedConfig: map[string]string{
109                         "KONG_DOMAIN":             "kong",
110                         "KONG_PROTOCOL":           "http",
111                         "KONG_CONTROL_PLANE_IPV4": mockKongHost,
112                         "KONG_CONTROL_PLANE_PORT": mockKongControlPlanePort,
113                         "KONG_DATA_PLANE_IPV4":    "10.101.1.101",
114                         "KONG_DATA_PLANE_PORT":    "32080",
115                         "CAPIF_PROTOCOL":          "http",
116                         "CAPIF_IPV4":              capifHost,
117                         "CAPIF_PORT":              capifPort,
118                         "LOG_LEVEL":               "Info",
119                         "SERVICE_MANAGER_PORT":    "8095",
120                         "TEST_SERVICE_IPV4":       "10.101.1.101",
121                         "TEST_SERVICE_PORT":       "30951",
122                 },
123         }
124
125         // Use the mock implementation for testing
126         myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
127         if err != nil {
128                 log.Fatalf("error reading mock config: %v", err)
129         }
130
131         eServiceManager = echo.New()
132         err = registerHandlers(eServiceManager, myEnv, myPorts)
133         if err != nil {
134                 log.Fatal("registerHandlers fatal error on setupTest")
135                 return err
136         }
137         serviceManagerServer = httptest.NewServer(eServiceManager)
138         capifCleanUp()
139         return err
140 }
141
142 func getProvider() provapi.APIProviderEnrolmentDetails {
143         var (
144                 domainInfo  = "Kong"
145                 funcInfoAPF = "rApp Kong as APF"
146                 funcInfoAEF = "rApp Kong as AEF"
147         )
148
149         testFuncs := []provapi.APIProviderFunctionDetails{
150                 {
151                         ApiProvFuncInfo: &funcInfoAPF,
152                         ApiProvFuncRole: provapi.ApiProviderFuncRoleAPF,
153                         RegInfo: provapi.RegistrationInformation{
154                                 ApiProvPubKey: "APF-PublicKey",
155                         },
156                 },
157                 {
158                         ApiProvFuncInfo: &funcInfoAEF,
159                         ApiProvFuncRole: provapi.ApiProviderFuncRoleAEF,
160                         RegInfo: provapi.RegistrationInformation{
161                                 ApiProvPubKey: "AEF-PublicKey",
162                         },
163                 },
164         }
165         return provapi.APIProviderEnrolmentDetails{
166                 RegSec:         "sec",
167                 ApiProvDomInfo: &domainInfo,
168                 ApiProvFuncs:   &testFuncs,
169         }
170 }
171
172 func capifCleanUp() {
173         t := new(testing.T) // Create a new testing.T instance for capifCleanUp
174
175         // Delete the invoker
176         invokerInfo := "invoker a"
177         invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
178
179         result := testutil.NewRequest().Delete("/api-invoker-management/v1/onboardedInvokers/"+invokerId).Go(t, eServiceManager)
180         assert.Equal(t, http.StatusNoContent, result.Code())
181
182         // Delete the original published service
183         apfId := "APF_id_rApp_Kong_as_APF"
184         apiName := "apiName"
185         apiId := "api_id_" + apiName
186
187         result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
188         assert.Equal(t, http.StatusNoContent, result.Code())
189
190         // Delete the first published service
191         apfId = "APF_id_rApp_Kong_as_APF"
192         apiName = "apiName1"
193         apiId = "api_id_" + apiName
194
195         result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
196         assert.Equal(t, http.StatusNoContent, result.Code())
197
198         // Delete the second published service
199         apiName = "apiName2"
200         apiId = "api_id_" + apiName
201
202         result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
203         assert.Equal(t, http.StatusNoContent, result.Code())
204
205         // Delete the provider
206         domainID := "domain_id_Kong"
207         result = testutil.NewRequest().Delete("/api-provider-management/v1/registrations/"+domainID).Go(t, eServiceManager)
208         assert.Equal(t, http.StatusNoContent, result.Code())
209 }
210
211 func teardown() error {
212         log.Trace("entering teardown")
213
214         myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
215         if err != nil {
216                 log.Fatal("error loading environment file")
217                 return err
218         }
219
220         err = kongclear.KongClear(myEnv, myPorts)
221         if err != nil {
222                 log.Fatal("error clearing Kong on teardown")
223         }
224
225         mockKongServer.Close()
226         capifServer.Close()
227         serviceManagerServer.Close()
228
229         return nil
230 }
231
232 func TestPostUnpublishedServiceWithUnregisteredPublisher(t *testing.T) {
233         capifCleanUp()
234
235         apfId := "APF_id_rApp_Kong_as_APF"
236
237         // Check no services published
238         result := testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager)
239         assert.Equal(t, http.StatusNotFound, result.Code())
240
241         var resultError common29122.ProblemDetails
242         err := result.UnmarshalBodyToObject(&resultError)
243         assert.NoError(t, err, "error unmarshaling response")
244
245         assert.Contains(t, *resultError.Cause, apfId)
246         assert.Contains(t, *resultError.Cause, "api is only available for publishers")
247
248         aefId := "AEF_id_rApp_Kong_as_AEF"
249         namespace := "namespace"
250         repoName := "repoName"
251         chartName := "chartName"
252         releaseName := "releaseName"
253         description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
254
255         myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
256         assert.Nil(t, err, "error reading env file")
257
258         testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
259         testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
260
261         assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
262         assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
263
264         apiName := "apiName"
265         newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
266
267         // Attempt to publish a service for provider
268         result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
269         assert.Equal(t, http.StatusForbidden, result.Code())
270
271         // var resultError common29122.ProblemDetails
272         err = result.UnmarshalBodyToObject(&resultError)
273         assert.NoError(t, err, "error unmarshaling response")
274
275         assert.Contains(t, *resultError.Cause, apfId)
276         assert.Contains(t, *resultError.Cause, "Unable to publish the service due to api is only available for publishers")
277 }
278
279 func TestRegisterValidProvider(t *testing.T) {
280         newProvider := getProvider()
281
282         // Register a valid provider
283         result := testutil.NewRequest().Post("/api-provider-management/v1/registrations").WithJsonBody(newProvider).Go(t, eServiceManager)
284         assert.Equal(t, http.StatusCreated, result.Code())
285
286         var resultProvider provapi.APIProviderEnrolmentDetails
287         err := result.UnmarshalBodyToObject(&resultProvider)
288         assert.NoError(t, err, "error unmarshaling response")
289 }
290
291 func TestPublishUnpublishService(t *testing.T) {
292         apfId := "APF_id_rApp_Kong_as_APF"
293         apiName := "apiName"
294
295         myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
296         assert.Nil(t, err, "error reading env file")
297
298         testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
299         testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
300
301         assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
302         assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
303
304         // Check no services published
305         result := testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager)
306         assert.Equal(t, http.StatusOK, result.Code())
307
308         // Parse JSON from the response body
309         var resultServices []publishapi.ServiceAPIDescription
310         err = result.UnmarshalJsonToObject(&resultServices)
311         assert.NoError(t, err, "error unmarshaling response")
312
313         // Check if the parsed array is empty
314         assert.Zero(t, len(resultServices))
315
316         aefId := "AEF_id_rApp_Kong_as_AEF"
317         namespace := "namespace"
318         repoName := "repoName"
319         chartName := "chartName"
320         releaseName := "releaseName"
321         description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
322
323         newServiceDescription := getServiceAPIDescriptionMissingInterface(aefId, apiName, description)
324
325         // Publish a service for provider
326         result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
327         assert.Equal(t, http.StatusBadRequest, result.Code())
328
329         var resultError common29122.ProblemDetails
330         err = result.UnmarshalJsonToObject(&resultError)
331         assert.NoError(t, err, "error unmarshaling response")
332
333         assert.Contains(t, *resultError.Cause, "cannot read interfaceDescription")
334
335         newServiceDescription = getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
336
337         // Publish a service for provider
338         result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
339         assert.Equal(t, http.StatusCreated, result.Code())
340
341         if result.Code() != http.StatusCreated {
342                 log.Fatalf("failed to publish the service with HTTP result code %d", result.Code())
343                 return
344         }
345
346         var resultService publishapi.ServiceAPIDescription
347         err = result.UnmarshalJsonToObject(&resultService)
348         assert.NoError(t, err, "error unmarshaling response")
349         newApiId := "api_id_" + apiName
350         assert.Equal(t, newApiId, *resultService.ApiId)
351
352         assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation))
353
354         // Check that the service is published for the provider
355         result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
356         assert.Equal(t, http.StatusOK, result.Code())
357
358         err = result.UnmarshalJsonToObject(&resultService)
359         assert.NoError(t, err, "error unmarshaling response")
360         assert.Equal(t, newApiId, *resultService.ApiId)
361
362         aefProfile := (*resultService.AefProfiles)[0]
363         interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
364
365         resultServiceIpv4 := *interfaceDescription.Ipv4Addr
366         resultServicePort := *interfaceDescription.Port
367
368         kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"])
369         kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
370
371         assert.NotEmpty(t, kongDataPlaneIPv4, "KONG_DATA_PLANE_IPV4 is required in .env file for unit testing")
372         assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing")
373
374         assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4)
375         assert.Equal(t, kongDataPlanePort, resultServicePort)
376
377         // Publish the same service again should result in Forbidden
378         newServiceDescription.ApiId = &newApiId
379         result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
380         assert.Equal(t, http.StatusForbidden, result.Code())
381
382         err = result.UnmarshalBodyToObject(&resultError)
383         assert.NoError(t, err, "error unmarshaling response")
384         assert.Contains(t, *resultError.Cause, "already published")
385         assert.Equal(t, http.StatusForbidden, *resultError.Status)
386
387         // Delete the service
388         result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
389         assert.Equal(t, http.StatusNoContent, result.Code())
390
391         // Check no services published
392         result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager)
393         assert.Equal(t, http.StatusOK, result.Code())
394
395         // Parse JSON from the response body
396         err = result.UnmarshalJsonToObject(&resultServices)
397         assert.NoError(t, err, "error unmarshaling response")
398
399         // Check if the parsed array is empty
400         assert.Zero(t, len(resultServices))
401         capifCleanUp()
402 }
403
404 func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string]int) (err error) {
405         capifProtocol := myEnv["CAPIF_PROTOCOL"]
406         capifIPv4 := common29122.Ipv4Addr(myEnv["CAPIF_IPV4"])
407         capifPort := common29122.Port(myPorts["CAPIF_PORT"])
408         kongDomain := myEnv["KONG_DOMAIN"]
409         kongProtocol := myEnv["KONG_PROTOCOL"]
410         kongControlPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_CONTROL_PLANE_IPV4"])
411         kongControlPlanePort := common29122.Port(myPorts["KONG_CONTROL_PLANE_PORT"])
412         kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"])
413         kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
414
415         // Register ProviderManagement
416         providerManagerSwagger, err := provapi.GetSwagger()
417         if err != nil {
418                 log.Fatalf("error loading ProviderManagement swagger spec\n: %s", err)
419                 return err
420         }
421         providerManagerSwagger.Servers = nil
422         providerManager := providermanagement.NewProviderManager(capifProtocol, capifIPv4, capifPort)
423
424         var group *echo.Group
425
426         group = e.Group("/api-provider-management/v1")
427         group.Use(middleware.OapiRequestValidator(providerManagerSwagger))
428         provapi.RegisterHandlersWithBaseURL(e, providerManager, "/api-provider-management/v1")
429
430         publishServiceSwagger, err := publishapi.GetSwagger()
431         if err != nil {
432                 fmt.Fprintf(os.Stderr, "Error loading PublishService swagger spec\n: %s", err)
433                 return err
434         }
435
436         publishServiceSwagger.Servers = nil
437
438         ps := NewPublishService(
439                 kongDomain, kongProtocol,
440                 kongControlPlaneIPv4, kongControlPlanePort,
441                 kongDataPlaneIPv4, kongDataPlanePort,
442                 capifProtocol, capifIPv4, capifPort)
443
444         group = e.Group("/published-apis/v1")
445         group.Use(echomiddleware.Logger())
446         group.Use(middleware.OapiRequestValidator(publishServiceSwagger))
447         publishapi.RegisterHandlersWithBaseURL(e, ps, "/published-apis/v1")
448
449         return err
450 }
451
452 func getServiceAPIDescription(aefId, apiName, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port) publishapi.ServiceAPIDescription {
453         domainName := "Kong"
454         var protocol publishapi.Protocol = "HTTP_1_1"
455
456         return publishapi.ServiceAPIDescription{
457                 AefProfiles: &[]publishapi.AefProfile{
458                         {
459                                 AefId: aefId,
460                                 InterfaceDescriptions: &[]publishapi.InterfaceDescription{
461                                         {
462                                                 Ipv4Addr: &testServiceIpv4,
463                                                 Port:     &testServicePort,
464                                                 SecurityMethods: &[]publishapi.SecurityMethod{
465                                                         "PKI",
466                                                 },
467                                         },
468                                 },
469                                 DomainName: &domainName,
470                                 Protocol:   &protocol,
471                                 Versions: []publishapi.Version{
472                                         {
473                                                 ApiVersion: "v1",
474                                                 Resources: &[]publishapi.Resource{
475                                                         {
476                                                                 CommType: "REQUEST_RESPONSE",
477                                                                 Operations: &[]publishapi.Operation{
478                                                                         "GET",
479                                                                 },
480                                                                 ResourceName: "helloworld",
481                                                                 Uri:          "/helloworld",
482                                                         },
483                                                 },
484                                         },
485                                 },
486                         },
487                 },
488                 ApiName:     apiName,
489                 Description: &description,
490         }
491 }
492
493 func getServiceAPIDescriptionMissingInterface(aefId, apiName, description string) publishapi.ServiceAPIDescription {
494         domainName := "Kong"
495         var protocol publishapi.Protocol = "HTTP_1_1"
496
497         return publishapi.ServiceAPIDescription{
498                 AefProfiles: &[]publishapi.AefProfile{
499                         {
500                                 AefId:      aefId,
501                                 DomainName: &domainName,
502                                 Protocol:   &protocol,
503                                 Versions: []publishapi.Version{
504                                         {
505                                                 ApiVersion: "v1",
506                                                 Resources: &[]publishapi.Resource{
507                                                         {
508                                                                 CommType: "REQUEST_RESPONSE",
509                                                                 Operations: &[]publishapi.Operation{
510                                                                         "GET",
511                                                                 },
512                                                                 ResourceName: "helloworld",
513                                                                 Uri:          "/helloworld",
514                                                         },
515                                                 },
516                                         },
517                                 },
518                         },
519                 },
520                 ApiName:     apiName,
521                 Description: &description,
522         }
523 }