NONRTRIC-946: Fix Capifcore intersection panic
[nonrtric/plt/sme.git] / capifcore / internal / invokermanagement / invokermanagement_test.go
index 0b90de8..ae79c90 100644 (file)
@@ -3,7 +3,8 @@
 //     ========================LICENSE_START=================================
 //     O-RAN-SC
 //     %%
-//     Copyright (C) 2022: Nordix Foundation
+//     Copyright (C) 2022-2023: Nordix Foundation
+//     Copyright (C) 2024: OpenInfra Foundation Europe
 //     %%
 //     Licensed under the Apache License, Version 2.0 (the "License");
 //     you may not use this file except in compliance with the License.
@@ -23,17 +24,20 @@ import (
        "fmt"
        "net/http"
        "os"
-       "path"
        "strings"
        "testing"
+       "time"
 
+       "oransc.org/nonrtric/capifcore/internal/eventsapi"
        "oransc.org/nonrtric/capifcore/internal/invokermanagementapi"
+       "oransc.org/nonrtric/capifcore/internal/keycloak"
 
        "github.com/labstack/echo/v4"
 
        "oransc.org/nonrtric/capifcore/internal/common29122"
        "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
 
+       keycloackmocks "oransc.org/nonrtric/capifcore/internal/keycloak/mocks"
        "oransc.org/nonrtric/capifcore/internal/publishservice"
        publishmocks "oransc.org/nonrtric/capifcore/internal/publishservice/mocks"
 
@@ -41,6 +45,7 @@ import (
        "github.com/deepmap/oapi-codegen/pkg/testutil"
        echomiddleware "github.com/labstack/echo/v4/middleware"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
 )
 
 func TestOnboardInvoker(t *testing.T) {
@@ -54,11 +59,20 @@ func TestOnboardInvoker(t *testing.T) {
                        AefProfiles: &aefProfiles,
                },
        }
-       publishRegisterMock := publishmocks.PublishRegister{}
-       publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices)
-       invokerUnderTest, requestHandler := getEcho(&publishRegisterMock)
 
        invokerInfo := "invoker a"
+       wantedInvokerSecret := "onboarding_secret_" + strings.Replace(invokerInfo, " ", "_", 1)
+       var client keycloak.Client
+       client.Secret = &wantedInvokerSecret
+       publishRegisterMock := publishmocks.PublishRegister{}
+       publishRegisterMock.On("GetAllowedPublishedServices", mock.AnythingOfType("[]publishserviceapi.ServiceAPIDescription")).Return(publishedServices)
+
+       accessMgmMock := keycloackmocks.AccessManagement{}
+       accessMgmMock.On("AddClient", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil)
+       accessMgmMock.On("GetClientRepresentation", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&client, nil)
+
+       invokerUnderTest, eventChannel, requestHandler := getEcho(&publishRegisterMock, &accessMgmMock)
+
        newInvoker := getInvoker(invokerInfo)
 
        // Onboard a valid invoker
