Updates for G Maintenance release 28/10428/2 1.0.1
authorychacon <yennifer.chacon@est.tech>
Thu, 9 Feb 2023 09:36:52 +0000 (10:36 +0100)
committerychacon <yennifer.chacon@est.tech>
Thu, 9 Feb 2023 11:32:26 +0000 (12:32 +0100)
    Refactor check earlier registration
    Generate new mock for PublishRegister
    Added typeupdate and typeaccess
    Refactor check earlier registration
    Add check for same invoker onboarded
    Add check for same service publication
    Add check for same provider registration
    Add licence
    Add support for https
    Type validation in publish service
    Add validation to event service
    Merge "Improve validation for invoker management"
    Improve validation for invoker management
    Improve validation for provider management
    Refactor invoker management
    Cleanup provider validation test
    Refactor providermaagement
    test new functionality to update and delete profiles
    Improve locking
    Refactored put in publish service
    Improve locking
    Improve locking/unlocking
    Add .readthedocs.yaml
    Update release notes and step version

Issue-ID: NONRTRIC-838
Signed-off-by: ychacon <yennifer.chacon@est.tech>
Change-Id: Ib86b21632be4754e594873872a5207e006988e96

30 files changed:
capifcore/README.md
capifcore/certs/cert.pem [new file with mode: 0644]
capifcore/certs/key.pem [new file with mode: 0644]
capifcore/internal/eventsapi/typevalidation.go [new file with mode: 0644]
capifcore/internal/eventsapi/typevalidation_test.go [new file with mode: 0644]
capifcore/internal/eventservice/eventservice.go
capifcore/internal/eventservice/eventservice_test.go
capifcore/internal/invokermanagement/invokermanagement.go
capifcore/internal/invokermanagement/invokermanagement_test.go
capifcore/internal/invokermanagementapi/typeupdate.go [new file with mode: 0644]
capifcore/internal/invokermanagementapi/typeupdate_test.go [new file with mode: 0644]
capifcore/internal/invokermanagementapi/typevalidation.go [new file with mode: 0644]
capifcore/internal/invokermanagementapi/typevalidation_test.go [new file with mode: 0644]
capifcore/internal/providermanagement/providermanagement.go
capifcore/internal/providermanagement/providermanagement_test.go
capifcore/internal/providermanagementapi/typeaccess.go [new file with mode: 0644]
capifcore/internal/providermanagementapi/typeaccess_test.go [new file with mode: 0644]
capifcore/internal/providermanagementapi/typeupdate.go [new file with mode: 0644]
capifcore/internal/providermanagementapi/typeupdate_test.go [new file with mode: 0644]
capifcore/internal/providermanagementapi/typevalidation.go [new file with mode: 0644]
capifcore/internal/providermanagementapi/typevalidation_test.go [new file with mode: 0644]
capifcore/internal/publishservice/mocks/PublishRegister.go
capifcore/internal/publishservice/publishservice.go
capifcore/internal/publishservice/publishservice_test.go
capifcore/internal/publishserviceapi/typeaccess.go [new file with mode: 0644]
capifcore/internal/publishserviceapi/typeupdate.go [new file with mode: 0644]
capifcore/internal/publishserviceapi/typevalidation.go [new file with mode: 0644]
capifcore/internal/publishserviceapi/typevalidation_test.go [new file with mode: 0644]
capifcore/main.go
capifcore/main_test.go

index faa18a6..1f70c6e 100644 (file)
@@ -94,6 +94,6 @@ The application can also be built as a Docker image, by using the following comm
 
 To run the Core Function from the command line, run the following commands from this folder. For the parameter `chartMuseumUrl`, if it is not provided CAPIF Core will not do any Helm integration, i.e. try to start any Halm chart when publishing a service.
 
-    ./capifcore [-port <port (default 8090)>] [-chartMuseumUrl <URL to ChartMuseum>] [-repoName <Helm repo name (default capifcore)>] [-loglevel <log level (default Info)>]
+    ./capifcore [-port <port (default 8090)>] [-secPort <Secure port (default 4433)>] [-chartMuseumUrl <URL to ChartMuseum>] [-repoName <Helm repo name (default capifcore)>] [-loglevel <log level (default Info)>] [-certPath <Path to certificate>] [-keyPath <Path to private key>]
 
 To run CAPIF Core as a K8s pod together with ChartMuseum, start and stop scripts are provided. The pod configurations are provided in the `configs` folder. CAPIF Core is then available on port `31570`.
