diff --git a/v2/constants.go b/v2/constants.go index 1699108e..4bdc7635 100644 --- a/v2/constants.go +++ b/v2/constants.go @@ -194,8 +194,9 @@ const ( // Constants for Address const ( // Type - REST = "REST" - MQTT = "MQTT" + REST = "REST" + MQTT = "MQTT" + EMAIL = "EMAIL" ) // Constants for DeviceProfile diff --git a/v2/dtos/address.go b/v2/dtos/address.go index 5f3e7505..1949a409 100644 --- a/v2/dtos/address.go +++ b/v2/dtos/address.go @@ -12,10 +12,11 @@ import ( ) type Address struct { - Type string `json:"type" validate:"oneof='REST' 'MQTT'"` + Type string `json:"type" validate:"oneof='REST' 'MQTT' 'EMAIL'"` - Host string `json:"host" validate:"required"` - Port int `json:"port" validate:"required"` + Host string `json:"host" validate:"required_without=EmailAddresses"` + Port int `json:"port" validate:"required_without=EmailAddresses"` + EmailAddresses []string `json:"emailAddresses,omitempty" validate:"omitempty,gt=0,dive,email"` RESTAddress `json:",inline" validate:"-"` MQTTPubAddress `json:",inline" validate:"-"` @@ -83,6 +84,13 @@ func NewMQTTAddress(host string, port int, publisher string, topic string) Addre } } +func NewEmailAddress(emailAddresses []string) Address { + return Address{ + Type: v2.EMAIL, + EmailAddresses: emailAddresses, + } +} + func ToAddressModel(a Address) models.Address { var address models.Address @@ -144,3 +152,19 @@ func FromAddressModelToDTO(address models.Address) Address { } return dto } + +func ToAddressModels(dtos []Address) []models.Address { + models := make([]models.Address, len(dtos)) + for i, c := range dtos { + models[i] = ToAddressModel(c) + } + return models +} + +func FromAddressModelsToDTOs(models []models.Address) []Address { + dtos := make([]Address, len(models)) + for i, c := range models { + dtos[i] = FromAddressModelToDTO(c) + } + return dtos +} diff --git a/v2/dtos/address_test.go b/v2/dtos/address_test.go index fa38dd61..e07b2cbc 100644 --- a/v2/dtos/address_test.go +++ b/v2/dtos/address_test.go @@ -24,6 +24,7 @@ const ( testHTTPMethod = "GET" testPublisher = "testPublisher" testTopic = "testTopic" + testEmail = "test@example.com" ) var testRESTAddress = Address{ @@ -47,6 +48,8 @@ var testMQTTPubAddress = Address{ }, } +var testEmailAddress = NewEmailAddress([]string{testEmail}) + func TestAddress_UnmarshalJSON(t *testing.T) { restJsonStr := fmt.Sprintf( `{"type":"%s","host":"%s","port":%d,"path":"%s","queryParameters":"%s","httpMethod":"%s"}`, @@ -58,10 +61,8 @@ func TestAddress_UnmarshalJSON(t *testing.T) { testMQTTPubAddress.Type, testMQTTPubAddress.Host, testMQTTPubAddress.Port, testMQTTPubAddress.Publisher, testMQTTPubAddress.Topic, ) + emailJsonStr := fmt.Sprintf(`{"type":"%s","EmailAddresses":["%s"]}`, testEmailAddress.Type, testEmail) - type args struct { - data []byte - } tests := []struct { name string expected Address @@ -70,6 +71,7 @@ func TestAddress_UnmarshalJSON(t *testing.T) { }{ {"unmarshal RESTAddress with success", testRESTAddress, []byte(restJsonStr), false}, {"unmarshal MQTTPubAddress with success", testMQTTPubAddress, []byte(mqttJsonStr), false}, + {"unmarshal EmailAddress with success", testEmailAddress, []byte(emailJsonStr), false}, {"unmarshal invalid Address, empty data", Address{}, []byte{}, true}, {"unmarshal invalid Address, string data", Address{}, []byte("Invalid address"), true}, } @@ -98,6 +100,11 @@ func TestAddress_Validate(t *testing.T) { noMQTTPublisher.Publisher = "" noMQTTTopic := testMQTTPubAddress noMQTTTopic.Topic = "" + + validEmail := testEmailAddress + invalidEmailAddress := testEmailAddress + invalidEmailAddress.EmailAddresses = []string{"test.example.com"} + tests := []struct { name string dto Address @@ -108,6 +115,8 @@ func TestAddress_Validate(t *testing.T) { {"valid MQTTPubAddress", validMQTT, false}, {"invalid MQTTPubAddress, no MQTT publisher", noMQTTPublisher, true}, {"invalid MQTTPubAddress, no MQTT Topic", noMQTTTopic, true}, + {"valid EmailAddress", validEmail, false}, + {"invalid EmailAddress", invalidEmailAddress, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/v2/dtos/requests/subscription.go b/v2/dtos/requests/subscription.go index b0cfafd5..243cf922 100644 --- a/v2/dtos/requests/subscription.go +++ b/v2/dtos/requests/subscription.go @@ -15,6 +15,8 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/models" ) +var supportedChannelTypes = []string{v2.EMAIL, v2.REST} + // AddSubscriptionRequest defines the Request Content for POST Subscription DTO. // This object and its properties correspond to the AddSubscriptionRequest object in the APIv2 specification: // https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/support-notifications/2.x#/AddSubscriptionRequest @@ -26,7 +28,18 @@ type AddSubscriptionRequest struct { // Validate satisfies the Validator interface func (request AddSubscriptionRequest) Validate() error { err := v2.Validate(request) - return err + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + for _, c := range request.Subscription.Channels { + err = c.Validate() + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } else if !contains(supportedChannelTypes, c.Type) { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "MQTT is not valid type for Channel", nil) + } + } + return nil } // UnmarshalJSON implements the Unmarshaler interface for the AddSubscriptionRequest type @@ -43,7 +56,7 @@ func (request *AddSubscriptionRequest) UnmarshalJSON(b []byte) error { // validate AddSubscriptionRequest DTO if err := request.Validate(); err != nil { - return err + return errors.NewCommonEdgeXWrapper(err) } return nil } @@ -68,7 +81,18 @@ type UpdateSubscriptionRequest struct { // Validate satisfies the Validator interface func (request UpdateSubscriptionRequest) Validate() error { err := v2.Validate(request) - return err + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + for _, c := range request.Subscription.Channels { + err = c.Validate() + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } else if !contains(supportedChannelTypes, c.Type) { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "MQTT is not valid type for Channel", nil) + } + } + return nil } // UnmarshalJSON implements the Unmarshaler interface for the UpdateSubscriptionRequest type @@ -85,7 +109,7 @@ func (request *UpdateSubscriptionRequest) UnmarshalJSON(b []byte) error { // validate UpdateSubscriptionRequest DTO if err := request.Validate(); err != nil { - return err + return errors.NewCommonEdgeXWrapper(err) } return nil } @@ -93,7 +117,7 @@ func (request *UpdateSubscriptionRequest) UnmarshalJSON(b []byte) error { // ReplaceSubscriptionModelFieldsWithDTO replace existing Subscription's fields with DTO patch func ReplaceSubscriptionModelFieldsWithDTO(s *models.Subscription, patch dtos.UpdateSubscription) { if patch.Channels != nil { - s.Channels = dtos.ToChannelModels(patch.Channels) + s.Channels = dtos.ToAddressModels(patch.Channels) } if patch.Categories != nil { s.Categories = patch.Categories @@ -128,3 +152,12 @@ func NewUpdateSubscriptionRequest(dto dtos.UpdateSubscription) UpdateSubscriptio Subscription: dto, } } + +func contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} diff --git a/v2/dtos/requests/subscription_test.go b/v2/dtos/requests/subscription_test.go index 4ea8311c..37d8a96f 100644 --- a/v2/dtos/requests/subscription_test.go +++ b/v2/dtos/requests/subscription_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/models" "github.com/stretchr/testify/assert" @@ -21,8 +20,8 @@ var ( testSubscriptionName = "subscriptionName" testSubscriptionCategories = []string{"category1", "category2"} testSubscriptionLabels = []string{"label"} - testSubscriptionChannels = []dtos.Channel{ - {Type: models.Email, EmailAddresses: []string{"test@example.com"}}, + testSubscriptionChannels = []dtos.Address{ + dtos.NewEmailAddress([]string{"test@example.com"}), } testSubscriptionDescription = "description" testSubscriptionReceiver = "receiver" @@ -32,32 +31,16 @@ var ( ) func addSubscriptionRequestData() AddSubscriptionRequest { - return AddSubscriptionRequest{ - BaseRequest: common.BaseRequest{ - RequestId: ExampleUUID, - Versionable: common.NewVersionable(), - }, - Subscription: dtos.Subscription{ - Name: testSubscriptionName, - Categories: testSubscriptionCategories, - Labels: testSubscriptionLabels, - Channels: testSubscriptionChannels, - Description: testSubscriptionDescription, - Receiver: testSubscriptionReceiver, - ResendLimit: testSubscriptionResendLimit, - ResendInterval: testSubscriptionResendInterval, - }, - } -} - -func updateSubscriptionRequestData() UpdateSubscriptionRequest { - return UpdateSubscriptionRequest{ - BaseRequest: common.BaseRequest{ - RequestId: ExampleUUID, - Versionable: common.NewVersionable(), - }, - Subscription: updateSubscriptionData(), - } + return NewAddSubscriptionRequest(dtos.Subscription{ + Name: testSubscriptionName, + Categories: testSubscriptionCategories, + Labels: testSubscriptionLabels, + Channels: testSubscriptionChannels, + Description: testSubscriptionDescription, + Receiver: testSubscriptionReceiver, + ResendLimit: testSubscriptionResendLimit, + ResendInterval: testSubscriptionResendInterval, + }) } func updateSubscriptionData() dtos.UpdateSubscription { @@ -97,18 +80,14 @@ func TestAddSubscriptionRequest_Validate(t *testing.T) { subscriptionNameWithReservedChars.Subscription.Name = namesWithReservedChar[0] noChannel := addSubscriptionRequestData() - noChannel.Subscription.Channels = []dtos.Channel{} - invalidChannelType := addSubscriptionRequestData() - invalidChannelType.Subscription.Channels = []dtos.Channel{ - {Type: unsupportedChannelType, EmailAddresses: []string{"test@example.com"}}, - } + noChannel.Subscription.Channels = []dtos.Address{} invalidEmailAddress := addSubscriptionRequestData() - invalidEmailAddress.Subscription.Channels = []dtos.Channel{ - {Type: models.Email, EmailAddresses: []string{"test.example.com"}}, + invalidEmailAddress.Subscription.Channels = []dtos.Address{ + dtos.NewEmailAddress([]string{"test.example.com"}), } - invalidUrl := addSubscriptionRequestData() - invalidUrl.Subscription.Channels = []dtos.Channel{ - {Type: models.Rest, Url: "http127.0.0.1"}, + unsupportedChannelType := addSubscriptionRequestData() + unsupportedChannelType.Subscription.Channels = []dtos.Address{ + dtos.NewMQTTAddress("host", 123, "publisher", "topic"), } noCategories := addSubscriptionRequestData() @@ -142,9 +121,8 @@ func TestAddSubscriptionRequest_Validate(t *testing.T) { {"invalid, no subscription name", noSubscriptionName, true}, {"invalid, subscription name containing reserved chars", subscriptionNameWithReservedChars, true}, {"invalid, no channels specified", noChannel, true}, - {"invalid, unsupported channel type", invalidChannelType, true}, {"invalid, email address is invalid", invalidEmailAddress, true}, - {"invalid, url is invalid", invalidUrl, true}, + {"invalid, unsupported channel type", unsupportedChannelType, true}, {"invalid, no categories and labels specified", noCategoriesAndLabels, true}, {"invalid, unsupported category type", categoryNameWithReservedChar, true}, {"invalid, no receiver specified", noReceiver, true}, @@ -160,15 +138,15 @@ func TestAddSubscriptionRequest_Validate(t *testing.T) { } func TestAddSubscription_UnmarshalJSON(t *testing.T) { - valid := addSubscriptionRequestData() - jsonData, _ := json.Marshal(addSubscriptionRequestData()) + validAddSubscriptionRequest := addSubscriptionRequestData() + jsonData, _ := json.Marshal(validAddSubscriptionRequest) tests := []struct { name string expected AddSubscriptionRequest data []byte wantErr bool }{ - {"unmarshal AddSubscriptionRequest with success", valid, jsonData, false}, + {"unmarshal AddSubscriptionRequest with success", validAddSubscriptionRequest, jsonData, false}, {"unmarshal invalid AddSubscriptionRequest, empty data", AddSubscriptionRequest{}, []byte{}, true}, {"unmarshal invalid AddSubscriptionRequest, string data", AddSubscriptionRequest{}, []byte("Invalid AddSubscriptionRequest"), true}, } @@ -193,7 +171,7 @@ func TestAddSubscriptionReqToSubscriptionModels(t *testing.T) { Name: testSubscriptionName, Categories: testSubscriptionCategories, Labels: testSubscriptionLabels, - Channels: dtos.ToChannelModels(testSubscriptionChannels), + Channels: dtos.ToAddressModels(testSubscriptionChannels), Description: testSubscriptionDescription, Receiver: testSubscriptionReceiver, ResendLimit: testSubscriptionResendLimit, @@ -209,7 +187,7 @@ func TestUpdateSubscriptionRequest_Validate(t *testing.T) { invalidUUID := "invalidUUID" invalidReceiverName := namesWithReservedChar[0] - valid := updateSubscriptionRequestData() + valid := NewUpdateSubscriptionRequest(updateSubscriptionData()) noReqId := valid noReqId.RequestId = "" invalidReqId := valid @@ -225,26 +203,22 @@ func TestUpdateSubscriptionRequest_Validate(t *testing.T) { invalidEmptyName := valid invalidEmptyName.Subscription.Name = &emptyString - invalidChannelType := updateSubscriptionRequestData() - invalidChannelType.Subscription.Channels = []dtos.Channel{ - {Type: unsupportedChannelType, EmailAddresses: []string{"test@example.com"}}, - } - invalidEmailAddress := updateSubscriptionRequestData() - invalidEmailAddress.Subscription.Channels = []dtos.Channel{ - {Type: models.Email, EmailAddresses: []string{"test.example.com"}}, + invalidEmailAddress := NewUpdateSubscriptionRequest(updateSubscriptionData()) + invalidEmailAddress.Subscription.Channels = []dtos.Address{ + dtos.NewEmailAddress([]string{"test.example.com"}), } - invalidUrl := updateSubscriptionRequestData() - invalidUrl.Subscription.Channels = []dtos.Channel{ - {Type: models.Rest, Url: "http127.0.0.1"}, + unsupportedChannelType := NewUpdateSubscriptionRequest(updateSubscriptionData()) + unsupportedChannelType.Subscription.Channels = []dtos.Address{ + dtos.NewMQTTAddress("host", 123, "publisher", "topic"), } - categoryNameWithReservedChar := updateSubscriptionRequestData() + categoryNameWithReservedChar := NewUpdateSubscriptionRequest(updateSubscriptionData()) categoryNameWithReservedChar.Subscription.Categories = []string{namesWithReservedChar[0]} - receiverNameWithReservedChars := updateSubscriptionRequestData() + receiverNameWithReservedChars := NewUpdateSubscriptionRequest(updateSubscriptionData()) receiverNameWithReservedChars.Subscription.Receiver = &invalidReceiverName - invalidResendInterval := updateSubscriptionRequestData() + invalidResendInterval := NewUpdateSubscriptionRequest(updateSubscriptionData()) invalidResendIntervalValue := "10" invalidResendInterval.Subscription.ResendInterval = &invalidResendIntervalValue @@ -256,15 +230,12 @@ func TestUpdateSubscriptionRequest_Validate(t *testing.T) { {"valid", valid, false}, {"valid, no request ID", noReqId, false}, {"invalid, request ID is not an UUID", invalidReqId, true}, - {"valid, only ID", validOnlyId, false}, {"invalid, invalid ID", invalidId, true}, {"valid, only name", validOnlyName, false}, {"invalid, empty name", invalidEmptyName, true}, - - {"invalid, unsupported channel type", invalidChannelType, true}, {"invalid, email address is invalid", invalidEmailAddress, true}, - {"invalid, url is invalid", invalidUrl, true}, + {"invalid, unsupported channel type", unsupportedChannelType, true}, {"invalid, category name containing reserved chars", categoryNameWithReservedChar, true}, {"invalid, receiver name containing reserved chars", receiverNameWithReservedChars, true}, {"invalid, resendInterval is not specified in ISO8601 format", invalidResendInterval, true}, @@ -278,15 +249,15 @@ func TestUpdateSubscriptionRequest_Validate(t *testing.T) { } func TestUpdateSubscriptionRequest_UnmarshalJSON(t *testing.T) { - valid := updateSubscriptionRequestData() - jsonData, _ := json.Marshal(updateSubscriptionRequestData()) + validUpdateSubscriptionRequest := NewUpdateSubscriptionRequest(updateSubscriptionData()) + jsonData, _ := json.Marshal(validUpdateSubscriptionRequest) tests := []struct { name string expected UpdateSubscriptionRequest data []byte wantErr bool }{ - {"unmarshal UpdateSubscriptionRequest with success", valid, jsonData, false}, + {"unmarshal UpdateSubscriptionRequest with success", validUpdateSubscriptionRequest, jsonData, false}, {"unmarshal invalid UpdateSubscriptionRequest, empty data", UpdateSubscriptionRequest{}, []byte{}, true}, {"unmarshal invalid UpdateSubscriptionRequest, string data", UpdateSubscriptionRequest{}, []byte("Invalid UpdateSubscriptionRequest"), true}, } @@ -315,7 +286,7 @@ func TestReplaceSubscriptionModelFieldsWithDTO(t *testing.T) { assert.Equal(t, testSubscriptionCategories, subscription.Categories) assert.Equal(t, testSubscriptionLabels, subscription.Labels) - assert.Equal(t, dtos.ToChannelModels(testSubscriptionChannels), subscription.Channels) + assert.Equal(t, dtos.ToAddressModels(testSubscriptionChannels), subscription.Channels) assert.Equal(t, testSubscriptionDescription, subscription.Description) assert.Equal(t, testSubscriptionReceiver, subscription.Receiver) assert.Equal(t, testSubscriptionResendLimit, subscription.ResendLimit) diff --git a/v2/dtos/subscription.go b/v2/dtos/subscription.go index a09a5caa..f5bd61c0 100644 --- a/v2/dtos/subscription.go +++ b/v2/dtos/subscription.go @@ -14,7 +14,7 @@ import ( type Subscription struct { Id string `json:"id,omitempty" validate:"omitempty,uuid"` Name string `json:"name" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` - Channels []Channel `json:"channels" validate:"required,gt=0,dive"` + Channels []Address `json:"channels" validate:"required,gt=0,dive"` Receiver string `json:"receiver" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` Categories []string `json:"categories,omitempty" validate:"required_without=Labels,omitempty,gt=0,dive,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` Labels []string `json:"labels,omitempty" validate:"required_without=Categories,omitempty,gt=0,dive,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` @@ -30,7 +30,7 @@ type Subscription struct { type UpdateSubscription struct { Id *string `json:"id,omitempty" validate:"required_without=Name,edgex-dto-uuid"` Name *string `json:"name,omitempty" validate:"required_without=Id,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` - Channels []Channel `json:"channels,omitempty" validate:"omitempty,dive"` + Channels []Address `json:"channels,omitempty" validate:"omitempty,dive"` Receiver *string `json:"receiver,omitempty" validate:"omitempty,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` Categories []string `json:"categories,omitempty" validate:"omitempty,dive,gt=0,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` Labels []string `json:"labels,omitempty" validate:"omitempty,dive,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"` @@ -39,20 +39,12 @@ type UpdateSubscription struct { ResendInterval *string `json:"resendInterval,omitempty" validate:"omitempty,edgex-dto-frequency"` } -// Channel and its properties are defined in the APIv2 specification: -// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/support-notifications/2.x#/Channel -type Channel struct { - Type string `json:"type" validate:"required,oneof='REST' 'EMAIL'"` - EmailAddresses []string `json:"emailAddresses,omitempty" validate:"omitempty,required_without=Url,gt=0,dive,email"` - Url string `json:"url,omitempty" validate:"omitempty,required_without=EmailAddresses,uri"` -} - // ToSubscriptionModel transforms the Subscription DTO to the Subscription Model func ToSubscriptionModel(s Subscription) models.Subscription { var m models.Subscription m.Categories = s.Categories m.Labels = s.Labels - m.Channels = ToChannelModels(s.Channels) + m.Channels = ToAddressModels(s.Channels) m.Created = s.Created m.Modified = s.Modified m.Description = s.Description @@ -78,7 +70,7 @@ func FromSubscriptionModelToDTO(s models.Subscription) Subscription { return Subscription{ Categories: s.Categories, Labels: s.Labels, - Channels: FromChannelModelsToDTOs(s.Channels), + Channels: FromAddressModelsToDTOs(s.Channels), Created: s.Created, Modified: s.Modified, Description: s.Description, @@ -98,37 +90,3 @@ func FromSubscriptionModelsToDTOs(subscruptions []models.Subscription) []Subscri } return dtos } - -func ToChannelModels(channelDTOs []Channel) []models.Channel { - channelModels := make([]models.Channel, len(channelDTOs)) - for i, c := range channelDTOs { - channelModels[i] = ToChannelModel(c) - } - return channelModels -} - -func ToChannelModel(c Channel) models.Channel { - return models.Channel{ - Type: models.ChannelType(c.Type), - EmailAddresses: c.EmailAddresses, - Url: c.Url, - } -} - -// FromChannelModelsToDTOs transforms the Channel model array to the Channel DTO array -func FromChannelModelsToDTOs(cs []models.Channel) []Channel { - dtos := make([]Channel, len(cs)) - for i, c := range cs { - dtos[i] = FromChannelModelToDTO(c) - } - return dtos -} - -// FromChannelModelToDTO transforms the Channel model to the Channel DTO -func FromChannelModelToDTO(c models.Channel) Channel { - return Channel{ - Type: string(c.Type), - EmailAddresses: c.EmailAddresses, - Url: c.Url, - } -} diff --git a/v2/models/subscription.go b/v2/models/subscription.go index df5789a5..9b0ee21c 100644 --- a/v2/models/subscription.go +++ b/v2/models/subscription.go @@ -12,7 +12,7 @@ type Subscription struct { Timestamps Categories []string Labels []string - Channels []Channel + Channels []Address Created int64 Modified int64 Description string @@ -22,11 +22,3 @@ type Subscription struct { ResendLimit int64 ResendInterval string } - -// Channel and its properties are defined in the APIv2 specification: -// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/support-notifications/2.x#/Channel -type Channel struct { - Type ChannelType - EmailAddresses []string - Url string -}