From 71373a65f664a7065a01e83f33c41c91403ac52e Mon Sep 17 00:00:00 2001 From: Nicholas Raffone Date: Tue, 9 Jul 2024 14:07:31 +0900 Subject: [PATCH] add sns listtopics support minor fixes add not implemented comment to sns model reset tests affecting new tests add debug message when listing sns topics remove merge remnants, minor fixups update listtopics test to handle new mock config --- app/gosns/gosns.go | 18 ---- app/gosns/gosns_test.go | 38 ++------ app/gosns/list_topics.go | 40 +++++++++ app/gosns/list_topics_test.go | 93 +++++++++++++++++++ app/models/responses.go | 27 ++++++ app/models/sns.go | 12 +++ app/router/router.go | 4 +- app/router/router_test.go | 4 +- app/sns_messages.go | 19 ---- smoke_tests/fixtures/requests.go | 8 ++ smoke_tests/sns_create_topic_test.go | 11 ++- smoke_tests/sns_list_topics_test.go | 129 +++++++++++++++++++++++++++ 12 files changed, 326 insertions(+), 77 deletions(-) create mode 100644 app/gosns/list_topics.go create mode 100644 app/gosns/list_topics_test.go create mode 100644 smoke_tests/sns_list_topics_test.go diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 16b0f038..d23f7759 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -85,24 +85,6 @@ func createPemFile() (privkey *rsa.PrivateKey, pemkey []byte, err error) { return } -func ListTopics(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - - respStruct := app.ListTopicsResponse{} - respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" - uuid, _ := common.NewUUID() - respStruct.Metadata = app.ResponseMetadata{RequestId: uuid} - - respStruct.Result.Topics.Member = make([]app.TopicArnResult, 0, 0) - log.Println("Listing Topics") - for _, topic := range app.SyncTopics.Topics { - ta := app.TopicArnResult{TopicArn: topic.Arn} - respStruct.Result.Topics.Member = append(respStruct.Result.Topics.Member, ta) - } - - SendResponseBack(w, req, respStruct, content) -} - func signMessage(privkey *rsa.PrivateKey, snsMsg *app.SNSMessage) (string, error) { fs, err := formatSignature(snsMsg) if err != nil { diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index a5ddce98..25775a7a 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -15,36 +15,6 @@ import ( "github.com/Admiral-Piett/goaws/app/common" ) -func TestListTopicshandler_POST_NoTopics(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. - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(ListTopics) - - // 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 := "" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - // TODO - add a subscription and I think this should work func TestListSubscriptionByTopicResponse_No_Owner(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") @@ -229,6 +199,10 @@ func TestGetSubscriptionAttributesHandler_POST_Success(t *testing.T) { t.Fatal(err) } + defer func() { + test.ResetApp() + }() + topicName := "testing" topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName subArn, _ := common.NewUUID() @@ -293,6 +267,10 @@ func TestSetSubscriptionAttributesHandler_FilterPolicy_POST_Success(t *testing.T t.Fatal(err) } + defer func() { + test.ResetApp() + }() + topicName := "testing" topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName subArn, _ := common.NewUUID() diff --git a/app/gosns/list_topics.go b/app/gosns/list_topics.go new file mode 100644 index 00000000..c7a94385 --- /dev/null +++ b/app/gosns/list_topics.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 ListTopicsV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewListTopicsRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - ListTopicsV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + log.Debug("Listing Topics") + arnList := make([]models.TopicArnResult, 0) + + for _, topic := range app.SyncTopics.Topics { + ta := models.TopicArnResult{TopicArn: topic.Arn} + arnList = append(arnList, ta) + } + + requestId := uuid.NewString() + respStruct := models.ListTopicsResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.ListTopicsResult{Topics: models.TopicNamestype{Member: arnList}}, + Metadata: app.ResponseMetadata{RequestId: requestId}, + } + + return http.StatusOK, respStruct +} diff --git a/app/gosns/list_topics_test.go b/app/gosns/list_topics_test.go new file mode 100644 index 00000000..f025f1e7 --- /dev/null +++ b/app/gosns/list_topics_test.go @@ -0,0 +1,93 @@ +package gosns + +import ( + "fmt" + "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 TestListTopicsV1_NoTopics(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.ListTopicsRequest) + *v = models.ListTopicsRequest{ + NextToken: "", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := ListTopicsV1(r) + + response, _ := res.(models.ListTopicsResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + assert.NotEqual(t, "", response.Metadata) + + assert.Len(t, response.Result.Topics.Member, 0) +} + +func TestListTopicsV1_BaseTopics(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) { + v := resultingStruct.(*models.ListTopicsRequest) + *v = models.ListTopicsRequest{ + NextToken: "", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := ListTopicsV1(r) + + response, _ := res.(models.ListTopicsResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + assert.NotEqual(t, "", response.Metadata) + + assert.Len(t, response.Result.Topics.Member, 4) + + topicArnVisited := map[string]bool{} + + for _, member := range response.Result.Topics.Member { + _, ok := topicArnVisited[member.TopicArn] + assert.False(t, ok, fmt.Sprintf("Found duplicated listed arn entry: %s", member.TopicArn)) + topicArnVisited[member.TopicArn] = true + } +} + +func TestListTopicsV1_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, _ := ListTopicsV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/models/responses.go b/app/models/responses.go index a82e4bb4..db7d6eed 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -420,3 +420,30 @@ func (r PublishResponse) GetResult() interface{} { func (r PublishResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** List Topics ***/ +type TopicArnResult struct { + TopicArn string `xml:"TopicArn"` + NextToken string `xml:"NextToken"` // not implemented +} +type TopicNamestype struct { + Member []TopicArnResult `xml:"member"` +} + +type ListTopicsResult struct { + Topics TopicNamestype `xml:"Topics"` +} + +type ListTopicsResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result ListTopicsResult `xml:"ListTopicsResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r ListTopicsResponse) GetResult() interface{} { + return r.Result +} + +func (r ListTopicsResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index 422e8cd2..a4859d1a 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -224,3 +224,15 @@ func (r *PublishRequest) SetAttributesFromForm(values url.Values) { } } } + +// ListTopics + +func NewListTopicsRequest() *ListTopicsRequest { + return &ListTopicsRequest{} +} + +type ListTopicsRequest struct { + NextToken string `json:"NextToken" schema:"NextToken"` // not implemented +} + +func (r *ListTopicsRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index 48fa8571..e05412f7 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -79,15 +79,15 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "DeleteMessageBatch": sqs.DeleteMessageBatchV1, // SNS - "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, "Unsubscribe": sns.UnsubscribeV1, "Publish": sns.PublishV1, + "ListTopics": sns.ListTopicsV1, + "CreateTopic": sns.CreateTopicV1, } var routingTable = map[string]http.HandlerFunc{ // SNS - "ListTopics": sns.ListTopics, "DeleteTopic": sns.DeleteTopic, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, diff --git a/app/router/router_test.go b/app/router/router_test.go index b2f918ae..c2b92438 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -271,15 +271,15 @@ func TestActionHandler_v0_xml(t *testing.T) { "DeleteMessageBatch": sqs.DeleteMessageBatchV1, // SNS - "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, "Unsubscribe": sns.UnsubscribeV1, "Publish": sns.PublishV1, + "ListTopics": sns.ListTopicsV1, + "CreateTopic": sns.CreateTopicV1, } routingTable = map[string]http.HandlerFunc{ // SNS - "ListTopics": sns.ListTopics, "DeleteTopic": sns.DeleteTopic, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, diff --git a/app/sns_messages.go b/app/sns_messages.go index 5917e54c..d720f17b 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -1,24 +1,5 @@ package app -/*** List Topics Response */ -type TopicArnResult struct { - TopicArn string `xml:"TopicArn"` -} - -type TopicNamestype struct { - Member []TopicArnResult `xml:"member"` -} - -type ListTopicsResult struct { - Topics TopicNamestype `xml:"Topics"` -} - -type ListTopicsResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result ListTopicsResult `xml:"ListTopicsResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Set Subscription Response ***/ type SetSubscriptionAttributesResponse struct { Xmlns string `xml:"xmlns,attr"` diff --git a/smoke_tests/fixtures/requests.go b/smoke_tests/fixtures/requests.go index 39cf15be..c8db931a 100644 --- a/smoke_tests/fixtures/requests.go +++ b/smoke_tests/fixtures/requests.go @@ -15,6 +15,14 @@ var ListQueuesRequestBodyXML = struct { Version: "2012-11-05", } +var ListTopicsRequestBodyXML = struct { + Action string `xml:"Action"` + Version string `xml:"Version"` +}{ + Action: "ListTopics", + Version: "2012-11-05", +} + var GetQueueAttributesRequestBodyXML = struct { Action string `xml:"Action"` Version string `xml:"Version"` diff --git a/smoke_tests/sns_create_topic_test.go b/smoke_tests/sns_create_topic_test.go index 2224e3c9..4dfe1679 100644 --- a/smoke_tests/sns_create_topic_test.go +++ b/smoke_tests/sns_create_topic_test.go @@ -6,7 +6,6 @@ import ( "net/http" "testing" - "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/test" "github.com/aws/aws-sdk-go-v2/aws" @@ -50,7 +49,7 @@ func Test_CreateTopicV1_json_success(t *testing.T) { Expect(). Status(http.StatusOK). Body().Raw() - r2 := app.ListTopicsResponse{} + r2 := models.ListTopicsResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, 1, len(r2.Result.Topics.Member)) assert.Contains(t, r2.Result.Topics.Member[0].TopicArn, topicName) @@ -97,7 +96,7 @@ func Test_CreateTopicV1_json_existant_topic(t *testing.T) { Expect(). Status(http.StatusOK). Body().Raw() - r2 := app.ListTopicsResponse{} + r2 := models.ListTopicsResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, 1, len(r2.Result.Topics.Member)) assert.Contains(t, r2.Result.Topics.Member[0].TopicArn, topicName) @@ -145,7 +144,7 @@ func Test_CreateTopicV1_json_add_multiple_topics(t *testing.T) { Expect(). Status(http.StatusOK). Body().Raw() - r2 := app.ListTopicsResponse{} + r2 := models.ListTopicsResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, 2, len(r2.Result.Topics.Member)) } @@ -191,7 +190,7 @@ func Test_CreateTopicV1_xml_success(t *testing.T) { Expect(). Status(http.StatusOK). Body().Raw() - r3 := app.ListTopicsResponse{} + r3 := models.ListTopicsResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, 1, len(r3.Result.Topics.Member)) assert.Contains(t, r3.Result.Topics.Member[0].TopicArn, topicName) @@ -249,7 +248,7 @@ func Test_CreateTopicV1_xml_existant_topic(t *testing.T) { Expect(). Status(http.StatusOK). Body().Raw() - r3 := app.ListTopicsResponse{} + r3 := models.ListTopicsResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, 1, len(r3.Result.Topics.Member)) assert.Contains(t, r3.Result.Topics.Member[0].TopicArn, topicName) diff --git a/smoke_tests/sns_list_topics_test.go b/smoke_tests/sns_list_topics_test.go new file mode 100644 index 00000000..41620ce0 --- /dev/null +++ b/smoke_tests/sns_list_topics_test.go @@ -0,0 +1,129 @@ +package smoke_tests + +import ( + "context" + "net/http" + "testing" + + "encoding/xml" + + "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" + + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + + "github.com/gavv/httpexpect/v2" +) + +func Test_List_Topics_json_no_topics(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.ListTopics(context.TODO(), &sns.ListTopicsInput{}) + + assert.Nil(t, err) + assert.Len(t, sdkResponse.Topics, 0) +} + +func Test_List_Topics_json_multiple_topics(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) + + topicName1 := "topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName1), + }) + + topicName2 := "topic-2" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName2), + }) + + sdkResponse, err := snsClient.ListTopics(context.TODO(), &sns.ListTopicsInput{}) + + assert.Nil(t, err) + assert.Len(t, sdkResponse.Topics, 2) + assert.NotEqual(t, sdkResponse.Topics[0].TopicArn, sdkResponse.Topics[1].TopicArn) +} + +func Test_List_Topics_xml_no_topics(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithForm(sf.ListTopicsRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + listTopicsResponseObject := models.ListTopicsResponse{} + xml.Unmarshal([]byte(r), &listTopicsResponseObject) + + assert.Equal(t, "http://queue.amazonaws.com/doc/2012-11-05/", listTopicsResponseObject.Xmlns) + assert.Len(t, listTopicsResponseObject.Result.Topics.Member, 0) +} + +func Test_ListTopics_xml_multiple_topics(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) + + topicName1 := "topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName1), + }) + + topicName2 := "topic-2" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName2), + }) + + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithForm(sf.ListTopicsRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + listTopicsResponseObject := models.ListTopicsResponse{} + xml.Unmarshal([]byte(r), &listTopicsResponseObject) + + assert.Equal(t, "http://queue.amazonaws.com/doc/2012-11-05/", listTopicsResponseObject.Xmlns) + assert.Len(t, listTopicsResponseObject.Result.Topics.Member, 2) + assert.NotEqual(t, listTopicsResponseObject.Result.Topics.Member[0].TopicArn, listTopicsResponseObject.Result.Topics.Member[1].TopicArn) +}