diff --git a/capifcore/certs/cert.pem b/capifcore/certs/cert.pem
new file mode 100644 (file)
index 0000000..e6037cb
--- /dev/null
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICMzCCAZygAwIBAgIRAI4ZifW8kkZA8erUGGlExBIwDQYJKoZIhvcNAQELBQAw
+EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
+MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEAvjU9/c+2dpoRRMnJPMh9eGAWA0cfg2h+AVotSVCqgJ+Hlr91BWCE0dDV
+nIlNGVXZSkxI4rRTI3DZi8wdEWNeiPBIQDbUpNDofCZ/AeAMfzhMb3cyMMZcZMG6
+Zx0aXvEdZhAmJjBUAT1+XrIAegLQvhN2g9awcaWVkuxoawG2HF0CAwEAAaOBhjCB
+gzAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQUuhJx8jwPJYB3Zg2Aaa4ZsVZ78v8wLAYDVR0RBCUw
+I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEB
+CwUAA4GBAKHALs8ifFaoVQw0GaCmIQt/yS9eWcssGGJHmAMyXTn78wsjnTdySDjv
+ZG7naV3uFs1ffA8eci5p1Hjzt8JFFGfLgHaoqnZW84+giwGI0RJKLz1dwnSsoHBz
+VKxIPMRm2xkQTiOMWX5YlbhiQf5rbx2OEaOqscM2H1DEwXSXFtjd
+-----END CERTIFICATE-----
diff --git a/capifcore/certs/key.pem b/capifcore/certs/key.pem
new file mode 100644 (file)
index 0000000..95de966
--- /dev/null
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL41Pf3PtnaaEUTJ
+yTzIfXhgFgNHH4NofgFaLUlQqoCfh5a/dQVghNHQ1ZyJTRlV2UpMSOK0UyNw2YvM
+HRFjXojwSEA21KTQ6HwmfwHgDH84TG93MjDGXGTBumcdGl7xHWYQJiYwVAE9fl6y
+AHoC0L4TdoPWsHGllZLsaGsBthxdAgMBAAECgYEAklky5mwAT0cBzHSZ4qu8Znc/
+2KvLoncupGm2+HcZiTe1wpZzOnzmFO3ivbui18CHHLSPS+dFJLq6l+an4u4bGFu7
+HrbZPwPqrlPHt/sSrPlhk7J/bLqwdhIGgHZje5XYZUqobhRdRR505Lqz20eEZeax
+zzgIa2v7uiYmh/COQsECQQDr92Refg3w32QEvtmnumcq7Q3Rdy/8ol8kqP/LeaV6
+rYHV23Dj5qV3HXJpL7DOKNOkzYnfZ7tN7Ur0p5xANVwRAkEAzltQl6eCiVWtYO6G
+cTf5LLBYuhu7KJD2LCFwWeNdAF652yczunIc8K8UgVkuPPVMzP/fRxvv7+vfo6Lx
+9p73jQJALRBeFr20I+BF1bItFx8+PLBxByPgAjtwOCweTdm5hKhGN3VlJeESkKEL
+DJOTDIw3fy3RutywpL1Ap2CrMof+QQJAcjqOFET/t3Ib9YpUFZw8bIZ5txvesIf+
+HVOtU7TOKIRHMY8zzUOZzYm9OhTZyZioGNqTCFPor9DMDVMHydMZiQJBAN0Cd3SO
+RyyC7drmNy7oCdZe+WSKUHvgoE/J8y91AK7FREiPMgEEQSeOe7wPVzHuzDfWGRSu
+0CYjIAUsES7Oizc=
+-----END PRIVATE KEY-----
diff --git a/capifcore/internal/eventsapi/typevalidation.go b/capifcore/internal/eventsapi/typevalidation.go
new file mode 100644 (file)
index 0000000..5477706
--- /dev/null
@@ -0,0 +1,70 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package eventsapi
+
+import (
+       "errors"
+       "fmt"
+       "net/url"
+       "strings"
+)
+
+func (es EventSubscription) Validate() error {
+       if len(es.Events) == 0 {
+               return errors.New("required attribute EventSubscription:events must contain at least one element")
+       }
+
+       for _, event := range es.Events {
+               if err := validateEvent(event); err != nil {
+                       return errors.New("EventSubscription events contains invalid event")
+               }
+       }
+
+       if len(strings.TrimSpace(string(es.NotificationDestination))) == 0 {
+               return errors.New("EventSubscription missing required notificationDestination")
+       }
+       if _, err := url.ParseRequestURI(string(es.NotificationDestination)); err != nil {
+               return fmt.Errorf("APIInvokerEnrolmentDetails has invalid notificationDestination, err=%s", err)
+       }
+
+       return nil
+}
+
+func validateEvent(event CAPIFEvent) error {
+       switch event {
+       case CAPIFEventACCESSCONTROLPOLICYUNAVAILABLE:
+       case CAPIFEventACCESSCONTROLPOLICYUPDATE:
+       case CAPIFEventAPIINVOKERAUTHORIZATIONREVOKED:
+       case CAPIFEventAPIINVOKEROFFBOARDED:
+       case CAPIFEventAPIINVOKERONBOARDED:
+       case CAPIFEventAPIINVOKERUPDATED:
+       case CAPIFEventAPITOPOLOGYHIDINGCREATED:
+       case CAPIFEventAPITOPOLOGYHIDINGREVOKED:
+       case CAPIFEventSERVICEAPIAVAILABLE:
+       case CAPIFEventSERVICEAPIINVOCATIONFAILURE:
+       case CAPIFEventSERVICEAPIINVOCATIONSUCCESS:
+       case CAPIFEventSERVICEAPIUNAVAILABLE:
+       case CAPIFEventSERVICEAPIUPDATE:
+       default:
+               return errors.New("wrong event type")
+       }
+       return nil
+}
diff --git a/capifcore/internal/eventsapi/typevalidation_test.go b/capifcore/internal/eventsapi/typevalidation_test.go
new file mode 100644 (file)
index 0000000..df57897
--- /dev/null
@@ -0,0 +1,61 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package eventsapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestValidateEventSubscription(t *testing.T) {
+       subUnderTest := EventSubscription{}
+
+       err := subUnderTest.Validate()
+
+       assert.NotNil(t, err)
+       assert.Contains(t, err.Error(), "required")
+       assert.Contains(t, err.Error(), "events")
+
+       var invalidEventType CAPIFEvent = "invalid"
+       subUnderTest.Events = []CAPIFEvent{invalidEventType}
+       err = subUnderTest.Validate()
+       assert.NotNil(t, err)
+       assert.Contains(t, err.Error(), "invalid")
+       assert.Contains(t, err.Error(), "events")
+
+       subUnderTest.Events = []CAPIFEvent{CAPIFEventAPIINVOKERONBOARDED}
+       err = subUnderTest.Validate()
+       assert.NotNil(t, err)
+       assert.Contains(t, err.Error(), "missing")
+       assert.Contains(t, err.Error(), "notificationDestination")
+
+       subUnderTest.NotificationDestination = "invalid dest"
+       err = subUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "invalid")
+               assert.Contains(t, err.Error(), "notificationDestination")
+       }
+
+       subUnderTest.NotificationDestination = "http://golang.cafe/"
+       err = subUnderTest.Validate()
+       assert.Nil(t, err)
+}
index 0f7a8e8..1d63a45 100644 (file)
@@ -74,6 +74,11 @@ func (es *EventService) PostSubscriberIdSubscriptions(ctx echo.Context, subscrib
        if err != nil {
                return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
+
+       if err := newSubscription.Validate(); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
+       }
+
        uri := ctx.Request().Host + ctx.Request().URL.String()
        subId := es.getSubscriptionId(subscriberId)
        es.addSubscription(subId, newSubscription)
@@ -88,18 +93,22 @@ func (es *EventService) PostSubscriberIdSubscriptions(ctx echo.Context, subscrib
 }
 
 func (es *EventService) DeleteSubscriberIdSubscriptionsSubscriptionId(ctx echo.Context, subscriberId string, subscriptionId string) error {
-       es.lock.Lock()
-       defer es.lock.Unlock()
 
        log.Debug(es.subscriptions)
        if _, ok := es.subscriptions[subscriptionId]; ok {
-               log.Debug("Deleting subscription", subscriptionId)
-               delete(es.subscriptions, subscriptionId)
+               es.deleteSubscription(subscriptionId)
        }
 
        return ctx.NoContent(http.StatusNoContent)
 }
 
+func (es *EventService) deleteSubscription(subscriptionId string) {
+       log.Debug("Deleting subscription", subscriptionId)
+       es.lock.Lock()
+       defer es.lock.Unlock()
+       delete(es.subscriptions, subscriptionId)
+}
+
 func getEventSubscriptionFromRequest(ctx echo.Context) (eventsapi.EventSubscription, error) {
        var subscription eventsapi.EventSubscription
        err := ctx.Bind(&subscription)
@@ -162,8 +171,9 @@ func matchesFilters(eventIds *[]string, filters []eventsapi.CAPIFEventFilter, ge
                filterIds := getIds(filter)
                if filterIds == nil || len(*filterIds) == 0 {
                        return matchesFilters(eventIds, filters[1:], getIds)
+               } else {
+                       return slices.Contains(*getIds(filter), id) && matchesFilters(eventIds, filters[1:], getIds)
                }
-               return slices.Contains(*getIds(filter), id) && matchesFilters(eventIds, filters[1:], getIds)
        }
        return true
 }
@@ -211,8 +221,8 @@ func (es *EventService) getSubscriptionId(subscriberId string) string {
 
 func (es *EventService) addSubscription(subId string, subscription eventsapi.EventSubscription) {
        es.lock.Lock()
+       defer es.lock.Unlock()
        es.subscriptions[subId] = subscription
-       es.lock.Unlock()
 }
 
 func (es *EventService) getSubscription(subId string) *eventsapi.EventSubscription {
index a328e57..d0b646b 100644 (file)
@@ -24,7 +24,7 @@ import (
        "bytes"
        "encoding/json"
        "fmt"
-       "io/ioutil"
+       "io"
        "net/http"
        "os"
        "path"
@@ -48,7 +48,7 @@ func TestRegisterSubscriptions(t *testing.T) {
                Events: []eventsapi.CAPIFEvent{
                        eventsapi.CAPIFEventSERVICEAPIAVAILABLE,
                },
-               NotificationDestination: common29122.Uri("notificationUrl"),
+               NotificationDestination: common29122.Uri("http://golang.cafe/"),
        }
        serviceUnderTest, requestHandler := getEcho(nil)
        subscriberId := "subscriberId"
@@ -77,6 +77,27 @@ func TestRegisterSubscriptions(t *testing.T) {
        assert.Equal(t, subscription2, *registeredSub2)
 }
 
+func TestRegisterInvalidSubscription(t *testing.T) {
+       subscription1 := eventsapi.EventSubscription{
+               Events: []eventsapi.CAPIFEvent{eventsapi.CAPIFEventACCESSCONTROLPOLICYUNAVAILABLE},
+       }
+       serviceUnderTest, requestHandler := getEcho(nil)
+       subscriberId := "subscriberId"
+
+       result := testutil.NewRequest().Post("/"+subscriberId+"/subscriptions").WithJsonBody(subscription1).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.Contains(t, *problemDetails.Cause, "missing")
+       assert.Contains(t, *problemDetails.Cause, "notificationDestination")
+       subscriptionId := path.Base(result.Recorder.Header().Get(echo.HeaderLocation))
+       registeredSub := serviceUnderTest.getSubscription(subscriptionId)
+       assert.Nil(t, registeredSub)
+}
+
 func TestDeregisterSubscription(t *testing.T) {
        subscription := eventsapi.EventSubscription{
                Events: []eventsapi.CAPIFEvent{
@@ -113,7 +134,7 @@ func TestSendEvent(t *testing.T) {
                        wg.Done()
                        return &http.Response{
                                StatusCode: 200,
-                               Body:       ioutil.NopCloser(bytes.NewBufferString(`OK`)),
+                               Body:       io.NopCloser(bytes.NewBufferString(`OK`)),
                                Header:     make(http.Header), // Must be set to non-nil value or it panics
                        }
                }
@@ -143,7 +164,7 @@ func TestSendEvent(t *testing.T) {
        }()
 
        if waitTimeout(&wg, 1*time.Second) {
-               t.Error("Not all calls to server were made")
+               t.Error("No event notification was sent")
                t.Fail()
        }
 }
index ecf8de5..43bdc02 100644 (file)
 package invokermanagement
 
 import (
+       "fmt"
        "net/http"
        "path"
-       "strconv"
-       "strings"
        "sync"
 
        "oransc.org/nonrtric/capifcore/internal/eventsapi"
-       publishapi "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
 
        "oransc.org/nonrtric/capifcore/internal/common29122"
        invokerapi "oransc.org/nonrtric/capifcore/internal/invokermanagementapi"
@@ -89,11 +87,11 @@ func (im *InvokerManager) VerifyInvokerSecret(invokerId, secret string) bool {
 }
 
 func (im *InvokerManager) GetInvokerApiList(invokerId string) *invokerapi.APIList {
+       var apiList invokerapi.APIList = im.publishRegister.GetAllPublishedServices()
+       im.lock.Lock()
+       defer im.lock.Unlock()
        invoker, ok := im.onboardedInvokers[invokerId]
        if ok {
-               var apiList invokerapi.APIList = im.publishRegister.GetAllPublishedServices()
-               im.lock.Lock()
-               defer im.lock.Unlock()
                invoker.ApiList = &apiList
                return &apiList
        }
@@ -103,37 +101,26 @@ func (im *InvokerManager) GetInvokerApiList(invokerId string) *invokerapi.APILis
 // Creates a new individual API Invoker profile.
 func (im *InvokerManager) PostOnboardedInvokers(ctx echo.Context) error {
        var newInvoker invokerapi.APIInvokerEnrolmentDetails
-       err := ctx.Bind(&newInvoker)
-       if err != nil {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invalid format for invoker")
+       errMsg := "Unable to onboard invoker due to %s"
+       if err := ctx.Bind(&newInvoker); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for invoker"))
        }
 
-       shouldReturn, coreError := im.validateInvoker(newInvoker, ctx)
-       if shouldReturn {
-               return coreError
+       if err := im.isInvokerOnboarded(newInvoker); err != nil {
+               return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errMsg, err))
        }
 
-       im.lock.Lock()
-       defer im.lock.Unlock()
-
-       newInvoker.ApiInvokerId = im.getId(newInvoker.ApiInvokerInformation)
-       onboardingSecret := "onboarding_secret_"
-       if newInvoker.ApiInvokerInformation != nil {
-               onboardingSecret = onboardingSecret + strings.ReplaceAll(*newInvoker.ApiInvokerInformation, " ", "_")
-       } else {
-               onboardingSecret = onboardingSecret + *newInvoker.ApiInvokerId
+       if err := im.validateInvoker(newInvoker, ctx); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
-       newInvoker.OnboardingInformation.OnboardingSecret = &onboardingSecret
 
-       var apiList invokerapi.APIList = im.publishRegister.GetAllPublishedServices()
-       newInvoker.ApiList = &apiList
+       im.prepareNewInvoker(&newInvoker)
 
-       im.onboardedInvokers[*newInvoker.ApiInvokerId] = newInvoker
        go im.sendEvent(*newInvoker.ApiInvokerId, eventsapi.CAPIFEventAPIINVOKERONBOARDED)
 
        uri := ctx.Request().Host + ctx.Request().URL.String()
        ctx.Response().Header().Set(echo.HeaderLocation, ctx.Scheme()+`://`+path.Join(uri, *newInvoker.ApiInvokerId))
-       err = ctx.JSON(http.StatusCreated, newInvoker)
+       err := ctx.JSON(http.StatusCreated, newInvoker)
        if err != nil {
                // Something really bad happened, tell Echo that our handler failed
                return err
@@ -142,44 +129,67 @@ func (im *InvokerManager) PostOnboardedInvokers(ctx echo.Context) error {
        return nil
 }
 
-// Deletes an individual API Invoker.
-func (im *InvokerManager) DeleteOnboardedInvokersOnboardingId(ctx echo.Context, onboardingId string) error {
+func (im *InvokerManager) isInvokerOnboarded(newInvoker invokerapi.APIInvokerEnrolmentDetails) error {
+       for _, invoker := range im.onboardedInvokers {
+               if err := invoker.ValidateAlreadyOnboarded(newInvoker); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func (im *InvokerManager) prepareNewInvoker(newInvoker *invokerapi.APIInvokerEnrolmentDetails) {
+       var apiList invokerapi.APIList = im.publishRegister.GetAllPublishedServices()
+       newInvoker.ApiList = &apiList
+
        im.lock.Lock()
        defer im.lock.Unlock()
 
-       delete(im.onboardedInvokers, onboardingId)
+       newInvoker.PrepareNewInvoker()
+
+       im.onboardedInvokers[*newInvoker.ApiInvokerId] = *newInvoker
+}
+
+// Deletes an individual API Invoker.
+func (im *InvokerManager) DeleteOnboardedInvokersOnboardingId(ctx echo.Context, onboardingId string) error {
+       if _, ok := im.onboardedInvokers[onboardingId]; ok {
+               im.deleteInvoker(onboardingId)
+       }
+
        go im.sendEvent(onboardingId, eventsapi.CAPIFEventAPIINVOKEROFFBOARDED)
 
        return ctx.NoContent(http.StatusNoContent)
 }
 
+func (im *InvokerManager) deleteInvoker(onboardingId string) {
+       im.lock.Lock()
+       defer im.lock.Unlock()
+       delete(im.onboardedInvokers, onboardingId)
+}
+
 // Updates an individual API invoker details.
 func (im *InvokerManager) PutOnboardedInvokersOnboardingId(ctx echo.Context, onboardingId string) error {
        var invoker invokerapi.APIInvokerEnrolmentDetails
-       err := ctx.Bind(&invoker)
-       if err != nil {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invalid format for invoker")
+       errMsg := "Unable to update invoker due to %s"
+       if err := ctx.Bind(&invoker); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for invoker"))
        }
 
        if onboardingId != *invoker.ApiInvokerId {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invoker ApiInvokerId not matching")
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "ApiInvokerId not matching"))
        }
 
-       shouldReturn, coreError := im.validateInvoker(invoker, ctx)
-       if shouldReturn {
-               return coreError
+       if err := im.validateInvoker(invoker, ctx); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
 
-       im.lock.Lock()
-       defer im.lock.Unlock()
-
        if _, ok := im.onboardedInvokers[onboardingId]; ok {
-               im.onboardedInvokers[*invoker.ApiInvokerId] = invoker
+               im.updateInvoker(invoker)
        } else {
                return sendCoreError(ctx, http.StatusNotFound, "The invoker to update has not been onboarded")
        }
 
-       err = ctx.JSON(http.StatusOK, invoker)
+       err := ctx.JSON(http.StatusOK, invoker)
        if err != nil {
                // Something really bad happened, tell Echo that our handler failed
                return err
@@ -188,42 +198,22 @@ func (im *InvokerManager) PutOnboardedInvokersOnboardingId(ctx echo.Context, onb
        return nil
 }
 
-func (im *InvokerManager) ModifyIndApiInvokeEnrolment(ctx echo.Context, onboardingId string) error {
-       return ctx.NoContent(http.StatusNotImplemented)
+func (im *InvokerManager) updateInvoker(invoker invokerapi.APIInvokerEnrolmentDetails) {
+       im.lock.Lock()
+       defer im.lock.Unlock()
+       im.onboardedInvokers[*invoker.ApiInvokerId] = invoker
 }
 
-func (im *InvokerManager) validateInvoker(invoker invokerapi.APIInvokerEnrolmentDetails, ctx echo.Context) (bool, error) {
-       if invoker.NotificationDestination == "" {
-               return true, sendCoreError(ctx, http.StatusBadRequest, "Invoker missing required NotificationDestination")
-       }
-
-       if invoker.OnboardingInformation.ApiInvokerPublicKey == "" {
-               return true, sendCoreError(ctx, http.StatusBadRequest, "Invoker missing required OnboardingInformation.ApiInvokerPublicKey")
-       }
-
-       if !im.areAPIsPublished(invoker.ApiList) {
-               return true, sendCoreError(ctx, http.StatusBadRequest, "Some APIs needed by invoker are not registered")
-       }
-
-       return false, nil
+func (im *InvokerManager) ModifyIndApiInvokeEnrolment(ctx echo.Context, onboardingId string) error {
+       return ctx.NoContent(http.StatusNotImplemented)
 }
 
-func (im *InvokerManager) areAPIsPublished(apis *invokerapi.APIList) bool {
-       if apis == nil {
-               return true
+func (im *InvokerManager) validateInvoker(invoker invokerapi.APIInvokerEnrolmentDetails, ctx echo.Context) error {
+       if err := invoker.Validate(); err != nil {
+               return err
        }
-       return im.publishRegister.AreAPIsPublished((*[]publishapi.ServiceAPIDescription)(apis))
-}
 
-func (im *InvokerManager) getId(invokerInfo *string) *string {
-       idAsString := "api_invoker_id_"
-       if invokerInfo != nil {
-               idAsString = idAsString + strings.ReplaceAll(*invokerInfo, " ", "_")
-       } else {
-               idAsString = idAsString + strconv.FormatInt(im.nextId, 10)
-               im.nextId = im.nextId + 1
-       }
-       return &idAsString
+       return nil
 }
 
 func (im *InvokerManager) sendEvent(invokerId string, eventType eventsapi.CAPIFEvent) {
index 4613cc4..4365eb3 100644 (file)
@@ -80,33 +80,41 @@ func TestOnboardInvoker(t *testing.T) {
        assert.True(t, invokerUnderTest.VerifyInvokerSecret(wantedInvokerId, wantedInvokerSecret))
        publishRegisterMock.AssertCalled(t, "GetAllPublishedServices")
        assert.Equal(t, invokermanagementapi.APIList(publishedServices), *resultInvoker.ApiList)
-       if invokerEvent, ok := waitForEvent(eventChannel, 1*time.Second); ok {
+       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)
@@ -114,7 +122,7 @@ 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")
 }
@@ -138,7 +146,7 @@ func TestDeleteInvoker(t *testing.T) {
 
        assert.Equal(t, http.StatusNoContent, result.Code())
        assert.False(t, invokerUnderTest.IsInvokerRegistered(invokerId))
-       if invokerEvent, ok := waitForEvent(eventChannel, 1*time.Second); ok {
+       if invokerEvent, timeout := waitForEvent(eventChannel, 1*time.Second); timeout {
                assert.Fail(t, "No event sent")
        } else {
                assert.Equal(t, invokerId, (*invokerEvent.EventDetail.ApiInvokerIds)[0])
@@ -154,7 +162,7 @@ func TestUpdateInvoker(t *testing.T) {
        invokerId := "invokerId"
        invoker := invokermanagementapi.APIInvokerEnrolmentDetails{
                ApiInvokerId:            &invokerId,
-               NotificationDestination: "url",
+               NotificationDestination: "http://golang.cafe/",
                OnboardingInformation: invokermanagementapi.OnboardingInformation{
                        ApiInvokerPublicKey: "key",
                },
@@ -162,7 +170,7 @@ func TestUpdateInvoker(t *testing.T) {
        serviceUnderTest.onboardedInvokers[invokerId] = invoker
 
        // Update the invoker with valid invoker, should return 200 with updated invoker details
-       newNotifURL := "newUrl"
+       newNotifURL := "http://golang.org/"
        invoker.NotificationDestination = common29122.Uri(newNotifURL)
        newPublicKey := "newPublicKey"
        invoker.OnboardingInformation.ApiInvokerPublicKey = newPublicKey
@@ -190,20 +198,19 @@ func TestUpdateInvoker(t *testing.T) {
        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("/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")
 
@@ -216,11 +223,11 @@ func TestUpdateInvoker(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, "not matching")
        assert.Contains(t, *problemDetails.Cause, "ApiInvokerId")
 
-       // 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"
        invoker.ApiInvokerId = &missingId
        result = testutil.NewRequest().Put("/onboardedInvokers/"+missingId).WithJsonBody(invoker).Go(t, requestHandler)
@@ -228,8 +235,7 @@ func TestUpdateInvoker(t *testing.T) {
        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")
 }
@@ -312,7 +318,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",
                },
diff --git a/capifcore/internal/invokermanagementapi/typeupdate.go b/capifcore/internal/invokermanagementapi/typeupdate.go
new file mode 100644 (file)
index 0000000..1de6280
--- /dev/null
@@ -0,0 +1,59 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package invokermanagementapi
+
+import (
+       "strings"
+
+       "github.com/google/uuid"
+)
+
+var uuidFunc = getUUID
+
+func (ied *APIInvokerEnrolmentDetails) PrepareNewInvoker() {
+       ied.createId()
+       ied.getOnboardingSecret()
+
+}
+
+func (ied *APIInvokerEnrolmentDetails) createId() {
+       idAsString := "api_invoker_id_"
+       if ied.ApiInvokerInformation != nil {
+               idAsString = idAsString + strings.ReplaceAll(*ied.ApiInvokerInformation, " ", "_")
+       } else {
+               idAsString = idAsString + uuidFunc()
+       }
+       ied.ApiInvokerId = &idAsString
+}
+
+func getUUID() string {
+       return uuid.NewString()
+}
+
+func (ied *APIInvokerEnrolmentDetails) getOnboardingSecret() {
+       onboardingSecret := "onboarding_secret_"
+       if ied.ApiInvokerInformation != nil {
+               onboardingSecret = onboardingSecret + strings.ReplaceAll(*ied.ApiInvokerInformation, " ", "_")
+       } else {
+               onboardingSecret = onboardingSecret + *ied.ApiInvokerId
+       }
+       ied.OnboardingInformation.OnboardingSecret = &onboardingSecret
+}
diff --git a/capifcore/internal/invokermanagementapi/typeupdate_test.go b/capifcore/internal/invokermanagementapi/typeupdate_test.go
new file mode 100644 (file)
index 0000000..5128d5f
--- /dev/null
@@ -0,0 +1,44 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package invokermanagementapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestPrepareNewInvoker(t *testing.T) {
+       invokerUnderTest := APIInvokerEnrolmentDetails{}
+       uuidFunc = func() string {
+               return "1"
+       }
+
+       invokerUnderTest.PrepareNewInvoker()
+       assert.Equal(t, "api_invoker_id_1", *invokerUnderTest.ApiInvokerId)
+       assert.Equal(t, "onboarding_secret_api_invoker_id_1", *invokerUnderTest.OnboardingInformation.OnboardingSecret)
+
+       invokerInfo := "invoker info"
+       invokerUnderTest.ApiInvokerInformation = &invokerInfo
+       invokerUnderTest.PrepareNewInvoker()
+       assert.Equal(t, "api_invoker_id_invoker_info", *invokerUnderTest.ApiInvokerId)
+       assert.Equal(t, "onboarding_secret_invoker_info", *invokerUnderTest.OnboardingInformation.OnboardingSecret)
+}
diff --git a/capifcore/internal/invokermanagementapi/typevalidation.go b/capifcore/internal/invokermanagementapi/typevalidation.go
new file mode 100644 (file)
index 0000000..10db338
--- /dev/null
@@ -0,0 +1,50 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package invokermanagementapi
+
+import (
+       "errors"
+       "fmt"
+       "net/url"
+)
+
+func (ied *APIInvokerEnrolmentDetails) Validate() error {
+       if ied.NotificationDestination == "" {
+               return errors.New("APIInvokerEnrolmentDetails missing required NotificationDestination")
+       }
+
+       if _, err := url.ParseRequestURI(string(ied.NotificationDestination)); err != nil {
+               return fmt.Errorf("APIInvokerEnrolmentDetails has invalid NotificationDestination, err=%s", err)
+       }
+
+       if ied.OnboardingInformation.ApiInvokerPublicKey == "" {
+               return errors.New("APIInvokerEnrolmentDetails missing required OnboardingInformation.ApiInvokerPublicKey")
+       }
+
+       return nil
+}
+
+func (ied *APIInvokerEnrolmentDetails) ValidateAlreadyOnboarded(otherInvoker APIInvokerEnrolmentDetails) error {
+       if ied.OnboardingInformation.ApiInvokerPublicKey == otherInvoker.OnboardingInformation.ApiInvokerPublicKey {
+               return errors.New("invoker with identical public key already onboarded")
+       }
+       return nil
+}
diff --git a/capifcore/internal/invokermanagementapi/typevalidation_test.go b/capifcore/internal/invokermanagementapi/typevalidation_test.go
new file mode 100644 (file)
index 0000000..b6b491d
--- /dev/null
@@ -0,0 +1,74 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package invokermanagementapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestValidateInvoker(t *testing.T) {
+       invokerUnderTest := APIInvokerEnrolmentDetails{}
+
+       err := invokerUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "NotificationDestination")
+       }
+
+       invokerUnderTest.NotificationDestination = "invalid dest"
+       err = invokerUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "invalid")
+               assert.Contains(t, err.Error(), "NotificationDestination")
+       }
+
+       invokerUnderTest.NotificationDestination = "http://golang.cafe/"
+       err = invokerUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "OnboardingInformation.ApiInvokerPublicKey")
+       }
+
+       invokerUnderTest.OnboardingInformation.ApiInvokerPublicKey = "key"
+       err = invokerUnderTest.Validate()
+       assert.Nil(t, err)
+}
+
+func TestValidateAlreadyOnboarded(t *testing.T) {
+       publicKey := "publicKey"
+       invokerUnderTest := APIInvokerEnrolmentDetails{
+               OnboardingInformation: OnboardingInformation{
+                       ApiInvokerPublicKey: publicKey,
+               },
+       }
+
+       otherInvoker := APIInvokerEnrolmentDetails{
+               OnboardingInformation: OnboardingInformation{
+                       ApiInvokerPublicKey: "otherPublicKey",
+               },
+       }
+       assert.Nil(t, invokerUnderTest.ValidateAlreadyOnboarded(otherInvoker))
+
+       otherInvoker.OnboardingInformation.ApiInvokerPublicKey = publicKey
+       assert.NotNil(t, invokerUnderTest.ValidateAlreadyOnboarded(otherInvoker))
+}
index 87f297a..d5e7a63 100644 (file)
@@ -24,7 +24,6 @@ import (
        "fmt"
        "net/http"
        "path"
-       "strings"
        "sync"
 
        "github.com/labstack/echo/v4"
@@ -42,99 +41,93 @@ type ServiceRegister interface {
 }
 
 type ProviderManager struct {
-       onboardedProviders map[string]provapi.APIProviderEnrolmentDetails
-       lock               sync.Mutex
+       registeredProviders map[string]provapi.APIProviderEnrolmentDetails
+       lock                sync.Mutex
 }
 
 func NewProviderManager() *ProviderManager {
        return &ProviderManager{
-               onboardedProviders: make(map[string]provapi.APIProviderEnrolmentDetails),
+               registeredProviders: make(map[string]provapi.APIProviderEnrolmentDetails),
        }
 }
 
 func (pm *ProviderManager) IsFunctionRegistered(functionId string) bool {
-       registered := false
-out:
-       for _, provider := range pm.onboardedProviders {
-               for _, registeredFunc := range *provider.ApiProvFuncs {
-                       if *registeredFunc.ApiProvFuncId == functionId {
-                               registered = true
-                               break out
-                       }
+       for _, provider := range pm.registeredProviders {
+               if provider.IsFunctionRegistered(functionId) {
+                       return true
                }
        }
-
-       return registered
+       return false
 }
 
 func (pm *ProviderManager) GetAefsForPublisher(apfId string) []string {
-       for _, provider := range pm.onboardedProviders {
-               for _, registeredFunc := range *provider.ApiProvFuncs {
-                       if *registeredFunc.ApiProvFuncId == apfId && registeredFunc.ApiProvFuncRole == provapi.ApiProviderFuncRoleAPF {
-                               return getExposedFuncs(provider.ApiProvFuncs)
-                       }
+       for _, provider := range pm.registeredProviders {
+               if aefs := provider.GetExposingFunctionIdsForPublisher(apfId); aefs != nil {
+                       return aefs
                }
        }
        return nil
 }
 
-func getExposedFuncs(providerFuncs *[]provapi.APIProviderFunctionDetails) []string {
-       exposedFuncs := []string{}
-       for _, registeredFunc := range *providerFuncs {
-               if registeredFunc.ApiProvFuncRole == provapi.ApiProviderFuncRoleAEF {
-                       exposedFuncs = append(exposedFuncs, *registeredFunc.ApiProvFuncId)
-               }
-       }
-       return exposedFuncs
-}
-
 func (pm *ProviderManager) PostRegistrations(ctx echo.Context) error {
        var newProvider provapi.APIProviderEnrolmentDetails
-       err := ctx.Bind(&newProvider)
-       if err != nil {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invalid format for provider")
+       errMsg := "Unable to register provider due to %s"
+       if err := ctx.Bind(&newProvider); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for provider"))
        }
 
-       if newProvider.ApiProvDomInfo == nil || *newProvider.ApiProvDomInfo == "" {
-               return sendCoreError(ctx, http.StatusBadRequest, "Provider missing required ApiProvDomInfo")
+       if err := pm.isProviderRegistered(newProvider); err != nil {
+               return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errMsg, err))
        }
 
-       pm.lock.Lock()
-       defer pm.lock.Unlock()
-
-       newProvider.ApiProvDomId = pm.getDomainId(newProvider.ApiProvDomInfo)
+       if err := newProvider.Validate(); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
+       }
 
-       pm.registerFunctions(newProvider.ApiProvFuncs)
-       pm.onboardedProviders[*newProvider.ApiProvDomId] = newProvider
+       pm.prepareNewProvider(&newProvider)
 
        uri := ctx.Request().Host + ctx.Request().URL.String()
        ctx.Response().Header().Set(echo.HeaderLocation, ctx.Scheme()+`://`+path.Join(uri, *newProvider.ApiProvDomId))
-       err = ctx.JSON(http.StatusCreated, newProvider)
-       if err != nil {
+       if err := ctx.JSON(http.StatusCreated, newProvider); err != nil {
                // Something really bad happened, tell Echo that our handler failed
                return err
        }
+       return nil
+}
 
+func (pm *ProviderManager) isProviderRegistered(newProvider provapi.APIProviderEnrolmentDetails) error {
+       for _, prov := range pm.registeredProviders {
+               if err := prov.ValidateAlreadyRegistered(newProvider); err != nil {
+                       return err
+               }
+       }
        return nil
 }
 
-func (pm *ProviderManager) DeleteRegistrationsRegistrationId(ctx echo.Context, registrationId string) error {
+func (pm *ProviderManager) prepareNewProvider(newProvider *provapi.APIProviderEnrolmentDetails) {
        pm.lock.Lock()
        defer pm.lock.Unlock()
 
-       log.Debug(pm.onboardedProviders)
-       if _, ok := pm.onboardedProviders[registrationId]; ok {
-               log.Debug("Deleting provider", registrationId)
-               delete(pm.onboardedProviders, registrationId)
-       }
+       newProvider.PrepareNewProvider()
+       pm.registeredProviders[*newProvider.ApiProvDomId] = *newProvider
+}
 
+func (pm *ProviderManager) DeleteRegistrationsRegistrationId(ctx echo.Context, registrationId string) error {
+       log.Debug(pm.registeredProviders)
+       if _, ok := pm.registeredProviders[registrationId]; ok {
+               pm.deleteProvider(registrationId)
+       }
        return ctx.NoContent(http.StatusNoContent)
 }
 
-func (pm *ProviderManager) PutRegistrationsRegistrationId(ctx echo.Context, registrationId string) error {
+func (pm *ProviderManager) deleteProvider(registrationId string) {
+       log.Debug("Deleting provider", registrationId)
        pm.lock.Lock()
        defer pm.lock.Unlock()
+       delete(pm.registeredProviders, registrationId)
+}
 
+func (pm *ProviderManager) PutRegistrationsRegistrationId(ctx echo.Context, registrationId string) error {
        errMsg := "Unable to update provider due to %s."
        registeredProvider, err := pm.checkIfProviderIsRegistered(registrationId, ctx)
        if err != nil {
@@ -146,25 +139,27 @@ func (pm *ProviderManager) PutRegistrationsRegistrationId(ctx echo.Context, regi
                return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
 
-       updateDomainInfo(&updatedProvider, registeredProvider)
+       if updatedProvider.Validate() != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
+       }
 
-       registeredProvider.ApiProvFuncs, err = updateFuncs(updatedProvider.ApiProvFuncs, registeredProvider.ApiProvFuncs)
-       if err != nil {
+       if err = pm.updateProvider(updatedProvider, registeredProvider); err != nil {
                return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
 
-       pm.onboardedProviders[*registeredProvider.ApiProvDomId] = *registeredProvider
-       err = ctx.JSON(http.StatusOK, *registeredProvider)
-       if err != nil {
+       if err = ctx.JSON(http.StatusOK, updatedProvider); err != nil {
                // Something really bad happened, tell Echo that our handler failed
                return err
        }
-
        return nil
 }
 
+func (pm *ProviderManager) ModifyIndApiProviderEnrolment(ctx echo.Context, registrationId string) error {
+       return ctx.NoContent(http.StatusNotImplemented)
+}
+
 func (pm *ProviderManager) checkIfProviderIsRegistered(registrationId string, ctx echo.Context) (*provapi.APIProviderEnrolmentDetails, error) {
-       registeredProvider, ok := pm.onboardedProviders[registrationId]
+       registeredProvider, ok := pm.registeredProviders[registrationId]
        if !ok {
                return nil, fmt.Errorf("provider not onboarded")
        }
@@ -180,75 +175,16 @@ func getProviderFromRequest(ctx echo.Context) (provapi.APIProviderEnrolmentDetai
        return updatedProvider, nil
 }
 
-func updateDomainInfo(updatedProvider, registeredProvider *provapi.APIProviderEnrolmentDetails) {
-       if updatedProvider.ApiProvDomInfo != nil {
-               registeredProvider.ApiProvDomInfo = updatedProvider.ApiProvDomInfo
-       }
-}
-
-func updateFuncs(updatedFuncs, registeredFuncs *[]provapi.APIProviderFunctionDetails) (*[]provapi.APIProviderFunctionDetails, error) {
-       addedFuncs := []provapi.APIProviderFunctionDetails{}
-       changedFuncs := []provapi.APIProviderFunctionDetails{}
-       for _, function := range *updatedFuncs {
-               if function.ApiProvFuncId == nil {
-                       function.ApiProvFuncId = getFuncId(function.ApiProvFuncRole, function.ApiProvFuncInfo)
-                       addedFuncs = append(addedFuncs, function)
-               } else {
-                       registeredFunction, ok := getApiFunc(*function.ApiProvFuncId, registeredFuncs)
-                       if !ok {
-                               return nil, fmt.Errorf("function with ID %s is not registered for the provider", *function.ApiProvFuncId)
-                       }
-                       if function.ApiProvFuncInfo != nil {
-                               registeredFunction.ApiProvFuncInfo = function.ApiProvFuncInfo
-                       }
-                       changedFuncs = append(changedFuncs, function)
-               }
-       }
-       modifiedFuncs := append(changedFuncs, addedFuncs...)
-       return &modifiedFuncs, nil
-}
-
-func getApiFunc(funcId string, apiFunctions *[]provapi.APIProviderFunctionDetails) (provapi.APIProviderFunctionDetails, bool) {
-       for _, function := range *apiFunctions {
-               if *function.ApiProvFuncId == funcId {
-                       return function, true
-               }
-       }
-       return provapi.APIProviderFunctionDetails{}, false
-}
-
-func (pm *ProviderManager) ModifyIndApiProviderEnrolment(ctx echo.Context, registrationId string) error {
-       return ctx.NoContent(http.StatusNotImplemented)
-}
-
-func (pm *ProviderManager) registerFunctions(provFuncs *[]provapi.APIProviderFunctionDetails) {
-       if provFuncs == nil {
-               return
-       }
-       for i, provFunc := range *provFuncs {
-               (*provFuncs)[i].ApiProvFuncId = getFuncId(provFunc.ApiProvFuncRole, provFunc.ApiProvFuncInfo)
-       }
-}
-
-func (pm *ProviderManager) getDomainId(domainInfo *string) *string {
-       idAsString := "domain_id_" + strings.ReplaceAll(*domainInfo, " ", "_")
-       return &idAsString
-}
+func (pm *ProviderManager) updateProvider(updatedProvider provapi.APIProviderEnrolmentDetails, registeredProvider *provapi.APIProviderEnrolmentDetails) error {
+       pm.lock.Lock()
+       defer pm.lock.Unlock()
 
-func getFuncId(role provapi.ApiProviderFuncRole, funcInfo *string) *string {
-       var idPrefix string
-       switch role {
-       case provapi.ApiProviderFuncRoleAPF:
-               idPrefix = "APF_id_"
-       case provapi.ApiProviderFuncRoleAMF:
-               idPrefix = "AMF_id_"
-       case provapi.ApiProviderFuncRoleAEF:
-               idPrefix = "AEF_id_"
-       default:
-               idPrefix = "function_id_"
+       if err := updatedProvider.UpdateFuncs(*registeredProvider); err == nil {
+               pm.registeredProviders[*updatedProvider.ApiProvDomId] = updatedProvider
+               return nil
+       } else {
+               return err
        }
-       idAsString := idPrefix + strings.ReplaceAll(*funcInfo, " ", "_")
-       return &idAsString
 }
 
 // This function wraps sending of an error in the Error format, and
index a3bb7aa..010e7c5 100644 (file)
@@ -68,6 +68,15 @@ func TestRegisterValidProvider(t *testing.T) {
        assert.Empty(t, resultProvider.FailReason)
        assert.Equal(t, "http://example.com/registrations/"+*resultProvider.ApiProvDomId, result.Recorder.Header().Get(echo.HeaderLocation))
        assert.True(t, managerUnderTest.IsFunctionRegistered("APF_id_rApp_as_APF"))
+
+       // Register same provider again should result in Forbidden
+       result = testutil.NewRequest().Post("/registrations").WithJsonBody(newProvider).Go(t, requestHandler)
+       var errorObj common29122.ProblemDetails
+       assert.Equal(t, http.StatusForbidden, result.Code())
+       err = result.UnmarshalBodyToObject(&errorObj)
+       assert.NoError(t, err, "error unmarshaling response")
+       assert.Equal(t, http.StatusForbidden, *errorObj.Status)
+       assert.Contains(t, *errorObj.Cause, "already registered")
 }
 
 func TestUpdateValidProviderWithNewFunction(t *testing.T) {
@@ -78,7 +87,7 @@ func TestUpdateValidProviderWithNewFunction(t *testing.T) {
        (*provider.ApiProvFuncs)[0].ApiProvFuncId = &funcIdAPF
        (*provider.ApiProvFuncs)[1].ApiProvFuncId = &funcIdAMF
        (*provider.ApiProvFuncs)[2].ApiProvFuncId = &funcIdAEF
-       managerUnderTest.onboardedProviders[domainID] = provider
+       managerUnderTest.registeredProviders[domainID] = provider
 
        // Modify the provider
        updatedProvider := getProvider()
@@ -95,6 +104,9 @@ func TestUpdateValidProviderWithNewFunction(t *testing.T) {
        testFuncs = append(testFuncs, provapi.APIProviderFunctionDetails{
                ApiProvFuncInfo: &newFuncInfoAEF,
                ApiProvFuncRole: provapi.ApiProviderFuncRoleAEF,
+               RegInfo: provapi.RegistrationInformation{
+                       ApiProvPubKey: "key",
+               },
        })
        updatedProvider.ApiProvFuncs = &testFuncs
 
@@ -119,7 +131,7 @@ func TestUpdateValidProviderWithDeletedFunction(t *testing.T) {
        (*provider.ApiProvFuncs)[0].ApiProvFuncId = &funcIdAPF
        (*provider.ApiProvFuncs)[1].ApiProvFuncId = &funcIdAMF
        (*provider.ApiProvFuncs)[2].ApiProvFuncId = &funcIdAEF
-       managerUnderTest.onboardedProviders[domainID] = provider
+       managerUnderTest.registeredProviders[domainID] = provider
 
        // Modify the provider
        updatedProvider := getProvider()
@@ -152,7 +164,7 @@ func TestUpdateMissingFunction(t *testing.T) {
        (*provider.ApiProvFuncs)[0].ApiProvFuncId = &otherId
        (*provider.ApiProvFuncs)[1].ApiProvFuncId = &funcIdAMF
        (*provider.ApiProvFuncs)[2].ApiProvFuncId = &funcIdAEF
-       managerUnderTest.onboardedProviders[domainID] = provider
+       managerUnderTest.registeredProviders[domainID] = provider
 
        // Modify the provider
        updatedProvider := getProvider()
@@ -167,6 +179,7 @@ func TestUpdateMissingFunction(t *testing.T) {
        assert.Equal(t, http.StatusBadRequest, result.Code())
        err := result.UnmarshalBodyToObject(&errorObj)
        assert.NoError(t, err, "error unmarshaling response")
+       assert.Equal(t, http.StatusBadRequest, *errorObj.Status)
        assert.Contains(t, *errorObj.Cause, funcIdAPF)
        assert.Contains(t, *errorObj.Cause, "not registered")
 }
@@ -177,7 +190,7 @@ func TestDeleteProvider(t *testing.T) {
        provider := getProvider()
        provider.ApiProvDomId = &domainID
        (*provider.ApiProvFuncs)[0].ApiProvFuncId = &funcIdAPF
-       managerUnderTest.onboardedProviders[domainID] = provider
+       managerUnderTest.registeredProviders[domainID] = provider
        assert.True(t, managerUnderTest.IsFunctionRegistered(funcIdAPF))
 
        result := testutil.NewRequest().Delete("/registrations/"+domainID).Go(t, requestHandler)
@@ -190,17 +203,16 @@ func TestProviderHandlingValidation(t *testing.T) {
 
        newProvider := provapi.APIProviderEnrolmentDetails{}
 
-       // Register a valid provider
+       // Register an invalid provider
        result := testutil.NewRequest().Post("/registrations").WithJsonBody(newProvider).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)
-       errMsg := "Provider missing required ApiProvDomInfo"
-       assert.Equal(t, &errMsg, problemDetails.Cause)
+       assert.Equal(t, http.StatusBadRequest, *problemDetails.Status)
+       assert.Contains(t, *problemDetails.Cause, "missing")
+       assert.Contains(t, *problemDetails.Cause, "regSec")
 }
 
 func TestGetExposedFunctionsForPublishingFunction(t *testing.T) {
@@ -211,8 +223,8 @@ func TestGetExposedFunctionsForPublishingFunction(t *testing.T) {
        (*provider.ApiProvFuncs)[0].ApiProvFuncId = &funcIdAPF
        (*provider.ApiProvFuncs)[1].ApiProvFuncId = &funcIdAMF
        (*provider.ApiProvFuncs)[2].ApiProvFuncId = &funcIdAEF
-       managerUnderTest.onboardedProviders[domainID] = provider
-       managerUnderTest.onboardedProviders[otherDomainID] = getOtherProvider()
+       managerUnderTest.registeredProviders[domainID] = provider
+       managerUnderTest.registeredProviders[otherDomainID] = getOtherProvider()
 
        exposedFuncs := managerUnderTest.GetAefsForPublisher(funcIdAPF)
        assert.Equal(t, 1, len(exposedFuncs))
@@ -224,17 +236,27 @@ func getProvider() provapi.APIProviderEnrolmentDetails {
                {
                        ApiProvFuncInfo: &funcInfoAPF,
                        ApiProvFuncRole: provapi.ApiProviderFuncRoleAPF,
+                       RegInfo: provapi.RegistrationInformation{
+                               ApiProvPubKey: "key",
+                       },
                },
                {
                        ApiProvFuncInfo: &funcInfoAMF,
                        ApiProvFuncRole: provapi.ApiProviderFuncRoleAMF,
+                       RegInfo: provapi.RegistrationInformation{
+                               ApiProvPubKey: "key",
+                       },
                },
                {
                        ApiProvFuncInfo: &funcInfoAEF,
                        ApiProvFuncRole: provapi.ApiProviderFuncRoleAEF,
+                       RegInfo: provapi.RegistrationInformation{
+                               ApiProvPubKey: "key",
+                       },
                },
        }
        return provapi.APIProviderEnrolmentDetails{
+               RegSec:         "sec",
                ApiProvDomInfo: &domainInfo,
                ApiProvFuncs:   &testFuncs,
        }
diff --git a/capifcore/internal/providermanagementapi/typeaccess.go b/capifcore/internal/providermanagementapi/typeaccess.go
new file mode 100644 (file)
index 0000000..78b68c0
--- /dev/null
@@ -0,0 +1,57 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package providermanagementapi
+
+func (ed APIProviderEnrolmentDetails) GetExposingFunctionIdsForPublisher(apfId string) []string {
+       for _, registeredFunc := range *ed.ApiProvFuncs {
+               if *registeredFunc.ApiProvFuncId == apfId && registeredFunc.isProvidingFunction() {
+                       return ed.getExposingFunctionIds()
+               }
+       }
+       return nil
+}
+
+func (ed APIProviderEnrolmentDetails) getExposingFunctionIds() []string {
+       exposedFuncs := []string{}
+       for _, registeredFunc := range *ed.ApiProvFuncs {
+               if registeredFunc.isExposingFunction() {
+                       exposedFuncs = append(exposedFuncs, *registeredFunc.ApiProvFuncId)
+               }
+       }
+       return exposedFuncs
+}
+
+func (ed APIProviderEnrolmentDetails) IsFunctionRegistered(functionId string) bool {
+       for _, registeredFunc := range *ed.ApiProvFuncs {
+               if *registeredFunc.ApiProvFuncId == functionId {
+                       return true
+               }
+       }
+       return false
+}
+
+func (fd APIProviderFunctionDetails) isProvidingFunction() bool {
+       return fd.ApiProvFuncRole == ApiProviderFuncRoleAPF
+}
+
+func (fd APIProviderFunctionDetails) isExposingFunction() bool {
+       return fd.ApiProvFuncRole == ApiProviderFuncRoleAEF
+}
diff --git a/capifcore/internal/providermanagementapi/typeaccess_test.go b/capifcore/internal/providermanagementapi/typeaccess_test.go
new file mode 100644 (file)
index 0000000..e108185
--- /dev/null
@@ -0,0 +1,52 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package providermanagementapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestGetExposedFunctionIds(t *testing.T) {
+       providerUnderTest := getProvider()
+
+       exposedFuncs := providerUnderTest.GetExposingFunctionIdsForPublisher(funcIdAPF)
+
+       assert.Len(t, exposedFuncs, 1)
+       assert.Equal(t, funcIdAEF, exposedFuncs[0])
+
+       exposedFuncs = providerUnderTest.GetExposingFunctionIdsForPublisher("anyId")
+
+       assert.Len(t, exposedFuncs, 0)
+}
+
+func TestIsFunctionRegistered(t *testing.T) {
+       providerUnderTest := getProvider()
+
+       registered := providerUnderTest.IsFunctionRegistered(funcIdAPF)
+
+       assert.True(t, registered)
+
+       registered = providerUnderTest.IsFunctionRegistered("anyID")
+
+       assert.False(t, registered)
+}
diff --git a/capifcore/internal/providermanagementapi/typeupdate.go b/capifcore/internal/providermanagementapi/typeupdate.go
new file mode 100644 (file)
index 0000000..cbeb1cd
--- /dev/null
@@ -0,0 +1,94 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package providermanagementapi
+
+import (
+       "fmt"
+       "strings"
+
+       "github.com/google/uuid"
+)
+
+var uuidFunc = getUUID
+
+func (ed *APIProviderEnrolmentDetails) UpdateFuncs(registeredProvider APIProviderEnrolmentDetails) error {
+       for pos, function := range *ed.ApiProvFuncs {
+               if function.ApiProvFuncId == nil {
+                       (*ed.ApiProvFuncs)[pos].ApiProvFuncId = getFuncId(function.ApiProvFuncRole, function.ApiProvFuncInfo)
+               } else {
+                       if !registeredProvider.IsFunctionRegistered(*function.ApiProvFuncId) {
+                               return fmt.Errorf("function with ID %s is not registered for the provider", *function.ApiProvFuncId)
+                       }
+               }
+       }
+       return nil
+}
+
+func (ed *APIProviderEnrolmentDetails) PrepareNewProvider() {
+       ed.ApiProvDomId = ed.getDomainId()
+
+       ed.registerFunctions()
+
+}
+
+func (ed *APIProviderEnrolmentDetails) getDomainId() *string {
+       var idAsString string
+       if ed.ApiProvDomInfo != nil {
+               idAsString = strings.ReplaceAll(*ed.ApiProvDomInfo, " ", "_")
+       } else {
+               idAsString = uuidFunc()
+       }
+       newId := "domain_id_" + idAsString
+       return &newId
+}
+
+func (ed *APIProviderEnrolmentDetails) registerFunctions() {
+       if ed.ApiProvFuncs == nil {
+               return
+       }
+       for i, provFunc := range *ed.ApiProvFuncs {
+               (*ed.ApiProvFuncs)[i].ApiProvFuncId = getFuncId(provFunc.ApiProvFuncRole, provFunc.ApiProvFuncInfo)
+       }
+}
+
+func getFuncId(role ApiProviderFuncRole, funcInfo *string) *string {
+       var idPrefix string
+       switch role {
+       case ApiProviderFuncRoleAPF:
+               idPrefix = "APF_id_"
+       case ApiProviderFuncRoleAMF:
+               idPrefix = "AMF_id_"
+       case ApiProviderFuncRoleAEF:
+               idPrefix = "AEF_id_"
+       }
+       var id string
+       if funcInfo != nil {
+               id = strings.ReplaceAll(*funcInfo, " ", "_")
+       } else {
+               id = uuidFunc()
+       }
+       idAsString := idPrefix + id
+       return &idAsString
+}
+
+func getUUID() string {
+       return uuid.NewString()
+}
diff --git a/capifcore/internal/providermanagementapi/typeupdate_test.go b/capifcore/internal/providermanagementapi/typeupdate_test.go
new file mode 100644 (file)
index 0000000..f693359
--- /dev/null
@@ -0,0 +1,83 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package providermanagementapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestPrepareNewProvider(t *testing.T) {
+       domainInfo := "domain info"
+       funcInfo := "func info"
+       providerUnderTest := APIProviderEnrolmentDetails{
+               ApiProvDomInfo: &domainInfo,
+               ApiProvFuncs: &[]APIProviderFunctionDetails{
+                       {
+                               ApiProvFuncRole: ApiProviderFuncRoleAPF,
+                               ApiProvFuncInfo: &funcInfo,
+                       },
+                       {
+                               ApiProvFuncRole: ApiProviderFuncRoleAEF,
+                       },
+               },
+       }
+       uuidFunc = func() string {
+               return "1"
+       }
+
+       providerUnderTest.PrepareNewProvider()
+
+       assert.Equal(t, "domain_id_domain_info", *providerUnderTest.ApiProvDomId)
+       assert.Equal(t, "APF_id_func_info", *(*providerUnderTest.ApiProvFuncs)[0].ApiProvFuncId)
+       assert.Equal(t, "AEF_id_1", *(*providerUnderTest.ApiProvFuncs)[1].ApiProvFuncId)
+
+       providerUnderTest = APIProviderEnrolmentDetails{}
+
+       providerUnderTest.PrepareNewProvider()
+
+       assert.Equal(t, "domain_id_1", *providerUnderTest.ApiProvDomId)
+}
+
+func TestUpdateFuncs(t *testing.T) {
+       registeredProvider := getProvider()
+
+       funcInfo := "func info"
+       updatedFuncs := []APIProviderFunctionDetails{
+               (*registeredProvider.ApiProvFuncs)[0],
+               (*registeredProvider.ApiProvFuncs)[2],
+               {
+                       ApiProvFuncRole: ApiProviderFuncRoleAEF,
+                       ApiProvFuncInfo: &funcInfo,
+               },
+       }
+       providerUnderTest := APIProviderEnrolmentDetails{
+               ApiProvFuncs: &updatedFuncs,
+       }
+       err := providerUnderTest.UpdateFuncs(registeredProvider)
+
+       assert.Nil(t, err)
+       assert.Len(t, *providerUnderTest.ApiProvFuncs, 3)
+       assert.Equal(t, funcIdAPF, *(*providerUnderTest.ApiProvFuncs)[0].ApiProvFuncId)
+       assert.Equal(t, funcIdAEF, *(*providerUnderTest.ApiProvFuncs)[1].ApiProvFuncId)
+       assert.Equal(t, "AEF_id_func_info", *(*providerUnderTest.ApiProvFuncs)[2].ApiProvFuncId)
+}
diff --git a/capifcore/internal/providermanagementapi/typevalidation.go b/capifcore/internal/providermanagementapi/typevalidation.go
new file mode 100644 (file)
index 0000000..bb32991
--- /dev/null
@@ -0,0 +1,76 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package providermanagementapi
+
+import (
+       "errors"
+       "fmt"
+       "strings"
+)
+
+func (ri RegistrationInformation) Validate() error {
+       if len(strings.TrimSpace(ri.ApiProvPubKey)) == 0 {
+               return errors.New("RegistrationInformation missing required apiProvPubKey")
+       }
+       return nil
+}
+
+func (fd APIProviderFunctionDetails) Validate() error {
+       if len(strings.TrimSpace(string(fd.ApiProvFuncRole))) == 0 {
+               return errors.New("APIProviderFunctionDetails missing required apiProvFuncRole")
+       }
+       switch role := fd.ApiProvFuncRole; role {
+       case ApiProviderFuncRoleAEF:
+       case ApiProviderFuncRoleAPF:
+       case ApiProviderFuncRoleAMF:
+       default:
+               return errors.New("APIProviderFunctionDetails has invalid apiProvFuncRole")
+       }
+
+       return fd.RegInfo.Validate()
+}
+
+func (pd APIProviderEnrolmentDetails) Validate() error {
+       if len(strings.TrimSpace(pd.RegSec)) == 0 {
+               return errors.New("APIProviderEnrolmentDetails missing required regSec")
+       }
+       if pd.ApiProvFuncs != nil {
+               return pd.validateFunctions()
+       }
+       return nil
+}
+
+func (pd APIProviderEnrolmentDetails) validateFunctions() error {
+       for _, function := range *pd.ApiProvFuncs {
+               err := function.Validate()
+               if err != nil {
+                       return fmt.Errorf("apiProvFuncs contains invalid function: %s", err)
+               }
+       }
+       return nil
+}
+
+func (pd APIProviderEnrolmentDetails) ValidateAlreadyRegistered(otherProvider APIProviderEnrolmentDetails) error {
+       if pd.RegSec == otherProvider.RegSec {
+               return errors.New("provider with identical regSec already registered")
+       }
+       return nil
+}
diff --git a/capifcore/internal/providermanagementapi/typevalidation_test.go b/capifcore/internal/providermanagementapi/typevalidation_test.go
new file mode 100644 (file)
index 0000000..a5a4e3c
--- /dev/null
@@ -0,0 +1,148 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package providermanagementapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+var (
+       domainID      = "domain_id_rApp_domain"
+       otherDomainID = "domain_id_other_domain"
+       domainInfo    = "rApp domain"
+       funcInfoAPF   = "rApp as APF"
+       funcIdAPF     = "APF_id_rApp_as_APF"
+       funcInfoAMF   = "rApp as AMF"
+       funcIdAMF     = "AMF_id_rApp_as_AMF"
+       funcInfoAEF   = "rApp as AEF"
+       funcIdAEF     = "AEF_id_rApp_as_AEF"
+)
+
+func TestValidateRegistrationInformation(t *testing.T) {
+       regInfoUnderTest := RegistrationInformation{}
+       err := regInfoUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "apiProvPubKey")
+       }
+
+       regInfoUnderTest.ApiProvPubKey = "key"
+       err = regInfoUnderTest.Validate()
+       assert.Nil(t, err)
+}
+
+func TestValidateAPIProviderFunctionDetails(t *testing.T) {
+       funcDetailsUnderTest := APIProviderFunctionDetails{}
+       err := funcDetailsUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "apiProvFuncRole")
+       }
+
+       var invalidFuncRole ApiProviderFuncRole = "invalid"
+       funcDetailsUnderTest.ApiProvFuncRole = invalidFuncRole
+       err = funcDetailsUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "invalid")
+               assert.Contains(t, err.Error(), "apiProvFuncRole")
+       }
+
+       funcDetailsUnderTest.ApiProvFuncRole = ApiProviderFuncRoleAEF
+       err = funcDetailsUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "apiProvPubKey")
+       }
+
+       funcDetailsUnderTest.RegInfo = RegistrationInformation{
+               ApiProvPubKey: "key",
+       }
+       assert.Nil(t, funcDetailsUnderTest.Validate())
+}
+
+func TestValidateAPIProviderEnrolmentDetails(t *testing.T) {
+       providerDetailsUnderTest := APIProviderEnrolmentDetails{}
+       err := providerDetailsUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "regSec")
+       }
+
+       providerDetailsUnderTest.RegSec = "sec"
+       funcs := []APIProviderFunctionDetails{{}}
+       providerDetailsUnderTest.ApiProvFuncs = &funcs
+       err = providerDetailsUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "apiProvFuncs")
+               assert.Contains(t, err.Error(), "contains invalid")
+       }
+
+       (*providerDetailsUnderTest.ApiProvFuncs)[0] = APIProviderFunctionDetails{
+               ApiProvFuncRole: ApiProviderFuncRoleAEF,
+               RegInfo: RegistrationInformation{
+                       ApiProvPubKey: "key",
+               },
+       }
+       assert.Nil(t, providerDetailsUnderTest.Validate())
+}
+
+func TestValidateAlreadyRegistered(t *testing.T) {
+       regSec := "regSec"
+       providerUnderTest := APIProviderEnrolmentDetails{
+               RegSec: regSec,
+       }
+
+       otherProvider := APIProviderEnrolmentDetails{
+               RegSec: "otherRegSec",
+       }
+       assert.Nil(t, providerUnderTest.ValidateAlreadyRegistered(otherProvider))
+
+       otherProvider.RegSec = regSec
+       assert.NotNil(t, providerUnderTest.ValidateAlreadyRegistered(otherProvider))
+}
+
+func getProvider() APIProviderEnrolmentDetails {
+       testFuncs := []APIProviderFunctionDetails{
+               {
+                       ApiProvFuncId:   &funcIdAPF,
+                       ApiProvFuncInfo: &funcInfoAPF,
+                       ApiProvFuncRole: ApiProviderFuncRoleAPF,
+               },
+               {
+                       ApiProvFuncId:   &funcIdAMF,
+                       ApiProvFuncInfo: &funcInfoAMF,
+                       ApiProvFuncRole: ApiProviderFuncRoleAMF,
+               },
+               {
+                       ApiProvFuncId:   &funcIdAEF,
+                       ApiProvFuncInfo: &funcInfoAEF,
+                       ApiProvFuncRole: ApiProviderFuncRoleAEF,
+               },
+       }
+       return APIProviderEnrolmentDetails{
+               ApiProvDomId:   &domainID,
+               ApiProvDomInfo: &domainInfo,
+               ApiProvFuncs:   &testFuncs,
+       }
+
+}
index a798a71..df25620 100644 (file)
@@ -13,20 +13,6 @@ type PublishRegister struct {
        mock.Mock
 }
 
-// AreAPIsPublished provides a mock function with given fields: serviceDescriptions
-func (_m *PublishRegister) AreAPIsPublished(serviceDescriptions *[]publishserviceapi.ServiceAPIDescription) bool {
-       ret := _m.Called(serviceDescriptions)
-
-       var r0 bool
-       if rf, ok := ret.Get(0).(func(*[]publishserviceapi.ServiceAPIDescription) bool); ok {
-               r0 = rf(serviceDescriptions)
-       } else {
-               r0 = ret.Get(0).(bool)
-       }
-
-       return r0
-}
-
 // GetAllPublishedServices provides a mock function with given fields:
 func (_m *PublishRegister) GetAllPublishedServices() []publishserviceapi.ServiceAPIDescription {
        ret := _m.Called()
index d85f077..7960f12 100644 (file)
@@ -42,9 +42,6 @@ import (
 
 //go:generate mockery --name PublishRegister
 type PublishRegister interface {
-       // Checks if the provided APIs are published.
-       // Returns true if all provided APIs have been published, false otherwise.
-       AreAPIsPublished(serviceDescriptions *[]publishapi.ServiceAPIDescription) bool
        // Checks if the provided API is published.
        // Returns true if the provided API has been published, false otherwise.
        IsAPIPublished(aefId, path string) bool
@@ -71,15 +68,6 @@ func NewPublishService(serviceRegister providermanagement.ServiceRegister, hm he
        }
 }
 
-func (ps *PublishService) AreAPIsPublished(serviceDescriptions *[]publishapi.ServiceAPIDescription) bool {
-
-       if serviceDescriptions != nil {
-               registeredApis := ps.getAllAefIds()
-               return checkNewDescriptions(*serviceDescriptions, registeredApis)
-       }
-       return true
-}
-
 func (ps *PublishService) getAllAefIds() []string {
        ps.lock.Lock()
        defer ps.lock.Unlock()
@@ -87,46 +75,12 @@ func (ps *PublishService) getAllAefIds() []string {
        allIds := []string{}
        for _, descriptions := range ps.publishedServices {
                for _, description := range descriptions {
-                       allIds = append(allIds, getIdsFromDescription(description)...)
-               }
-       }
-       return allIds
-}
-
-func getIdsFromDescription(description publishapi.ServiceAPIDescription) []string {
-       allIds := []string{}
-       if description.AefProfiles != nil {
-               for _, aefProfile := range *description.AefProfiles {
-                       allIds = append(allIds, aefProfile.AefId)
+                       allIds = append(allIds, description.GetAefIds()...)
                }
        }
        return allIds
 }
 
-func checkNewDescriptions(newDescriptions []publishapi.ServiceAPIDescription, registeredAefIds []string) bool {
-       registered := true
-       for _, newApi := range newDescriptions {
-               if !checkProfiles(newApi.AefProfiles, registeredAefIds) {
-                       registered = false
-                       break
-               }
-       }
-       return registered
-}
-
-func checkProfiles(newProfiles *[]publishapi.AefProfile, registeredAefIds []string) bool {
-       allRegistered := true
-       if newProfiles != nil {
-               for _, profile := range *newProfiles {
-                       if !slices.Contains(registeredAefIds, profile.AefId) {
-                               allRegistered = false
-                               break
-                       }
-               }
-       }
-       return allRegistered
-}
-
 func (ps *PublishService) IsAPIPublished(aefId, path string) bool {
        return slices.Contains(ps.getAllAefIds(), aefId)
 }
@@ -158,23 +112,30 @@ func (ps *PublishService) GetApfIdServiceApis(ctx echo.Context, apfId string) er
 // Publish a new API.
 func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) error {
        var newServiceAPIDescription publishapi.ServiceAPIDescription
+       errorMsg := "Unable to publish the service due to %s "
        err := ctx.Bind(&newServiceAPIDescription)
        if err != nil {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invalid format for service "+apfId)
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errorMsg, "invalid format for service "+apfId))
+       }
+
+       if err := ps.isServicePublished(newServiceAPIDescription); err != nil {
+               return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errorMsg, err))
        }
 
+       if err := newServiceAPIDescription.Validate(); err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errorMsg, err))
+       }
        ps.lock.Lock()
        defer ps.lock.Unlock()
 
        registeredFuncs := ps.serviceRegister.GetAefsForPublisher(apfId)
        for _, profile := range *newServiceAPIDescription.AefProfiles {
                if !slices.Contains(registeredFuncs, profile.AefId) {
-                       return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("Function %s not registered", profile.AefId))
+                       return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf(errorMsg, fmt.Sprintf("function %s not registered", profile.AefId)))
                }
        }
 
-       newId := "api_id_" + newServiceAPIDescription.ApiName
-       newServiceAPIDescription.ApiId = &newId
+       newServiceAPIDescription.PrepareNewService()
 
        shouldReturn, returnValue := ps.installHelmChart(newServiceAPIDescription, ctx)
        if shouldReturn {
@@ -200,6 +161,17 @@ func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) e
        return nil
 }
 
+func (ps *PublishService) isServicePublished(newService publishapi.ServiceAPIDescription) error {
+       for _, services := range ps.publishedServices {
+               for _, service := range services {
+                       if err := service.ValidateAlreadyPublished(newService); err != nil {
+                               return err
+                       }
+               }
+       }
+       return nil
+}
+
 func (ps *PublishService) installHelmChart(newServiceAPIDescription publishapi.ServiceAPIDescription, ctx echo.Context) (bool, error) {
        info := strings.Split(*newServiceAPIDescription.Description, ",")
        if len(info) == 5 {
@@ -224,8 +196,8 @@ func (ps *PublishService) DeleteApfIdServiceApisServiceApiId(ctx echo.Context, a
                                log.Debug("Deleted service: ", serviceApiId)
                        }
                        ps.lock.Lock()
-                       defer ps.lock.Unlock()
                        ps.publishedServices[string(apfId)] = removeServiceDescription(pos, serviceDescriptions)
+                       ps.lock.Unlock()
                        go ps.sendEvent(*description, eventsapi.CAPIFEventSERVICEAPIUNAVAILABLE)
                }
        }
@@ -235,9 +207,9 @@ func (ps *PublishService) DeleteApfIdServiceApisServiceApiId(ctx echo.Context, a
 // Retrieve a published service API.
 func (ps *PublishService) GetApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
        ps.lock.Lock()
-       defer ps.lock.Unlock()
-
        serviceDescriptions, ok := ps.publishedServices[apfId]
+       ps.lock.Unlock()
+
        if ok {
                _, serviceDescription := getServiceDescription(serviceApiId, serviceDescriptions)
                if serviceDescription == nil {
@@ -277,84 +249,57 @@ func (ps *PublishService) ModifyIndAPFPubAPI(ctx echo.Context, apfId string, ser
 
 // Update a published service API.
 func (ps *PublishService) PutApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
-       pos, publishedService, shouldReturn, returnValue := ps.checkIfServiceIsPublished(apfId, serviceApiId, ctx)
-       if shouldReturn {
-               return returnValue
-       }
-
-       updatedServiceDescription, shouldReturn, returnValue := getServiceFromRequest(ctx)
-       if shouldReturn {
-               return returnValue
+       ps.lock.Lock()
+       defer ps.lock.Unlock()
+       errMsg := "Unable to update service due to %s."
+       pos, publishedService, err := ps.checkIfServiceIsPublished(apfId, serviceApiId, ctx)
+       if err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
-
-       if updatedServiceDescription.Description != nil {
-               ps.lock.Lock()
-               defer ps.lock.Unlock()
-
-               publishedService.Description = updatedServiceDescription.Description
-               ps.publishedServices[apfId][pos] = publishedService
-               go ps.sendEvent(publishedService, eventsapi.CAPIFEventSERVICEAPIUPDATE)
+       updatedServiceDescription, err := getServiceFromRequest(ctx)
+       if err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
-
-       pos, shouldReturn, returnValue = ps.updateProfiles(pos, apfId, updatedServiceDescription, publishedService, ctx)
-       if shouldReturn {
-               return returnValue
+       err = ps.checkProfilesRegistered(apfId, *updatedServiceDescription.AefProfiles)
+       if err != nil {
+               return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
        }
-
-       err := ctx.JSON(http.StatusOK, ps.publishedServices[apfId][pos])
+       ps.updateDescription(pos, apfId, &updatedServiceDescription, &publishedService)
+       publishedService.AefProfiles = updatedServiceDescription.AefProfiles
+       ps.publishedServices[apfId][pos] = publishedService
+       err = ctx.JSON(http.StatusOK, publishedService)
        if err != nil {
                // Something really bad happened, tell Echo that our handler failed
                return err
        }
        return nil
 }
-
-func (ps *PublishService) checkIfServiceIsPublished(apfId string, serviceApiId string, ctx echo.Context) (int, publishapi.ServiceAPIDescription, bool, error) {
+func (ps *PublishService) checkIfServiceIsPublished(apfId string, serviceApiId string, ctx echo.Context) (int, publishapi.ServiceAPIDescription, error) {
        publishedServices, ok := ps.publishedServices[apfId]
        if !ok {
-               return 0, publishapi.ServiceAPIDescription{}, true, sendCoreError(ctx, http.StatusBadRequest, "Service must be published before updating it")
+               return 0, publishapi.ServiceAPIDescription{}, fmt.Errorf("service must be published before updating it")
        } else {
                for pos, description := range publishedServices {
                        if *description.ApiId == serviceApiId {
-                               return pos, description, false, nil
-
+                               return pos, description, nil
                        }
-
                }
-
        }
-       return 0, publishapi.ServiceAPIDescription{}, true, sendCoreError(ctx, http.StatusBadRequest, "Service must be published before updating it")
+       return 0, publishapi.ServiceAPIDescription{}, fmt.Errorf("service must be published before updating it")
 }
-
-func getServiceFromRequest(ctx echo.Context) (publishapi.ServiceAPIDescription, bool, error) {
+func getServiceFromRequest(ctx echo.Context) (publishapi.ServiceAPIDescription, error) {
        var updatedServiceDescription publishapi.ServiceAPIDescription
        err := ctx.Bind(&updatedServiceDescription)
        if err != nil {
-               return publishapi.ServiceAPIDescription{}, true, sendCoreError(ctx, http.StatusBadRequest, "Invalid format for service")
+               return publishapi.ServiceAPIDescription{}, fmt.Errorf("invalid format for service")
        }
-       return updatedServiceDescription, false, nil
+       return updatedServiceDescription, nil
 }
-
-func (ps *PublishService) updateProfiles(pos int, apfId string, updatedServiceDescription publishapi.ServiceAPIDescription, publishedService publishapi.ServiceAPIDescription, ctx echo.Context) (int, bool, error) {
-       registeredFuncs := ps.serviceRegister.GetAefsForPublisher(apfId)
-       for _, profile := range *updatedServiceDescription.AefProfiles {
-               if !slices.Contains(registeredFuncs, profile.AefId) {
-                       return 0, false, sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("Function %s not registered", profile.AefId))
-               }
-               if ps.checkIfProfileIsNew(profile.AefId, *publishedService.AefProfiles) {
-
-                       publishedService.AefProfiles = ps.addProfile(profile, publishedService)
-                       ps.publishedServices[apfId][pos] = publishedService
-
-               } else {
-                       pos, shouldReturn, returnValue := ps.updateProfile(profile, publishedService, ctx)
-                       if shouldReturn {
-                               return pos, true, returnValue
-                       }
-               }
-
+func (ps *PublishService) updateDescription(pos int, apfId string, updatedServiceDescription, publishedService *publishapi.ServiceAPIDescription) {
+       if updatedServiceDescription.Description != nil {
+               publishedService.Description = updatedServiceDescription.Description
+               go ps.sendEvent(*publishedService, eventsapi.CAPIFEventSERVICEAPIUPDATE)
        }
-       return 0, false, nil
 }
 
 func (ps *PublishService) sendEvent(service publishapi.ServiceAPIDescription, eventType eventsapi.CAPIFEvent) {
@@ -370,41 +315,14 @@ func (ps *PublishService) sendEvent(service publishapi.ServiceAPIDescription, ev
        ps.eventChannel <- event
 }
 
-func (ps *PublishService) checkIfProfileIsNew(aefId string, publishedPofiles []publishapi.AefProfile) bool {
-       for _, profile := range publishedPofiles {
-               if profile.AefId == aefId {
-                       return false
-               }
-       }
-       return true
-}
-func (ps *PublishService) addProfile(profile publishapi.AefProfile, publishedService publishapi.ServiceAPIDescription) *[]publishapi.AefProfile {
-       registeredProfiles := *publishedService.AefProfiles
-       newProfiles := append(registeredProfiles, profile)
-       publishedService.AefProfiles = &newProfiles
-       return &newProfiles
-
-}
-
-func (*PublishService) updateProfile(profile publishapi.AefProfile, publishedService publishapi.ServiceAPIDescription, ctx echo.Context) (int, bool, error) {
-       pos, registeredProfile, err := getProfile(profile.AefId, publishedService.AefProfiles)
-       if err != nil {
-               return pos, true, sendCoreError(ctx, http.StatusBadRequest, "Unable to update service due to: "+err.Error())
-       }
-       if profile.DomainName != nil {
-               registeredProfile.DomainName = profile.DomainName
-               (*publishedService.AefProfiles)[pos] = registeredProfile
-       }
-       return -1, false, nil
-}
-
-func getProfile(profileId string, apiProfiles *[]publishapi.AefProfile) (int, publishapi.AefProfile, error) {
-       for pos, profile := range *apiProfiles {
-               if profile.AefId == profileId {
-                       return pos, profile, nil
+func (ps *PublishService) checkProfilesRegistered(apfId string, updatedProfiles []publishapi.AefProfile) error {
+       registeredFuncs := ps.serviceRegister.GetAefsForPublisher(apfId)
+       for _, profile := range updatedProfiles {
+               if !slices.Contains(registeredFuncs, profile.AefId) {
+                       return fmt.Errorf("function %s not registered", profile.AefId)
                }
        }
-       return 0, publishapi.AefProfile{}, fmt.Errorf("profile with ID %s is not registered for the service", profileId)
+       return nil
 }
 
 // This function wraps sending of an error in the Error format, and
index 2483053..b69b956 100644 (file)
@@ -47,6 +47,7 @@ import (
 )
 
 func TestPublishUnpublishService(t *testing.T) {
+
        apfId := "apfId"
        aefId := "aefId"
        serviceRegisterMock := serviceMocks.ServiceRegister{}
@@ -76,11 +77,9 @@ func TestPublishUnpublishService(t *testing.T) {
        err := result.UnmarshalBodyToObject(&resultService)
        assert.NoError(t, err, "error unmarshaling response")
        newApiId := "api_id_" + apiName
-       assert.Equal(t, *resultService.ApiId, newApiId)
+       assert.Equal(t, newApiId, *resultService.ApiId)
        assert.Equal(t, "http://example.com/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation))
        newServiceDescription.ApiId = &newApiId
-       wantedAPILIst := []publishapi.ServiceAPIDescription{newServiceDescription}
-       assert.True(t, serviceUnderTest.AreAPIsPublished(&wantedAPILIst))
        assert.True(t, serviceUnderTest.IsAPIPublished(aefId, apiName))
        serviceRegisterMock.AssertCalled(t, "GetAefsForPublisher", apfId)
        helmManagerMock.AssertCalled(t, "InstallHelmChart", namespace, repoName, chartName, releaseName)
@@ -100,8 +99,19 @@ func TestPublishUnpublishService(t *testing.T) {
        assert.NoError(t, err, "error unmarshaling response")
        assert.Equal(t, *resultService.ApiId, newApiId)
 
+       // Publish the same service again should result in Forbidden
+       result = testutil.NewRequest().Post("/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, requestHandler)
+
+       assert.Equal(t, http.StatusForbidden, result.Code())
+       var resultError common29122.ProblemDetails
+       err = result.UnmarshalBodyToObject(&resultError)
+       assert.NoError(t, err, "error unmarshaling response")
+       assert.Contains(t, *resultError.Cause, "already published")
+       assert.Equal(t, http.StatusForbidden, *resultError.Status)
+
        // Delete the service
        helmManagerMock.On("UninstallHelmChart", mock.Anything, mock.Anything).Return(nil)
+
        result = testutil.NewRequest().Delete("/"+apfId+"/service-apis/"+newApiId).Go(t, requestHandler)
 
        assert.Equal(t, http.StatusNoContent, result.Code())
@@ -139,8 +149,7 @@ func TestPostUnpublishedServiceWithUnregisteredFunction(t *testing.T) {
        assert.NoError(t, err, "error unmarshaling response")
        assert.Contains(t, *resultError.Cause, aefId)
        assert.Contains(t, *resultError.Cause, "not registered")
-       notFound := http.StatusNotFound
-       assert.Equal(t, &notFound, resultError.Status)
+       assert.Equal(t, http.StatusNotFound, *resultError.Status)
 }
 
 func TestGetServices(t *testing.T) {
@@ -258,6 +267,7 @@ func TestUpdateDescription(t *testing.T) {
        assert.Equal(t, newDescription, *resultService.Description)
        assert.Equal(t, newDomainName, *(*resultService.AefProfiles)[0].DomainName)
        assert.Equal(t, "aefIdNew", (*resultService.AefProfiles)[1].AefId)
+       assert.True(t, serviceUnderTest.IsAPIPublished("aefIdNew", "path"))
 
        if publishEvent, ok := waitForEvent(eventChannel, 1*time.Second); ok {
                assert.Fail(t, "No event sent")
@@ -267,6 +277,111 @@ func TestUpdateDescription(t *testing.T) {
        }
 }
 
+func TestUpdateValidServiceWithDeletedFunction(t *testing.T) {
+       apfId := "apfId"
+       serviceApiId := "serviceApiId"
+       aefId := "aefId"
+       apiName := "apiName"
+       description := "description"
+
+       serviceRegisterMock := serviceMocks.ServiceRegister{}
+       serviceRegisterMock.On("GetAefsForPublisher", apfId).Return([]string{aefId, "otherAefId", "aefIdNew"})
+       helmManagerMock := helmMocks.HelmManager{}
+       helmManagerMock.On("InstallHelmChart", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+       serviceUnderTest, _, requestHandler := getEcho(&serviceRegisterMock, &helmManagerMock)
+
+       serviceDescription := getServiceAPIDescription(aefId, apiName, description)
+       serviceDescription.ApiId = &serviceApiId
+       (*serviceDescription.AefProfiles)[0].AefId = aefId
+
+       newProfileDomain := "new profile Domain name"
+       var protocol publishapi.Protocol = "HTTP_1_1"
+       test := make([]publishapi.AefProfile, 1)
+       test = *serviceDescription.AefProfiles
+       test = append(test, publishapi.AefProfile{
+
+               AefId:      "aefIdNew",
+               DomainName: &newProfileDomain,
+               Protocol:   &protocol,
+               Versions: []publishapi.Version{
+                       {
+                               ApiVersion: "v1",
+                               Resources: &[]publishapi.Resource{
+                                       {
+                                               CommType: "REQUEST_RESPONSE",
+                                               Operations: &[]publishapi.Operation{
+                                                       "POST",
+                                               },
+                                               ResourceName: "app",
+                                               Uri:          "app",
+                                       },
+                               },
+                       },
+               },
+       },
+       )
+       serviceDescription.AefProfiles = &test
+       serviceUnderTest.publishedServices[apfId] = []publishapi.ServiceAPIDescription{serviceDescription}
+
+       //Modify the service
+       updatedServiceDescription := getServiceAPIDescription(aefId, apiName, description)
+       updatedServiceDescription.ApiId = &serviceApiId
+       test1 := make([]publishapi.AefProfile, 1)
+       test1 = *updatedServiceDescription.AefProfiles
+       test1 = append(test1, publishapi.AefProfile{
+
+               AefId:      "aefIdNew",
+               DomainName: &newProfileDomain,
+               Protocol:   &protocol,
+               Versions: []publishapi.Version{
+                       {
+                               ApiVersion: "v1",
+                               Resources: &[]publishapi.Resource{
+                                       {
+                                               CommType: "REQUEST_RESPONSE",
+                                               Operations: &[]publishapi.Operation{
+                                                       "POST",
+                                               },
+                                               ResourceName: "app",
+                                               Uri:          "app",
+                                       },
+                               },
+                       },
+               },
+       },
+       )
+       updatedServiceDescription.AefProfiles = &test1
+       testFunc := []publishapi.AefProfile{
+               (*updatedServiceDescription.AefProfiles)[1],
+       }
+
+       updatedServiceDescription.AefProfiles = &testFunc
+       result := testutil.NewRequest().Put("/"+apfId+"/service-apis/"+serviceApiId).WithJsonBody(updatedServiceDescription).Go(t, requestHandler)
+       var resultService publishapi.ServiceAPIDescription
+       assert.Equal(t, http.StatusOK, result.Code())
+       err := result.UnmarshalBodyToObject(&resultService)
+       assert.NoError(t, err, "error unmarshaling response")
+       assert.Len(t, (*resultService.AefProfiles), 1)
+       assert.False(t, serviceUnderTest.IsAPIPublished("aefId", "path"))
+
+}
+
+func TestPublishInvalidService(t *testing.T) {
+       _, _, requestHandler := getEcho(nil, nil)
+       newServiceDescription := getServiceAPIDescription("aefId", " ", "description")
+
+       // Publish a service
+       result := testutil.NewRequest().Post("/apfId/service-apis").WithJsonBody(newServiceDescription).Go(t, requestHandler)
+
+       assert.Equal(t, http.StatusBadRequest, result.Code())
+       var resultError common29122.ProblemDetails
+       err := result.UnmarshalBodyToObject(&resultError)
+       assert.NoError(t, err, "error unmarshaling response")
+       assert.Contains(t, *resultError.Cause, "missing")
+       assert.Contains(t, *resultError.Cause, "apiName")
+       assert.Equal(t, http.StatusBadRequest, *resultError.Status)
+
+}
 func getEcho(serviceRegister providermanagement.ServiceRegister, helmManager helmmanagement.HelmManager) (*PublishService, chan eventsapi.EventNotification, *echo.Echo) {
        swagger, err := publishapi.GetSwagger()
        if err != nil {
diff --git a/capifcore/internal/publishserviceapi/typeaccess.go b/capifcore/internal/publishserviceapi/typeaccess.go
new file mode 100644 (file)
index 0000000..32c1a7a
--- /dev/null
@@ -0,0 +1,30 @@
+// -
+//
+//     ========================LICENSE_START=================================
+//     O-RAN-SC
+//     %%
+//     Copyright (C) 2023: Nordix Foundation
+//     %%
+//     Licensed under the Apache License, Version 2.0 (the "License");
+//     you may not use this file except in compliance with the License.
+//     You may obtain a copy of the License at
+//
+//          http://www.apache.org/licenses/LICENSE-2.0
+//
+//     Unless required by applicable law or agreed to in writing, software
+//     distributed under the License is distributed on an "AS IS" BASIS,
+//     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//     See the License for the specific language governing permissions and
+//     limitations under the License.
+//     ========================LICENSE_END===================================
+package publishserviceapi
+
+func (sd ServiceAPIDescription) GetAefIds() []string {
+       allIds := []string{}
+       if sd.AefProfiles != nil {
+               for _, aefProfile := range *sd.AefProfiles {
+                       allIds = append(allIds, aefProfile.AefId)
+               }
+       }
+       return allIds
+}
diff --git a/capifcore/internal/publishserviceapi/typeupdate.go b/capifcore/internal/publishserviceapi/typeupdate.go
new file mode 100644 (file)
index 0000000..98e059c
--- /dev/null
@@ -0,0 +1,26 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package publishserviceapi
+
+func (sd *ServiceAPIDescription) PrepareNewService() {
+       apiName := "api_id_" + sd.ApiName
+       sd.ApiId = &apiName
+}
diff --git a/capifcore/internal/publishserviceapi/typevalidation.go b/capifcore/internal/publishserviceapi/typevalidation.go
new file mode 100644 (file)
index 0000000..287a07d
--- /dev/null
@@ -0,0 +1,40 @@
+// -
+//
+//     ========================LICENSE_START=================================
+//     O-RAN-SC
+//     %%
+//     Copyright (C) 2023: Nordix Foundation
+//     %%
+//     Licensed under the Apache License, Version 2.0 (the "License");
+//     you may not use this file except in compliance with the License.
+//     You may obtain a copy of the License at
+//
+//          http://www.apache.org/licenses/LICENSE-2.0
+//
+//     Unless required by applicable law or agreed to in writing, software
+//     distributed under the License is distributed on an "AS IS" BASIS,
+//     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//     See the License for the specific language governing permissions and
+//     limitations under the License.
+//     ========================LICENSE_END===================================
+package publishserviceapi
+
+import (
+       "errors"
+       //"fmt"
+       "strings"
+)
+
+func (sd ServiceAPIDescription) Validate() error {
+       if len(strings.TrimSpace(sd.ApiName)) == 0 {
+               return errors.New("ServiceAPIDescription missing required apiName")
+       }
+       return nil
+}
+
+func (sd ServiceAPIDescription) ValidateAlreadyPublished(otherService ServiceAPIDescription) error {
+       if sd.ApiName == otherService.ApiName {
+               return errors.New("service with identical apiName is already published")
+       }
+       return nil
+}
diff --git a/capifcore/internal/publishserviceapi/typevalidation_test.go b/capifcore/internal/publishserviceapi/typevalidation_test.go
new file mode 100644 (file)
index 0000000..d7b59d5
--- /dev/null
@@ -0,0 +1,55 @@
+// -
+//
+//     ========================LICENSE_START=================================
+//     O-RAN-SC
+//     %%
+//     Copyright (C) 2023: Nordix Foundation
+//     %%
+//     Licensed under the Apache License, Version 2.0 (the "License");
+//     you may not use this file except in compliance with the License.
+//     You may obtain a copy of the License at
+//
+//          http://www.apache.org/licenses/LICENSE-2.0
+//
+//     Unless required by applicable law or agreed to in writing, software
+//     distributed under the License is distributed on an "AS IS" BASIS,
+//     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//     See the License for the specific language governing permissions and
+//     limitations under the License.
+//     ========================LICENSE_END===================================
+package publishserviceapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestValidate(t *testing.T) {
+       serviceDescriptionUnderTest := ServiceAPIDescription{}
+       err := serviceDescriptionUnderTest.Validate()
+       if assert.Error(t, err) {
+               assert.Contains(t, err.Error(), "missing")
+               assert.Contains(t, err.Error(), "apiName")
+       }
+
+       serviceDescriptionUnderTest.ApiName = "apiName"
+       err = serviceDescriptionUnderTest.Validate()
+       assert.Nil(t, err)
+
+}
+
+func TestValidateAlreadyPublished(t *testing.T) {
+       apiName := "apiName"
+       serviceUnderTest := ServiceAPIDescription{
+               ApiName: apiName,
+       }
+
+       otherService := ServiceAPIDescription{
+               ApiName: "otherApiName",
+       }
+       assert.Nil(t, serviceUnderTest.ValidateAlreadyPublished(otherService))
+
+       otherService.ApiName = apiName
+       assert.NotNil(t, serviceUnderTest.ValidateAlreadyPublished(otherService))
+}
index 2cd5ccc..1c1890f 100644 (file)
@@ -54,9 +54,13 @@ var repoName string
 
 func main() {
        var port = flag.Int("port", 8090, "Port for CAPIF Core Function HTTP server")
+       var secPort = flag.Int("secPort", 4433, "Port for CAPIF Core Function HTTPS server")
        flag.StringVar(&url, "chartMuseumUrl", "", "ChartMuseum URL")
        flag.StringVar(&repoName, "repoName", "capifcore", "Repository name")
        var logLevelStr = flag.String("loglevel", "Info", "Log level")
+       var certPath = flag.String("certPath", "certs/cert.pem", "Path for server certificate")
+       var keyPath = flag.String("keyPath", "certs/key.pem", "Path for server private key")
+
        flag.Parse()
 
        if loglevel, err := log.ParseLevel(*logLevelStr); err == nil {
@@ -71,6 +75,7 @@ func main() {
        }
 
        go startWebServer(getEcho(), *port)
+       go startHttpsWebServer(getEcho(), *secPort, *certPath, *keyPath)
 
        log.Info("Server started and listening on port: ", *port)
 
@@ -162,13 +167,17 @@ func startWebServer(e *echo.Echo, port int) {
        e.Logger.Fatal(e.Start(fmt.Sprintf("0.0.0.0:%d", port)))
 }
 
+func startHttpsWebServer(e *echo.Echo, port int, certPath string, keyPath string) {
+       e.Logger.Fatal(e.StartTLS(fmt.Sprintf("0.0.0.0:%d", port), certPath, keyPath))
+}
+
 func keepServerAlive() {
        forever := make(chan int)
        <-forever
 }
 
 func hello(c echo.Context) error {
-       return c.String(http.StatusOK, "Hello, World!\n")
+       return c.String(http.StatusOK, "Hello, World!")
 }
 
 func getSwagger(c echo.Context) error {
index f3a5f15..7d77b92 100644 (file)
 package main
 
 import (
+       "crypto/tls"
+       "fmt"
+       "io"
        "net/http"
        "testing"
+       "time"
 
        "github.com/deepmap/oapi-codegen/pkg/testutil"
        "github.com/getkin/kin-openapi/openapi3"
@@ -189,3 +193,32 @@ func TestGetSwagger(t *testing.T) {
        assert.Contains(t, *errorResponse.Cause, "Invalid API")
        assert.Contains(t, *errorResponse.Cause, invalidApi)
 }
+
+func TestHTTPSServer(t *testing.T) {
+       e = getEcho()
+       var port = 44333
+       go startHttpsWebServer(e, 44333, "certs/cert.pem", "certs/key.pem") //"certs/test/cert.pem", "certs/test/key.pem"
+
+       time.Sleep(100 * time.Millisecond)
+
+       tr := &http.Transport{
+               TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+       }
+
+       client := &http.Client{Transport: tr}
+       res, err := client.Get(fmt.Sprintf("https://localhost:%d", port))
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       defer res.Body.Close()
+       assert.Equal(t, res.StatusCode, res.StatusCode)
+
+       body, err := io.ReadAll(res.Body)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       expected := []byte("Hello, World!")
+       assert.Equal(t, expected, body)
+}