@@ -72,34 +86,50 @@ func TestOnboardInvoker(t *testing.T) {
        assert.Equal(t, wantedInvokerId, *resultInvoker.ApiInvokerId)
        assert.Equal(t, newInvoker.NotificationDestination, resultInvoker.NotificationDestination)
        assert.Equal(t, newInvoker.OnboardingInformation.ApiInvokerPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
-       wantedInvokerSecret := "onboarding_secret_" + strings.Replace(invokerInfo, " ", "_", 1)
+
        assert.Equal(t, wantedInvokerSecret, *resultInvoker.OnboardingInformation.OnboardingSecret)
        assert.Equal(t, "http://example.com/onboardedInvokers/"+*resultInvoker.ApiInvokerId, result.Recorder.Header().Get(echo.HeaderLocation))
        assert.True(t, invokerUnderTest.IsInvokerRegistered(wantedInvokerId))
        assert.True(t, invokerUnderTest.VerifyInvokerSecret(wantedInvokerId, wantedInvokerSecret))
-       publishRegisterMock.AssertCalled(t, "GetAllPublishedServices")
+
+       publishRegisterMock.AssertCalled(t, "GetAllowedPublishedServices", mock.AnythingOfType("[]publishserviceapi.ServiceAPIDescription"))
+
        assert.Equal(t, invokermanagementapi.APIList(publishedServices), *resultInvoker.ApiList)
+       if invokerEvent, timeout := waitForEvent(eventChannel, 1*time.Second); timeout {
+               assert.Fail(t, "No event sent")
+       } else {
+               assert.Equal(t, *resultInvoker.ApiInvokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
+               assert.Equal(t, eventsapi.CAPIFEventAPIINVOKERONBOARDED, invokerEvent.Events)
+       }
+
+       // Onboarding the same invoker should result in Forbidden
+       result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
+
+       assert.Equal(t, http.StatusForbidden, result.Code())
+       var problemDetails common29122.ProblemDetails
+       err = result.UnmarshalBodyToObject(&problemDetails)
+       assert.NoError(t, err, "error unmarshaling response")
+       assert.Equal(t, http.StatusForbidden, *problemDetails.Status)
+       assert.Contains(t, *problemDetails.Cause, "already onboarded")
 
        // Onboard an invoker missing required NotificationDestination, should get 400 with problem details
        invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
                OnboardingInformation: invokermanagementapi.OnboardingInformation{
-                       ApiInvokerPublicKey: "key",
+                       ApiInvokerPublicKey: "newKey",
                },
        }
        result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusBadRequest, result.Code())
-       var problemDetails common29122.ProblemDetails
        err = result.UnmarshalBodyToObject(&problemDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       badRequest := http.StatusBadRequest
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
        assert.Contains(t, *problemDetails.Cause, "missing")
        assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
 
        // Onboard an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
        invalidInvoker = invokermanagementapi.APIInvokerEnrolmentDetails{
-               NotificationDestination: "url",
+               NotificationDestination: "http://golang.cafe/",
        }
 
        result = testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(invalidInvoker).Go(t, requestHandler)
@@ -107,67 +137,88 @@ func TestOnboardInvoker(t *testing.T) {
        assert.Equal(t, http.StatusBadRequest, result.Code())
        err = result.UnmarshalBodyToObject(&problemDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
        assert.Contains(t, *problemDetails.Cause, "missing")
        assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
 }
 
 func TestDeleteInvoker(t *testing.T) {
-       publishRegisterMock := publishmocks.PublishRegister{}
-       publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
-       invokerUnderTest, requestHandler := getEcho(&publishRegisterMock)
+       invokerUnderTest, eventChannel, requestHandler := getEcho(nil, nil)
 
+       invokerId := "invokerId"
        newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
+               ApiInvokerId:            &invokerId,
                NotificationDestination: "url",
                OnboardingInformation: invokermanagementapi.OnboardingInformation{
                        ApiInvokerPublicKey: "key",
                },
        }
-
-       // Onboard an invoker
-       result := testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
-
-       invokerUrl := result.Recorder.Header().Get(echo.HeaderLocation)
-       assert.True(t, invokerUnderTest.IsInvokerRegistered(path.Base(invokerUrl)))
+       invokerUnderTest.onboardedInvokers[invokerId] = newInvoker
+       assert.True(t, invokerUnderTest.IsInvokerRegistered(invokerId))
 
        // Delete the invoker
-       result = testutil.NewRequest().Delete(invokerUrl).Go(t, requestHandler)
+       result := testutil.NewRequest().Delete("/onboardedInvokers/"+invokerId).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusNoContent, result.Code())
-       assert.False(t, invokerUnderTest.IsInvokerRegistered(path.Base(invokerUrl)))
+       assert.False(t, invokerUnderTest.IsInvokerRegistered(invokerId))
+       if invokerEvent, timeout := waitForEvent(eventChannel, 1*time.Second); timeout {
+               assert.Fail(t, "No event sent")
+       } else {
+               assert.Equal(t, invokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
+               assert.Equal(t, eventsapi.CAPIFEventAPIINVOKEROFFBOARDED, invokerEvent.Events)
+       }
+}
+
+func TestFailedUpdateInvoker(t *testing.T) {
+       publishRegisterMock := publishmocks.PublishRegister{}
+       publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
+       serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
+
+       invokerInfo := "invoker a"
+       invokerId := "api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1)
+
+       // Attempt to update with an invoker without the ApiInvokerId provided in the parameter body. We should get 400 with problem details.
+       invalidInvoker := getInvoker(invokerInfo)
+       serviceUnderTest.onboardedInvokers[invokerId] = invalidInvoker
+
+       result := testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
+       assert.Equal(t, http.StatusBadRequest, result.Code())
+
+       var problemDetails common29122.ProblemDetails
+       err := result.UnmarshalBodyToObject(&problemDetails)
+       assert.NoError(t, err, "error unmarshaling response")
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
+
+       assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
 }
 
 func TestUpdateInvoker(t *testing.T) {
        publishRegisterMock := publishmocks.PublishRegister{}
        publishRegisterMock.On("GetAllPublishedServices").Return([]publishserviceapi.ServiceAPIDescription{})
-       _, requestHandler := getEcho(&publishRegisterMock)
+       serviceUnderTest, _, requestHandler := getEcho(&publishRegisterMock, nil)
 
-       newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
-               NotificationDestination: "url",
+       invokerId := "invokerId"
+       invoker := invokermanagementapi.APIInvokerEnrolmentDetails{
+               ApiInvokerId:            &invokerId,
+               NotificationDestination: "http://golang.cafe/",
                OnboardingInformation: invokermanagementapi.OnboardingInformation{
                        ApiInvokerPublicKey: "key",
                },
        }
-
-       // Onboard an invoker
-       result := testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
-       var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
-       result.UnmarshalBodyToObject(&resultInvoker)
+       serviceUnderTest.onboardedInvokers[invokerId] = invoker
 
        // Update the invoker with valid invoker, should return 200 with updated invoker details
-       invokerId := resultInvoker.ApiInvokerId
-       invokerUrl := result.Recorder.Header().Get(echo.HeaderLocation)
-       resultInvoker.ApiList = nil
-       newNotifURL := "newUrl"
-       resultInvoker.NotificationDestination = common29122.Uri(newNotifURL)
+       newNotifURL := "http://golang.org/"
+       invoker.NotificationDestination = common29122.Uri(newNotifURL)
        newPublicKey := "newPublicKey"
-       resultInvoker.OnboardingInformation.ApiInvokerPublicKey = newPublicKey
-       result = testutil.NewRequest().Put(invokerUrl).WithJsonBody(resultInvoker).Go(t, requestHandler)
+       invoker.OnboardingInformation.ApiInvokerPublicKey = newPublicKey
+       result := testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invoker).Go(t, requestHandler)
 
+       var resultInvoker invokermanagementapi.APIInvokerEnrolmentDetails
        assert.Equal(t, http.StatusOK, result.Code())
        err := result.UnmarshalBodyToObject(&resultInvoker)
        assert.NoError(t, err, "error unmarshaling response")
-       assert.Equal(t, invokerId, resultInvoker.ApiInvokerId)
+       assert.Equal(t, invokerId, *resultInvoker.ApiInvokerId)
        assert.Equal(t, newNotifURL, string(resultInvoker.NotificationDestination))
        assert.Equal(t, newPublicKey, resultInvoker.OnboardingInformation.ApiInvokerPublicKey)
 
@@ -176,29 +227,28 @@ func TestUpdateInvoker(t *testing.T) {
                ApiInvokerPublicKey: "key",
        }
        invalidInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
-               ApiInvokerId:          invokerId,
+               ApiInvokerId:          &invokerId,
                OnboardingInformation: validOnboardingInfo,
        }
-       result = testutil.NewRequest().Put(invokerUrl).WithJsonBody(invalidInvoker).Go(t, requestHandler)
+       result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusBadRequest, result.Code())
        var problemDetails common29122.ProblemDetails
        err = result.UnmarshalBodyToObject(&problemDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       badRequest := http.StatusBadRequest
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
        assert.Contains(t, *problemDetails.Cause, "missing")
        assert.Contains(t, *problemDetails.Cause, "NotificationDestination")
 
        // Update with an invoker missing required OnboardingInformation.ApiInvokerPublicKey, should get 400 with problem details
-       invalidInvoker.NotificationDestination = "url"
+       invalidInvoker.NotificationDestination = "http://golang.org/"
        invalidInvoker.OnboardingInformation = invokermanagementapi.OnboardingInformation{}
-       result = testutil.NewRequest().Put(invokerUrl).WithJsonBody(invalidInvoker).Go(t, requestHandler)
+       result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusBadRequest, result.Code())
        err = result.UnmarshalBodyToObject(&problemDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
        assert.Contains(t, *problemDetails.Cause, "missing")
        assert.Contains(t, *problemDetails.Cause, "OnboardingInformation.ApiInvokerPublicKey")
 
@@ -206,25 +256,23 @@ func TestUpdateInvoker(t *testing.T) {
        invalidId := "1"
        invalidInvoker.ApiInvokerId = &invalidId
        invalidInvoker.OnboardingInformation = validOnboardingInfo
-       result = testutil.NewRequest().Put(invokerUrl).WithJsonBody(invalidInvoker).Go(t, requestHandler)
+       result = testutil.NewRequest().Put("/onboardedInvokers/"+invokerId).WithJsonBody(invalidInvoker).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusBadRequest, result.Code())
        err = result.UnmarshalBodyToObject(&problemDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       assert.Equal(t, &badRequest, problemDetails.Status)
-       assert.Contains(t, *problemDetails.Cause, "not matching")
-       assert.Contains(t, *problemDetails.Cause, "ApiInvokerId")
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
+       assert.Contains(t, *problemDetails.Cause, "APIInvokerEnrolmentDetails ApiInvokerId doesn't match path parameter")
 
-       // Update an invoker that has not been onboarded, shold get 404 with problem details
+       // Update an invoker that has not been onboarded, should get 404 with problem details
        missingId := "1"
-       newInvoker.ApiInvokerId = &missingId
-       result = testutil.NewRequest().Put("/onboardedInvokers/"+missingId).WithJsonBody(newInvoker).Go(t, requestHandler)
+       invoker.ApiInvokerId = &missingId
+       result = testutil.NewRequest().Put("/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusNotFound, result.Code())
        err = result.UnmarshalBodyToObject(&problemDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       notFound := http.StatusNotFound
-       assert.Equal(t, &notFound, problemDetails.Status)
+       assert.Equal(t, http.StatusNotFound, *problemDetails.Status)
        assert.Contains(t, *problemDetails.Cause, "not been onboarded")
        assert.Contains(t, *problemDetails.Cause, "invoker")
 }
@@ -250,20 +298,26 @@ func TestGetInvokerApiList(t *testing.T) {
        })
        publishRegisterMock := publishmocks.PublishRegister{}
        publishRegisterMock.On("GetAllPublishedServices").Return(apiList)
-       invokerUnderTest, requestHandler := getEcho(&publishRegisterMock)
+       invokerUnderTest, _, _ := getEcho(&publishRegisterMock, nil)
 
        invokerInfo := "invoker a"
        newInvoker := getInvoker(invokerInfo)
-       testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
-       newInvoker = getInvoker("invoker b")
-       testutil.NewRequest().Post("/onboardedInvokers").WithJsonBody(newInvoker).Go(t, requestHandler)
-
-       wantedApiList := invokerUnderTest.GetInvokerApiList("api_invoker_id_" + strings.Replace(invokerInfo, " ", "_", 1))
+       invokerAId := "api_invoker_id_" + strings.ReplaceAll(invokerInfo, " ", "_")
+       newInvoker.ApiInvokerId = &invokerAId
+       invokerUnderTest.onboardedInvokers[invokerAId] = newInvoker
+       invokerInfo = "invoker b"
+       newInvoker = getInvoker(invokerInfo)
+       invokerId := "api_invoker_id_" + strings.ReplaceAll(invokerInfo, " ", "_")
+       newInvoker.ApiInvokerId = &invokerId
+       invokerUnderTest.onboardedInvokers[invokerId] = newInvoker
+
+       wantedApiList := invokerUnderTest.GetInvokerApiList(invokerAId)
        assert.NotNil(t, wantedApiList)
+       assert.Len(t, *wantedApiList, 2)
        assert.Equal(t, apiId, *(*wantedApiList)[0].ApiId)
 }
 
-func getEcho(publishRegister publishservice.PublishRegister) (*InvokerManager, *echo.Echo) {
+func getEcho(publishRegister publishservice.PublishRegister, keycloakMgm keycloak.AccessManagement) (*InvokerManager, chan eventsapi.EventNotification, *echo.Echo) {
        swagger, err := invokermanagementapi.GetSwagger()
        if err != nil {
                fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
@@ -272,14 +326,15 @@ func getEcho(publishRegister publishservice.PublishRegister) (*InvokerManager, *
 
        swagger.Servers = nil
 
-       im := NewInvokerManager(publishRegister)
+       eventChannel := make(chan eventsapi.EventNotification)
+       im := NewInvokerManager(publishRegister, keycloakMgm, eventChannel)
 
        e := echo.New()
        e.Use(echomiddleware.Logger())
        e.Use(middleware.OapiRequestValidator(swagger))
 
        invokermanagementapi.RegisterHandlers(e, im)
-       return im, e
+       return im, eventChannel, e
 }
 
 func getAefProfile(aefId string) publishserviceapi.AefProfile {
@@ -300,7 +355,7 @@ func getAefProfile(aefId string) publishserviceapi.AefProfile {
 func getInvoker(invokerInfo string) invokermanagementapi.APIInvokerEnrolmentDetails {
        newInvoker := invokermanagementapi.APIInvokerEnrolmentDetails{
                ApiInvokerInformation:   &invokerInfo,
-               NotificationDestination: "url",
+               NotificationDestination: "http://golang.cafe/",
                OnboardingInformation: invokermanagementapi.OnboardingInformation{
                        ApiInvokerPublicKey: "key",
                },
@@ -308,3 +363,14 @@ func getInvoker(invokerInfo string) invokermanagementapi.APIInvokerEnrolmentDeta
        }
        return newInvoker
 }
+
+// waitForEvent waits for the channel to receive an event for the specified max timeout.
+// Returns true if waiting timed out.
+func waitForEvent(ch chan eventsapi.EventNotification, timeout time.Duration) (*eventsapi.EventNotification, bool) {
+       select {
+       case event := <-ch:
+               return &event, false // completed normally
+       case <-time.After(timeout):
+               return nil, true // timed out
+       }
+}