diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index bc453d02..649d824c 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -146,26 +146,6 @@ func ConfirmSubscription(w http.ResponseWriter, req *http.Request) { } -func ListSubscriptions(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - - uuid, _ := common.NewUUID() - respStruct := app.ListSubscriptionsResponse{} - respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" - respStruct.Metadata.RequestId = uuid - respStruct.Result.Subscriptions.Member = make([]app.TopicMemberResult, 0, 0) - - for _, topic := range app.SyncTopics.Topics { - for _, sub := range topic.Subscriptions { - tar := app.TopicMemberResult{TopicArn: topic.Arn, Protocol: sub.Protocol, - SubscriptionArn: sub.SubscriptionArn, Endpoint: sub.EndPoint, Owner: app.CurrentEnvironment.AccountID} - respStruct.Result.Subscriptions.Member = append(respStruct.Result.Subscriptions.Member, tar) - } - } - - SendResponseBack(w, req, respStruct, content) -} - func ListSubscriptionsByTopic(w http.ResponseWriter, req *http.Request) { content := req.FormValue("ContentType") topicArn := req.FormValue("TopicArn") diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index f9b2260e..f67490aa 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -74,65 +74,6 @@ func TestListSubscriptionByTopicResponse_No_Owner(t *testing.T) { } } -func TestListSubscriptionsResponse_No_Owner(t *testing.T) { - conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") - defer func() { - test.ResetApp() - }() - - // set accountID to test value so it can be populated in response - app.CurrentEnvironment.AccountID = "100010001000" - - // Create a request to pass to our handler. We don't have any query parameters for now, so we'll - // pass 'nil' as the third parameter. - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("TopicArn", "arn:aws:sns:local:000000000000:local-topic1") - req.PostForm = form - - // Prepare existant topic - topic := &app.Topic{ - Name: "UnitTestTopic1", - Arn: "arn:aws:sns:local:100010001000:UnitTestTopic1", - Subscriptions: []*app.Subscription{ - { - TopicArn: "", - Protocol: "", - SubscriptionArn: "", - EndPoint: "", - Raw: false, - FilterPolicy: &app.FilterPolicy{}, - }, - }, - } - app.SyncTopics.Topics["UnitTestTopic1"] = topic - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(ListSubscriptions) - - // Our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. - handler.ServeHTTP(rr, req) - - // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect. - expected := `` + app.CurrentEnvironment.AccountID + `` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned empty owner for subscription member: got %v want %v", - rr.Body.String(), expected) - } -} - func TestGetSubscriptionAttributesHandler_POST_Success(t *testing.T) { // Create a request to pass to our handler. We don't have any query parameters for now, so we'll // pass 'nil' as the third parameter. diff --git a/app/gosns/list_subscriptions.go b/app/gosns/list_subscriptions.go new file mode 100644 index 00000000..c63d069c --- /dev/null +++ b/app/gosns/list_subscriptions.go @@ -0,0 +1,40 @@ +package gosns + +import ( + "net/http" + + "github.com/google/uuid" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app/interfaces" + log "github.com/sirupsen/logrus" +) + +func ListSubscriptionsV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewListSubscriptionsRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - ListSubscriptionsV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + log.Debug("Listing Subscriptions") + requestId := uuid.NewString() + respStruct := models.ListSubscriptionsResponse{} + respStruct.Xmlns = models.BASE_XMLNS + respStruct.Metadata.RequestId = requestId + respStruct.Result.Subscriptions.Member = make([]models.TopicMemberResult, 0) + + for _, topic := range app.SyncTopics.Topics { + for _, sub := range topic.Subscriptions { + tar := models.TopicMemberResult{TopicArn: topic.Arn, Protocol: sub.Protocol, + SubscriptionArn: sub.SubscriptionArn, Endpoint: sub.EndPoint, Owner: app.CurrentEnvironment.AccountID} + respStruct.Result.Subscriptions.Member = append(respStruct.Result.Subscriptions.Member, tar) + } + } + + return http.StatusOK, respStruct +} diff --git a/app/gosns/list_subscriptions_test.go b/app/gosns/list_subscriptions_test.go new file mode 100644 index 00000000..53222ffb --- /dev/null +++ b/app/gosns/list_subscriptions_test.go @@ -0,0 +1,85 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestListSubcriptionsV1_NoSubscriptions(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "NoQueuesOrTopics") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.ListSubscriptionsRequest) + *v = models.ListSubscriptionsRequest{ + NextToken: "", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := ListSubscriptionsV1(r) + + response, _ := res.(models.ListSubscriptionsResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + assert.NotEqual(t, "", response.Metadata) + + assert.Len(t, response.Result.Subscriptions.Member, 0) +} + +func TestListSubcriptionsV1_MultipleSubscriptions(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.ListSubscriptionsRequest) + *v = models.ListSubscriptionsRequest{ + NextToken: "", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := ListSubscriptionsV1(r) + + response, _ := res.(models.ListSubscriptionsResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + assert.NotEqual(t, "", response.Metadata) + + assert.Len(t, response.Result.Subscriptions.Member, 2) + assert.NotEqual(t, response.Result.Subscriptions.Member[0].SubscriptionArn, response.Result.Subscriptions.Member[1].SubscriptionArn) +} + +func TestListSubscriptionsV1_request_transformer_error(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + return false + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := ListSubscriptionsV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/models/responses.go b/app/models/responses.go index 5a01c632..d25e428b 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -461,3 +461,35 @@ func (r DeleteTopicResponse) GetResult() interface{} { func (r DeleteTopicResponse) GetRequestId() string { return r.Metadata.RequestId } + +/** List Subcriptions **/ + +type TopicMemberResult struct { + TopicArn string `xml:"TopicArn"` + Protocol string `xml:"Protocol"` + SubscriptionArn string `xml:"SubscriptionArn"` + Owner string `xml:"Owner"` + Endpoint string `xml:"Endpoint"` +} + +type TopicSubscriptions struct { + Member []TopicMemberResult `xml:"member"` +} + +type ListSubscriptionsResult struct { + Subscriptions TopicSubscriptions `xml:"Subscriptions"` +} + +type ListSubscriptionsResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result ListSubscriptionsResult `xml:"ListSubscriptionsResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r ListSubscriptionsResponse) GetResult() interface{} { + return r.Result +} + +func (r ListSubscriptionsResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index 5cbdf105..1e38e913 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -248,3 +248,15 @@ type DeleteTopicRequest struct { } func (r *DeleteTopicRequest) SetAttributesFromForm(values url.Values) {} + +// ListSubscriptionsV1 + +func NewListSubscriptionsRequest() *ListSubscriptionsRequest { + return &ListSubscriptionsRequest{} +} + +type ListSubscriptionsRequest struct { + NextToken string `json:"NextToken" schema:"NextToken"` // not implemented +} + +func (r *ListSubscriptionsRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index 5b78b332..e0a6acd8 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -79,12 +79,13 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "DeleteMessageBatch": sqs.DeleteMessageBatchV1, // SNS - "Subscribe": sns.SubscribeV1, - "Unsubscribe": sns.UnsubscribeV1, - "Publish": sns.PublishV1, - "ListTopics": sns.ListTopicsV1, - "CreateTopic": sns.CreateTopicV1, - "DeleteTopic": sns.DeleteTopicV1, + "Subscribe": sns.SubscribeV1, + "Unsubscribe": sns.UnsubscribeV1, + "Publish": sns.PublishV1, + "ListTopics": sns.ListTopicsV1, + "CreateTopic": sns.CreateTopicV1, + "DeleteTopic": sns.DeleteTopicV1, + "ListSubscriptions": sns.ListSubscriptionsV1, } var routingTable = map[string]http.HandlerFunc{ @@ -92,7 +93,6 @@ var routingTable = map[string]http.HandlerFunc{ "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, - "ListSubscriptions": sns.ListSubscriptions, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/router/router_test.go b/app/router/router_test.go index 10d66b72..8c4d58b1 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -271,12 +271,13 @@ func TestActionHandler_v0_xml(t *testing.T) { "DeleteMessageBatch": sqs.DeleteMessageBatchV1, // SNS - "Subscribe": sns.SubscribeV1, - "Unsubscribe": sns.UnsubscribeV1, - "Publish": sns.PublishV1, - "ListTopics": sns.ListTopicsV1, - "CreateTopic": sns.CreateTopicV1, - "DeleteTopic": sns.DeleteTopicV1, + "Subscribe": sns.SubscribeV1, + "Unsubscribe": sns.UnsubscribeV1, + "Publish": sns.PublishV1, + "ListTopics": sns.ListTopicsV1, + "CreateTopic": sns.CreateTopicV1, + "DeleteTopic": sns.DeleteTopicV1, + "ListSubscriptions": sns.ListSubscriptionsV1, } routingTable = map[string]http.HandlerFunc{ @@ -284,7 +285,6 @@ func TestActionHandler_v0_xml(t *testing.T) { "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, - "ListSubscriptions": sns.ListSubscriptions, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/sns_messages.go b/app/sns_messages.go index 06465ef1..abb66078 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -27,7 +27,8 @@ type GetSubscriptionAttributesResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` } -/*** List Subscriptions Response */ +/*** List Subscriptions By Topic Response */ + type TopicMemberResult struct { TopicArn string `xml:"TopicArn"` Protocol string `xml:"Protocol"` @@ -40,18 +41,6 @@ type TopicSubscriptions struct { Member []TopicMemberResult `xml:"member"` } -type ListSubscriptionsResult struct { - Subscriptions TopicSubscriptions `xml:"Subscriptions"` -} - -type ListSubscriptionsResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result ListSubscriptionsResult `xml:"ListSubscriptionsResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - -/*** List Subscriptions By Topic Response */ - type ListSubscriptionsByTopicResult struct { Subscriptions TopicSubscriptions `xml:"Subscriptions"` } diff --git a/smoke_tests/sns_list_subscriptions_test.go b/smoke_tests/sns_list_subscriptions_test.go new file mode 100644 index 00000000..c0ec9e70 --- /dev/null +++ b/smoke_tests/sns_list_subscriptions_test.go @@ -0,0 +1,208 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + "encoding/xml" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/test" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sns" + + "github.com/stretchr/testify/assert" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + + "github.com/gavv/httpexpect/v2" +) + +func Test_List_Subscriptions_json_no_subscriptions(t *testing.T) { + server := generateServer() + + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + sdkResponse, err := snsClient.ListSubscriptions(context.TODO(), &sns.ListSubscriptionsInput{}) + + assert.Nil(t, err) + assert.Len(t, sdkResponse.Subscriptions, 0) +} + +func Test_List_Subscriptions_json_multiple_subscriptions(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "NoQueueAttributeDefaults") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + // add new topics to subscribe to + topicName := "new-topic-1" + createTopicResponse, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + assert.Contains(t, *createTopicResponse.TopicArn, topicName) + + topicName2 := "new-topic-2" + createTopicResponse2, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName2, + }) + assert.Contains(t, *createTopicResponse2.TopicArn, topicName2) + + // subscribe to new topics + subscribeResponse, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, topicName)), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue1")), + ReturnSubscriptionArn: true, + }) + + subscribeResponse2, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, topicName2)), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue1")), + ReturnSubscriptionArn: true, + }) + + assert.NotNil(t, subscribeResponse) + assert.NotNil(t, subscribeResponse2) + + app.SyncTopics.Lock() + defer app.SyncTopics.Unlock() + + // check listed subscriptions + sdkResponse, err := snsClient.ListSubscriptions(context.TODO(), &sns.ListSubscriptionsInput{}) + assert.Nil(t, err) + assert.Len(t, sdkResponse.Subscriptions, 2) + + assert.NotEqual(t, sdkResponse.Subscriptions[0], sdkResponse.Subscriptions[1]) + + assert.Equal(t, *sdkResponse.Subscriptions[0].TopicArn, *createTopicResponse.TopicArn) + assert.Equal(t, *sdkResponse.Subscriptions[0].SubscriptionArn, *subscribeResponse.SubscriptionArn) + + assert.Equal(t, *sdkResponse.Subscriptions[1].TopicArn, *createTopicResponse2.TopicArn) + assert.Equal(t, *sdkResponse.Subscriptions[1].SubscriptionArn, *subscribeResponse2.SubscriptionArn) +} + +func Test_List_Subscriptions_xml_no_subscriptions(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithForm(struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListSubscriptions", + Version: "2012-11-05", + }). + Expect(). + Status(http.StatusOK). + Body().Raw() + + listSubscriptionsResponseObject := models.ListSubscriptionsResponse{} + xml.Unmarshal([]byte(r), &listSubscriptionsResponseObject) + + assert.Equal(t, "http://queue.amazonaws.com/doc/2012-11-05/", listSubscriptionsResponseObject.Xmlns) + assert.Len(t, listSubscriptionsResponseObject.Result.Subscriptions.Member, 0) +} + +func Test_List_Subscriptions_xml_multiple_subscriptions(t *testing.T) { + server := generateServer() + + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + // add new topics to subscribe to + topicName := "new-topic-1" + createTopicResponse, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + assert.Contains(t, *createTopicResponse.TopicArn, topicName) + + topicName2 := "new-topic-2" + createTopicResponse2, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName2, + }) + assert.Contains(t, *createTopicResponse2.TopicArn, topicName2) + + // subscribe to new topics + subscribeResponse, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, topicName)), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue1")), + ReturnSubscriptionArn: true, + }) + + subscribeResponse2, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, topicName2)), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue1")), + ReturnSubscriptionArn: true, + }) + assert.NotNil(t, subscribeResponse) + assert.NotNil(t, subscribeResponse2) + + e := httpexpect.Default(t, server.URL) + + // check listed subscriptions + r := e.POST("/"). + WithForm(struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListSubscriptions", + Version: "2012-11-05", + }). + Expect(). + Status(http.StatusOK). + Body().Raw() + + listSubscriptionsResponseObject := models.ListSubscriptionsResponse{} + xml.Unmarshal([]byte(r), &listSubscriptionsResponseObject) + + assert.Equal(t, "http://queue.amazonaws.com/doc/2012-11-05/", listSubscriptionsResponseObject.Xmlns) + assert.Len(t, listSubscriptionsResponseObject.Result.Subscriptions.Member, 2) + assert.NotEqual(t, listSubscriptionsResponseObject.Result.Subscriptions.Member[0].TopicArn, listSubscriptionsResponseObject.Result.Subscriptions.Member[1].TopicArn) + + assert.Equal(t, listSubscriptionsResponseObject.Result.Subscriptions.Member[0].TopicArn, *createTopicResponse.TopicArn) + assert.Equal(t, listSubscriptionsResponseObject.Result.Subscriptions.Member[0].SubscriptionArn, *subscribeResponse.SubscriptionArn) + + assert.Equal(t, listSubscriptionsResponseObject.Result.Subscriptions.Member[1].TopicArn, *createTopicResponse2.TopicArn) + assert.Equal(t, listSubscriptionsResponseObject.Result.Subscriptions.Member[1].SubscriptionArn, *subscribeResponse2.SubscriptionArn) +}