From 95ade1745ddc8a6e7d39b6d2ce26d3a41cd90bf1 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:05:43 +0100 Subject: [PATCH 01/41] Implement SNS:PublishBatch --- app/gosns/gosns.go | 121 +++++++++++++++++++++++++++++++++++++++++++ app/router/router.go | 1 + app/sns_messages.go | 26 ++++++++++ app/sqs_messages.go | 8 +++ 4 files changed, 156 insertions(+) create mode 100644 app/sqs_messages.go diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index bc453d02..9e6eb28e 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -290,6 +290,127 @@ func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. + +func PublishBatch(w http.ResponseWriter, req *http.Request) { + content := req.FormValue("ContentType") + topicArn := req.FormValue("TopicArn") + + arnSegments := strings.Split(topicArn, ":") + topicName := arnSegments[len(arnSegments)-1] + topic, ok := app.SyncTopics.Topics[topicName] + if !ok { + createErrorResponse(w, req, "TopicNotFound") + return + } + + var batchMessageIdToMessageBody map[string]string + var batchMessageIdToMessageStructure map[string]string + var batchMessageIdToMessageAttributes map[string]map[string]app.MessageAttributeValue + + for memberIndex := 1; true; memberIndex++ { + if memberIndex > 10 { + createErrorResponse(w, req, "TooManyEntriesInBatchRequest") + return + } + thisMessageFormKey := "PublishBatchRequestEntries.member." + strconv.Itoa(memberIndex) + if req.FormValue(thisMessageFormKey) == "" { + break + } + + batchMessageId := req.FormValue(thisMessageFormKey + ".Id") + if _, ok := batchMessageIdToMessageBody[batchMessageId]; ok { + createErrorResponse(w, req, "BatchEntryIdsNotDistinct") + return + } + + thisMessageBody := req.FormValue(thisMessageFormKey + ".Message") + thisMessageStructure := req.FormValue(thisMessageFormKey + ".MessageStructure") + + thisMessageAttributes := make(map[string]app.MessageAttributeValue) + + for i := 1; true; i++ { + name := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Name", thisMessageFormKey, i)) + if name == "" { + break + } + + dataType := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.DataType", thisMessageFormKey, i)) + if dataType == "" { + log.Warnf("DataType of %s.MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", thisMessageFormKey, name) + continue + } + + // StringListValue and BinaryListValue is currently not implemented + for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { + value := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.%s", thisMessageFormKey, i, valueKey)) + if value != "" { + attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} + } + } + + if _, ok := attributes[name]; !ok { + log.Warnf("StringValue or BinaryValue of %s.MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", thisMessageFormKey, name) + } + } + + batchMessageIdToMessageBody[batchMessageId] = thisMessageBody + batchMessageIdToMessageStructure[batchMessageId] = thisMessageStructure + batchMessageIdToMessageAttributes[batchMessageId] = thisMessageAttributes + } + + if len(batchMessageIdToMessageBody) == 0 { + createErrorResponse(w, req, "EmptyBatchRequest") + return + } + + successfulEntries := []app.PublishBatchResultEntry{} + failedEntries := []app.PublishBatchErrorEntry{} + for batchMessageId, messageBody := range batchMessageIdToMessageBody { + messageStructure := batchMessageIdToMessageStructure[batchMessageId] + messageAttributes := batchMessageIdToMessageAttributes[batchMessageId] + for _, sub := range topic.Subscriptions { + switch app.Protocol(subs.Protocol) { + case app.ProtocolSQS: + if err := publishSQS(subs, messageBody, messageAttributes, subject, topicArn, topicName, messageStructure); err != nil { + er := app.SnsErrors[err.Error()] + failedEntries = append(failedEntries, app.PublishBatchErrorEntry{ + Code: er.Code, + Id: batchMessageId, + Message: er.Message, + SenderFault: true, + }) + } else { + msgId, _ := common.NewUUID() + successfulEntries = append(successfulEntries, app.PublishBatchResultEntry{ + Id: batchMessageId, + MessageId: msgId, + }) + } + case app.ProtocolHTTP: + fallthrough + case app.ProtocolHTTPS: + publishHTTP(subs, messageBody, messageAttributes, subject, topicArn) + msgId, _ := common.NewUUID() + successfulEntries = append(successfulEntries, app.PublishBatchResultEntry{ + Id: batchMessageId, + MessageId: msgId, + }) + } + } + } + + uuid, _ := common.NewUUID() + respStruct := app.PublishBatchResponse{ + "https://sns.amazonaws.com/doc/2010-03-31/", + app.PublishBatchResult{ + Successful: app.PublishBatchResultEntries{Member: successfulEntries}, + Failed: app.PublishBatchErrorEntries{Member: failedEntries}, + }, + app.ResponseMetadata{RequestId: uuid}, + } + SendResponseBack(w, req, respStruct, content) +} + func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { log.WithFields(log.Fields{ "sns": msg, diff --git a/app/router/router.go b/app/router/router.go index 5b78b332..b624f208 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -93,6 +93,7 @@ var routingTable = map[string]http.HandlerFunc{ "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, "ListSubscriptions": sns.ListSubscriptions, + "PublishBatch": sns.PublishBatch, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/sns_messages.go b/app/sns_messages.go index 06465ef1..9395a7a5 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -61,3 +61,29 @@ type ListSubscriptionsByTopicResponse struct { Result ListSubscriptionsByTopicResult `xml:"ListSubscriptionsByTopicResult"` Metadata ResponseMetadata `xml:"ResponseMetadata"` } + +/*** Publish ***/ + +type PublishBatchFailed struct { + ErrorEntries []BatchResultErrorEntry `xml:"member"` +} + +type PublishBatchResultEntry struct { + Id string `xml:"Id"` + MessageId string `xml:"MessageId"` +} + +type PublishBatchSuccessful struct { + SuccessEntries []PublishBatchResultEntry `xml:"member"` +} + +type PublishBatchResult struct { + Failed PublishBatchFailed `xml:"Failed"` + Successful PublishBatchSuccessful `xml:"Successful"` +} + +type PublishBatchResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result PublishBatchResult `xml:"PublishBatchResult"` + Metadata ResponseMetadata `xml:"ResponseMetadata"` +} diff --git a/app/sqs_messages.go b/app/sqs_messages.go new file mode 100644 index 00000000..489fb815 --- /dev/null +++ b/app/sqs_messages.go @@ -0,0 +1,8 @@ +package app + +type BatchResultErrorEntry struct { + Code string `xml:"Code"` + Id string `xml:"Id"` + Message string `xml:"Message,omitempty"` + SenderFault bool `xml:"SenderFault"` +} From 97c1a049c44014184c706709ad719ae818b02ca3 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:05:51 +0100 Subject: [PATCH 02/41] Fixes --- app/gosns/gosns.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 9e6eb28e..4b3f80cd 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -303,9 +303,10 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { return } - var batchMessageIdToMessageBody map[string]string - var batchMessageIdToMessageStructure map[string]string - var batchMessageIdToMessageAttributes map[string]map[string]app.MessageAttributeValue + batchMessageIdToMessageBody := make(map[string]string, 10) + batchMessageIdToMessageStructure := make(map[string]string, 10) + batchMessageIdToMessageAttributes := make(map[string]map[string]app.MessageAttributeValue, 10) + batchMessageIdToSubject := make(map[string]string, 10) for memberIndex := 1; true; memberIndex++ { if memberIndex > 10 { @@ -313,11 +314,13 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { return } thisMessageFormKey := "PublishBatchRequestEntries.member." + strconv.Itoa(memberIndex) - if req.FormValue(thisMessageFormKey) == "" { - break - } batchMessageId := req.FormValue(thisMessageFormKey + ".Id") + if batchMessageId == "" { + // This is a required field, its absence likely indicates there are no further entries. + // TODO: how to tell the difference in case this is validation failure? + break + } if _, ok := batchMessageIdToMessageBody[batchMessageId]; ok { createErrorResponse(w, req, "BatchEntryIdsNotDistinct") return @@ -325,6 +328,7 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { thisMessageBody := req.FormValue(thisMessageFormKey + ".Message") thisMessageStructure := req.FormValue(thisMessageFormKey + ".MessageStructure") + thisMessageSubject := req.FormValue(thisMessageFormKey + ".Subject") thisMessageAttributes := make(map[string]app.MessageAttributeValue) @@ -344,11 +348,11 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { value := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.%s", thisMessageFormKey, i, valueKey)) if value != "" { - attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} + thisMessageAttributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} } } - if _, ok := attributes[name]; !ok { + if _, ok := thisMessageAttributes[name]; !ok { log.Warnf("StringValue or BinaryValue of %s.MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", thisMessageFormKey, name) } } @@ -356,6 +360,7 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { batchMessageIdToMessageBody[batchMessageId] = thisMessageBody batchMessageIdToMessageStructure[batchMessageId] = thisMessageStructure batchMessageIdToMessageAttributes[batchMessageId] = thisMessageAttributes + batchMessageIdToSubject[batchMessageId] = thisMessageSubject } if len(batchMessageIdToMessageBody) == 0 { @@ -364,16 +369,17 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { } successfulEntries := []app.PublishBatchResultEntry{} - failedEntries := []app.PublishBatchErrorEntry{} + failedEntries := []app.BatchResultErrorEntry{} for batchMessageId, messageBody := range batchMessageIdToMessageBody { messageStructure := batchMessageIdToMessageStructure[batchMessageId] messageAttributes := batchMessageIdToMessageAttributes[batchMessageId] + subject := batchMessageIdToSubject[batchMessageId] for _, sub := range topic.Subscriptions { - switch app.Protocol(subs.Protocol) { + switch app.Protocol(sub.Protocol) { case app.ProtocolSQS: - if err := publishSQS(subs, messageBody, messageAttributes, subject, topicArn, topicName, messageStructure); err != nil { + if err := publishSQS(sub, messageBody, messageAttributes, subject, topicArn, topicName, messageStructure); err != nil { er := app.SnsErrors[err.Error()] - failedEntries = append(failedEntries, app.PublishBatchErrorEntry{ + failedEntries = append(failedEntries, app.BatchResultErrorEntry{ Code: er.Code, Id: batchMessageId, Message: er.Message, @@ -389,7 +395,7 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { case app.ProtocolHTTP: fallthrough case app.ProtocolHTTPS: - publishHTTP(subs, messageBody, messageAttributes, subject, topicArn) + publishHTTP(sub, messageBody, messageAttributes, subject, topicArn) msgId, _ := common.NewUUID() successfulEntries = append(successfulEntries, app.PublishBatchResultEntry{ Id: batchMessageId, @@ -403,8 +409,8 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { respStruct := app.PublishBatchResponse{ "https://sns.amazonaws.com/doc/2010-03-31/", app.PublishBatchResult{ - Successful: app.PublishBatchResultEntries{Member: successfulEntries}, - Failed: app.PublishBatchErrorEntries{Member: failedEntries}, + Successful: app.PublishBatchSuccessful{SuccessEntries: successfulEntries}, + Failed: app.PublishBatchFailed{ErrorEntries: failedEntries}, }, app.ResponseMetadata{RequestId: uuid}, } From a24ff2e3081bec5bf398398d6a67da68e6f50d4b Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:05:53 +0100 Subject: [PATCH 03/41] Make some edits re feedback --- .github/README.md | 1 + app/gosns/gosns.go | 38 ++++++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/README.md b/.github/README.md index 7b45ad07..0b1f23a5 100644 --- a/.github/README.md +++ b/.github/README.md @@ -43,6 +43,7 @@ Here is a list of the APIs: - [x] Subscribe (raw) - [x] ListSubscriptions - [x] Publish + - [x] PublishBatch - [x] DeleteTopic - [x] Subscribe - [x] Unsubscribe diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 4b3f80cd..09030c17 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -308,17 +308,15 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { batchMessageIdToMessageAttributes := make(map[string]map[string]app.MessageAttributeValue, 10) batchMessageIdToSubject := make(map[string]string, 10) - for memberIndex := 1; true; memberIndex++ { - if memberIndex > 10 { - createErrorResponse(w, req, "TooManyEntriesInBatchRequest") - return - } + permissibleNumberOfEntries := 10 + for memberIndex := 1; len(batchMessageIdToMessageBody) <= permissibleNumberOfEntries; memberIndex++ { thisMessageFormKey := "PublishBatchRequestEntries.member." + strconv.Itoa(memberIndex) batchMessageId := req.FormValue(thisMessageFormKey + ".Id") if batchMessageId == "" { // This is a required field, its absence likely indicates there are no further entries. - // TODO: how to tell the difference in case this is validation failure? + // It is unclear from the AWS docs if an error is returned if there are other fields + // present for PublishBatchRequestEntries.member.N where N is some integer in range [1,10]. break } if _, ok := batchMessageIdToMessageBody[batchMessageId]; ok { @@ -330,8 +328,8 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { thisMessageStructure := req.FormValue(thisMessageFormKey + ".MessageStructure") thisMessageSubject := req.FormValue(thisMessageFormKey + ".Subject") + // Here we collate the MessageAttributes for the message at index memberIndex. thisMessageAttributes := make(map[string]app.MessageAttributeValue) - for i := 1; true; i++ { name := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Name", thisMessageFormKey, i)) if name == "" { @@ -344,15 +342,18 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { continue } - // StringListValue and BinaryListValue is currently not implemented - for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { - value := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.%s", thisMessageFormKey, i, valueKey)) - if value != "" { - thisMessageAttributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} - } + value := "" + valueKey := "" + if dataType == "Binary" { + valueKey = "BinaryValue" + value = req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.BinaryValue", thisMessageFormKey, i)) + } else { + valueKey = "StringValue" + value = req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.StringValue", thisMessageFormKey, i)) } - - if _, ok := thisMessageAttributes[name]; !ok { + if value != "" { + thisMessageAttributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} + } else { log.Warnf("StringValue or BinaryValue of %s.MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", thisMessageFormKey, name) } } @@ -363,10 +364,15 @@ func PublishBatch(w http.ResponseWriter, req *http.Request) { batchMessageIdToSubject[batchMessageId] = thisMessageSubject } - if len(batchMessageIdToMessageBody) == 0 { + numberOfEntries := len(batchMessageIdToMessageBody) + if numberOfEntries == 0 { createErrorResponse(w, req, "EmptyBatchRequest") return } + if numberOfEntries > permissibleNumberOfEntries { + createErrorResponse(w, req, "TooManyEntriesInBatchRequest") + return + } successfulEntries := []app.PublishBatchResultEntry{} failedEntries := []app.BatchResultErrorEntry{} From eb7a8af6797263ea683fdc2d88b1c29fdbd83ec0 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:05:56 +0100 Subject: [PATCH 04/41] Add PublishBatch to servertest --- app/servertest/server_test.go | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/servertest/server_test.go b/app/servertest/server_test.go index 271047c0..de5edff2 100644 --- a/app/servertest/server_test.go +++ b/app/servertest/server_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sqs" "github.com/aws/aws-sdk-go/service/sqs/sqsiface" "github.com/stretchr/testify/assert" @@ -102,6 +103,42 @@ func TestNewIntegration(t *testing.T) { } } +func TestSNSRoutes(t *testing.T) { + // Consume address + srv, err := NewSNSTest("localhost:4100", &snsTest{t: t}) + + noSetupError(t, err) + defer srv.Quit() + + creds := credentials.NewStaticCredentials("id", "secret", "token") + + awsConfig := aws.NewConfig(). + WithRegion("us-east-1"). + WithEndpoint(srv.URL()). + WithCredentials(creds) + + session1 := session.New(awsConfig) + client := sns.New(session1) + + publishBatchParams := &sns.PublishBatchInput{ + TopicArn: response.TopicArn, + PublishBatchRequestEntries: []*sns.PublishBatchRequestEntry{ + { + Id: aws.String("1"), + Message: aws.String("Cool"), + }, + { + Id: aws.String("2"), + Message: aws.String("Dog"), + }, + }, + } + publishBatchResponse, err := client.PublishBatch(publishBatchParams) + require.NoError(t, err, "SNS PublishBatch Failed") + assert.Empty(t, publishBatchResponse.Failed) + assert.Length(t, publishBatchResponse.Successful, 2) +} + func newSQS(t *testing.T, region string, endpoint string) *sqs.SQS { creds := credentials.NewStaticCredentials("id", "secret", "token") From 37d5c64c90deefc9c2c23225ecfb93a70c00818a Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:05:59 +0100 Subject: [PATCH 05/41] fix assert lenght test --- app/servertest/server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/servertest/server_test.go b/app/servertest/server_test.go index de5edff2..724bfc3f 100644 --- a/app/servertest/server_test.go +++ b/app/servertest/server_test.go @@ -136,7 +136,7 @@ func TestSNSRoutes(t *testing.T) { publishBatchResponse, err := client.PublishBatch(publishBatchParams) require.NoError(t, err, "SNS PublishBatch Failed") assert.Empty(t, publishBatchResponse.Failed) - assert.Length(t, publishBatchResponse.Successful, 2) + assert.Equal(t, 2, len(publishBatchResponse.Successful)) } func newSQS(t *testing.T, region string, endpoint string) *sqs.SQS { From 2fbb393a9157b5039e8ccac285ec0e3453ed6ab7 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:06:03 +0100 Subject: [PATCH 06/41] support aws json --- app/gosns/gosns.go | 132 ------------------ app/gosns/publish.go | 102 +++++++------- app/gosns/publish_batch.go | 111 +++++++++++++++ app/gosns/publish_batch_test.go | 232 ++++++++++++++++++++++++++++++++ app/gosns/publish_test.go | 18 ++- app/models/errors.go | 13 +- app/models/responses.go | 33 +++++ app/models/sns.go | 23 ++++ app/router/router.go | 14 +- app/router/router_test.go | 13 +- app/servertest/server_test.go | 37 ----- app/sns_messages.go | 26 ---- 12 files changed, 484 insertions(+), 270 deletions(-) create mode 100644 app/gosns/publish_batch.go create mode 100644 app/gosns/publish_batch_test.go diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 09030c17..9fca30a8 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -291,138 +291,6 @@ func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. -func PublishBatch(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - topicArn := req.FormValue("TopicArn") - - arnSegments := strings.Split(topicArn, ":") - topicName := arnSegments[len(arnSegments)-1] - topic, ok := app.SyncTopics.Topics[topicName] - if !ok { - createErrorResponse(w, req, "TopicNotFound") - return - } - - batchMessageIdToMessageBody := make(map[string]string, 10) - batchMessageIdToMessageStructure := make(map[string]string, 10) - batchMessageIdToMessageAttributes := make(map[string]map[string]app.MessageAttributeValue, 10) - batchMessageIdToSubject := make(map[string]string, 10) - - permissibleNumberOfEntries := 10 - for memberIndex := 1; len(batchMessageIdToMessageBody) <= permissibleNumberOfEntries; memberIndex++ { - thisMessageFormKey := "PublishBatchRequestEntries.member." + strconv.Itoa(memberIndex) - - batchMessageId := req.FormValue(thisMessageFormKey + ".Id") - if batchMessageId == "" { - // This is a required field, its absence likely indicates there are no further entries. - // It is unclear from the AWS docs if an error is returned if there are other fields - // present for PublishBatchRequestEntries.member.N where N is some integer in range [1,10]. - break - } - if _, ok := batchMessageIdToMessageBody[batchMessageId]; ok { - createErrorResponse(w, req, "BatchEntryIdsNotDistinct") - return - } - - thisMessageBody := req.FormValue(thisMessageFormKey + ".Message") - thisMessageStructure := req.FormValue(thisMessageFormKey + ".MessageStructure") - thisMessageSubject := req.FormValue(thisMessageFormKey + ".Subject") - - // Here we collate the MessageAttributes for the message at index memberIndex. - thisMessageAttributes := make(map[string]app.MessageAttributeValue) - for i := 1; true; i++ { - name := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Name", thisMessageFormKey, i)) - if name == "" { - break - } - - dataType := req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.DataType", thisMessageFormKey, i)) - if dataType == "" { - log.Warnf("DataType of %s.MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", thisMessageFormKey, name) - continue - } - - value := "" - valueKey := "" - if dataType == "Binary" { - valueKey = "BinaryValue" - value = req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.BinaryValue", thisMessageFormKey, i)) - } else { - valueKey = "StringValue" - value = req.FormValue(fmt.Sprintf("%s.MessageAttributes.entry.%d.Value.StringValue", thisMessageFormKey, i)) - } - if value != "" { - thisMessageAttributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} - } else { - log.Warnf("StringValue or BinaryValue of %s.MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", thisMessageFormKey, name) - } - } - - batchMessageIdToMessageBody[batchMessageId] = thisMessageBody - batchMessageIdToMessageStructure[batchMessageId] = thisMessageStructure - batchMessageIdToMessageAttributes[batchMessageId] = thisMessageAttributes - batchMessageIdToSubject[batchMessageId] = thisMessageSubject - } - - numberOfEntries := len(batchMessageIdToMessageBody) - if numberOfEntries == 0 { - createErrorResponse(w, req, "EmptyBatchRequest") - return - } - if numberOfEntries > permissibleNumberOfEntries { - createErrorResponse(w, req, "TooManyEntriesInBatchRequest") - return - } - - successfulEntries := []app.PublishBatchResultEntry{} - failedEntries := []app.BatchResultErrorEntry{} - for batchMessageId, messageBody := range batchMessageIdToMessageBody { - messageStructure := batchMessageIdToMessageStructure[batchMessageId] - messageAttributes := batchMessageIdToMessageAttributes[batchMessageId] - subject := batchMessageIdToSubject[batchMessageId] - for _, sub := range topic.Subscriptions { - switch app.Protocol(sub.Protocol) { - case app.ProtocolSQS: - if err := publishSQS(sub, messageBody, messageAttributes, subject, topicArn, topicName, messageStructure); err != nil { - er := app.SnsErrors[err.Error()] - failedEntries = append(failedEntries, app.BatchResultErrorEntry{ - Code: er.Code, - Id: batchMessageId, - Message: er.Message, - SenderFault: true, - }) - } else { - msgId, _ := common.NewUUID() - successfulEntries = append(successfulEntries, app.PublishBatchResultEntry{ - Id: batchMessageId, - MessageId: msgId, - }) - } - case app.ProtocolHTTP: - fallthrough - case app.ProtocolHTTPS: - publishHTTP(sub, messageBody, messageAttributes, subject, topicArn) - msgId, _ := common.NewUUID() - successfulEntries = append(successfulEntries, app.PublishBatchResultEntry{ - Id: batchMessageId, - MessageId: msgId, - }) - } - } - } - - uuid, _ := common.NewUUID() - respStruct := app.PublishBatchResponse{ - "https://sns.amazonaws.com/doc/2010-03-31/", - app.PublishBatchResult{ - Successful: app.PublishBatchSuccessful{SuccessEntries: successfulEntries}, - Failed: app.PublishBatchFailed{ErrorEntries: failedEntries}, - }, - app.ResponseMetadata{RequestId: uuid}, - } - SendResponseBack(w, req, respStruct, content) -} - func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { log.WithFields(log.Fields{ "sns": msg, diff --git a/app/gosns/publish.go b/app/gosns/publish.go index 34c7b0ae..28d27f08 100644 --- a/app/gosns/publish.go +++ b/app/gosns/publish.go @@ -38,27 +38,28 @@ func PublishV1(req *http.Request) (int, interfaces.AbstractResponseBody) { topicName := arnSegments[len(arnSegments)-1] _, ok = app.SyncTopics.Topics[topicName] - if ok { - log.WithFields(log.Fields{ - "topic": topicName, - "topicArn": requestBody.TopicArn, - "subject": requestBody.Subject, - }).Debug("Publish to Topic") - for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { - switch app.Protocol(subscription.Protocol) { - case app.ProtocolSQS: - err := publishSQS(subscription, topicName, requestBody) - if err != nil { - utils.CreateErrorResponseV1(err.Error(), false) - } - case app.ProtocolHTTP: - fallthrough - case app.ProtocolHTTPS: - publishHTTP(subscription, requestBody) + if !ok { + return utils.CreateErrorResponseV1("TopicNotFound", false) + } + + log.WithFields(log.Fields{ + "topic": topicName, + "topicArn": requestBody.TopicArn, + "subject": requestBody.Subject, + }).Debug("Publish to Topic") + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(requestBody.MessageAttributes) + for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { + switch app.Protocol(subscription.Protocol) { + case app.ProtocolSQS: + err := publishSQS(subscription, requestBody.Message, messageAttributes, requestBody.Subject, topicName, requestBody.MessageStructure) + if err != nil { + utils.CreateErrorResponseV1(err.Error(), false) } + case app.ProtocolHTTP: + fallthrough + case app.ProtocolHTTPS: + publishHTTP(subscription, requestBody.Message, messageAttributes, requestBody.Subject, requestBody.TopicArn) } - } else { - return utils.CreateErrorResponseV1("TopicNotFound", false) } //Create the response @@ -74,8 +75,7 @@ func PublishV1(req *http.Request) (int, interfaces.AbstractResponseBody) { return http.StatusOK, respStruct } -func publishSQS(subscription *app.Subscription, topicName string, requestBody *models.PublishRequest) error { - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(requestBody.MessageAttributes) +func publishSQS(subscription *app.Subscription, message string, messageAttributes map[string]app.MessageAttributeValue, subject string, topicName string, messageStructure string) error { if subscription.FilterPolicy != nil && !subscription.FilterPolicy.IsSatisfiedBy(messageAttributes) { return nil } @@ -86,51 +86,51 @@ func publishSQS(subscription *app.Subscription, topicName string, requestBody *m arnSegments := strings.Split(queueName, ":") queueName = arnSegments[len(arnSegments)-1] - if _, ok := app.SyncQueues.Queues[queueName]; ok { - msg := app.Message{} + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + log.Infof("%s: Queue %s does not exist, message discarded\n", time.Now().Format("2006-01-02 15:04:05"), queueName) + return nil + } + + msg := app.Message{} - if subscription.Raw == false { - m, err := createMessageBody(subscription, requestBody.Message, requestBody.Subject, requestBody.MessageStructure, messageAttributes) - if err != nil { - return err - } + if subscription.Raw == false { + m, err := createMessageBody(subscription, message, subject, messageStructure, messageAttributes) + if err != nil { + return err + } - msg.MessageBody = m + msg.MessageBody = m + } else { + msg.MessageAttributes = messageAttributes + msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) + m, err := extractMessageFromJSON(message, subscription.Protocol) + if err == nil { + msg.MessageBody = []byte(m) } else { - msg.MessageAttributes = messageAttributes - msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) - m, err := extractMessageFromJSON(requestBody.Message, subscription.Protocol) - if err == nil { - msg.MessageBody = []byte(m) - } else { - msg.MessageBody = []byte(requestBody.Message) - } + msg.MessageBody = []byte(message) } + } - msg.MD5OfMessageBody = common.GetMD5Hash(requestBody.Message) - msg.Uuid, _ = common.NewUUID() - app.SyncQueues.Lock() - app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) - app.SyncQueues.Unlock() + msg.MD5OfMessageBody = common.GetMD5Hash(message) + msg.Uuid, _ = common.NewUUID() + app.SyncQueues.Lock() + app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) + app.SyncQueues.Unlock() - log.Infof("%s: Topic: %s(%s), Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), topicName, queueName, msg.MessageBody) - } else { - log.Infof("%s: Queue %s does not exist, message discarded\n", time.Now().Format("2006-01-02 15:04:05"), queueName) - } + log.Infof("%s: Topic: %s(%s), Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), topicName, queueName, msg.MessageBody) return nil } -func publishHTTP(subs *app.Subscription, requestBody *models.PublishRequest) { - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(requestBody.MessageAttributes) +func publishHTTP(subs *app.Subscription, message string, messageAttributes map[string]app.MessageAttributeValue, subject string, topicArn string) { id := uuid.NewString() msg := app.SNSMessage{ Type: "Notification", MessageId: id, - TopicArn: requestBody.TopicArn, - Subject: requestBody.Subject, - Message: requestBody.Message, Timestamp: time.Now().UTC().Format(time.RFC3339), SignatureVersion: "1", + Message: message, + TopicArn: topicArn, + Subject: subject, SigningCertURL: fmt.Sprintf("http://%s:%s/SimpleNotificationService/%s.pem", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, id), UnsubscribeURL: fmt.Sprintf("http://%s:%s/?Action=Unsubscribe&SubscriptionArn=%s", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, subs.SubscriptionArn), MessageAttributes: formatAttributes(messageAttributes), diff --git a/app/gosns/publish_batch.go b/app/gosns/publish_batch.go new file mode 100644 index 00000000..6f747b41 --- /dev/null +++ b/app/gosns/publish_batch.go @@ -0,0 +1,111 @@ +package gosns + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewPublishBatchRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - PublishBatchV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + if requestBody.TopicArn == "" { + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + topicArn := requestBody.TopicArn + arnSegments := strings.Split(topicArn, ":") + topicName := arnSegments[len(arnSegments)-1] + topic, ok := app.SyncTopics.Topics[topicName] + if !ok { + return utils.CreateErrorResponseV1("TopicNotFound", false) + } + + seen := make(map[string]bool) + + for _, entry := range requestBody.PublishBatchRequestEntries { + if entry.ID == "" { + // This is a required field, its absence likely indicates there are no further entries. + // It is unclear from the AWS docs if an error is returned if there are other fields + // present for PublishBatchRequestEntries.member.N where N is some integer in range [1,10]. + break + } + if seen[entry.ID] { + return utils.CreateErrorResponseV1("BatchEntryIdsNotDistinct", false) + } + seen[entry.ID] = true + } + + if len(requestBody.PublishBatchRequestEntries) == 0 { + return utils.CreateErrorResponseV1("EmptyBatchRequest", false) + } + if len(requestBody.PublishBatchRequestEntries) > 10 { + return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", false) + } + + successfulEntries := []models.PublishBatchResultEntry{} + failedEntries := []models.BatchResultErrorEntry{} + for _, entry := range requestBody.PublishBatchRequestEntries { + // we now know all the entry.IDs are unique + if entry.ID == "" { + // This is a required field, its absence likely indicates there are no further entries. + // It is unclear from the AWS docs if an error is returned if there are other fields + // present for PublishBatchRequestEntries.member.N where N is some integer in range [1,10]. + break + } + for _, sub := range topic.Subscriptions { + switch app.Protocol(sub.Protocol) { + case app.ProtocolSQS: + oldMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(entry.MessageAttributes) + if err := publishSQS(sub, entry.Message, oldMessageAttributes, entry.Subject, topicName, entry.MessageStructure); err != nil { + er := models.SnsErrors[err.Error()] + failedEntries = append(failedEntries, models.BatchResultErrorEntry{ + Code: er.Code, + Id: entry.ID, + Message: er.Message, + SenderFault: true, + }) + } else { + msgId, _ := common.NewUUID() + successfulEntries = append(successfulEntries, models.PublishBatchResultEntry{ + Id: entry.ID, + MessageId: msgId, + }) + } + case app.ProtocolHTTP: + fallthrough + case app.ProtocolHTTPS: + oldMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(entry.MessageAttributes) + publishHTTP(sub, entry.Message, oldMessageAttributes, entry.Subject, topicArn) + msgId, _ := common.NewUUID() + successfulEntries = append(successfulEntries, models.PublishBatchResultEntry{ + Id: entry.ID, + MessageId: msgId, + }) + } + } + } + + respStruct := models.PublishBatchResponse{ + // "https://sns.amazonaws.com/doc/2010-03-31/", + Xmlns: models.BASE_XMLNS, + Result: models.PublishBatchResult{ + Successful: models.PublishBatchSuccessful{SuccessEntries: successfulEntries}, + Failed: models.PublishBatchFailed{ErrorEntries: failedEntries}, + }, + Metadata: app.ResponseMetadata{RequestId: uuid.NewString()}, + } + return http.StatusOK, respStruct +} diff --git a/app/gosns/publish_batch_test.go b/app/gosns/publish_batch_test.go new file mode 100644 index 00000000..178a080d --- /dev/null +++ b/app/gosns/publish_batch_test.go @@ -0,0 +1,232 @@ +package gosns + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "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" + "github.com/stretchr/testify/require" +) + +func TestPublishBatchV1_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, _ := PublishBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestBatchPublishV1_empty_topicArn_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) { + v := resultingStruct.(*models.PublishBatchRequest) + message := "{\"IAm\": \"aMessage\"}" + e := &models.PublishBatchRequestEntry{ID: "1", Message: message} + *v = models.PublishBatchRequest{ + TopicArn: "", + PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e}, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PublishBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestBatchPublishV1_nonexistent_topicArn_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) { + v := resultingStruct.(*models.PublishBatchRequest) + message := "{\"IAm\": \"aMessage\"}" + e := &models.PublishBatchRequestEntry{ID: "1", Message: message} + *v = models.PublishBatchRequest{ + TopicArn: "non-existing", + PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e}, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PublishBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestBatchPublishV1_zero_entries_error(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishBatchRequest) + *v = models.PublishBatchRequest{ + TopicArn: topicArn, + PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{}, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PublishBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestBatchPublishV1_too_many_entries_error(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishBatchRequest) + entries := make([]*models.PublishBatchRequestEntry, 0, 11) + for i := 0; i < 11; i++ { + message := fmt.Sprintf("{\"IAm\": \"aMessage-%d\"}", i) + e := &models.PublishBatchRequestEntry{ID: fmt.Sprintf("%d", i), Message: message} + entries = append(entries, e) + } + *v = models.PublishBatchRequest{ + TopicArn: topicArn, + PublishBatchRequestEntries: entries, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PublishBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestBatchPublishV1_success_sqs(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishBatchRequest) + e := &models.PublishBatchRequestEntry{ID: "1", Message: message} + *v = models.PublishBatchRequest{ + TopicArn: topicArn, + PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e}, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := PublishBatchV1(r) + + require.Equal(t, http.StatusOK, code) + _, ok := response.(models.PublishBatchResponse) + assert.True(t, ok) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + require.Len(t, messages, 1) + assert.Equal(t, message, string(messages[0].MessageBody)) +} + +func TestBatchPublishV1_duplicate_id_errors(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishBatchRequest) + e := &models.PublishBatchRequestEntry{ID: "1", Message: message} + *v = models.PublishBatchRequest{ + TopicArn: topicArn, + PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e, e}, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PublishBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestBatchPublishV1_multiple_success_sqs(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + entries := make([]*models.PublishBatchRequestEntry, 0, 10) + expectedMessages := make([]string, 0, 10) + for i := 0; i < 10; i++ { + message := fmt.Sprintf("{\"IAm\": \"aMessage-%d\"}", i) + e := &models.PublishBatchRequestEntry{ID: fmt.Sprintf("%d", i), Message: message} + entries = append(entries, e) + expectedMessages = append(expectedMessages, message) + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishBatchRequest) + *v = models.PublishBatchRequest{ + TopicArn: topicArn, + PublishBatchRequestEntries: entries, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := PublishBatchV1(r) + + require.Equal(t, http.StatusOK, code) + _, ok := response.(models.PublishBatchResponse) + assert.True(t, ok) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + gotMessages := []string{} + for _, m := range messages { + gotMessages = append(gotMessages, string(m.MessageBody)) + } + assert.ElementsMatch(t, expectedMessages, gotMessages) +} diff --git a/app/gosns/publish_test.go b/app/gosns/publish_test.go index bf93bb86..fbe17873 100644 --- a/app/gosns/publish_test.go +++ b/app/gosns/publish_test.go @@ -249,7 +249,8 @@ func Test_publishSQS_success_raw(t *testing.T) { TopicArn: topicArn, Message: message, } - err := publishSQS(sub, "unit-topic1", &request) + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) + err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) assert.Nil(t, err) @@ -277,7 +278,8 @@ func Test_publishSQS_success_json(t *testing.T) { TopicArn: topicArn, Message: message, } - err := publishSQS(sub, "unit-topic1", &request) + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) + err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) assert.Nil(t, err) @@ -320,7 +322,8 @@ func Test_publishSQS_filter_policy_not_satisfied_by_attributes(t *testing.T) { }, }, } - err := publishSQS(sub, "unit-topic1", &request) + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) + err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) assert.Nil(t, err) } @@ -344,7 +347,8 @@ func Test_publishSQS_missing_queue_returns_nil(t *testing.T) { TopicArn: topicArn, Message: message, } - err := publishSQS(sub, "unit-topic1", &request) + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) + err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) assert.Nil(t, err) } @@ -376,7 +380,8 @@ func Test_publishHTTP_success(t *testing.T) { Message: message, } - publishHTTP(sub, &request) + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) + publishHTTP(sub, request.Message, messageAttributes, request.Subject, request.TopicArn) assert.True(t, called) } @@ -399,7 +404,8 @@ func Test_publishHTTP_callEndpoint_failure(t *testing.T) { Message: message, } - publishHTTP(sub, &request) + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) + publishHTTP(sub, request.Message, messageAttributes, request.Subject, request.TopicArn) // swallows all errors } diff --git a/app/models/errors.go b/app/models/errors.go index ab421a0e..b1643498 100644 --- a/app/models/errors.go +++ b/app/models/errors.go @@ -18,11 +18,14 @@ func init() { "InvalidAttributeValue": {HttpError: http.StatusBadRequest, Type: "InvalidAttributeValue", Code: "AWS.SimpleQueueService.InvalidAttributeValue", Message: "Invalid Value for the parameter RedrivePolicy."}, } SnsErrors = map[string]SnsErrorType{ - "InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleNotificationService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."}, - "TopicNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentTopic", Message: "The specified topic does not exist for this wsdl version."}, - "SubscriptionNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."}, - "TopicExists": {HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleNotificationService.TopicAlreadyExists", Message: "The specified topic already exists."}, - "ValidationError": {HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."}, + "InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleNotificationService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."}, + "TopicNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentTopic", Message: "The specified topic does not exist for this wsdl version."}, + "SubscriptionNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."}, + "TopicExists": {HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleNotificationService.TopicAlreadyExists", Message: "The specified topic already exists."}, + "ValidationError": {HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."}, + "BatchEntryIdsNotDistinct": {HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.BatchEntryIdsNotDistinct", Message: "Two or more batch entries in the request have the same Id."}, + "TooManyEntriesInBatchRequest": {HttpError: http.StatusBadRequest, Type: "TooManyEntriesInBatchRequest", Code: "AWS.SimpleNotificationService.TooManyEntriesInBatchRequest", Message: "Maximum number of entries per request are 10."}, + "EmptyBatchRequest": {HttpError: http.StatusBadRequest, Type: "EmptyBatchRequest", Code: "AWS.SimpleNotificationService.EmptyBatchRequest", Message: "The batch request doesn't contain any entries."}, } } diff --git a/app/models/responses.go b/app/models/responses.go index 5a01c632..be2922fa 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -421,6 +421,39 @@ func (r PublishResponse) GetRequestId() string { return r.Metadata.RequestId } +/*** Publish Batch ***/ +type PublishBatchFailed struct { + ErrorEntries []BatchResultErrorEntry `xml:"member"` +} + +type PublishBatchResultEntry struct { + Id string `xml:"Id"` + MessageId string `xml:"MessageId"` +} + +type PublishBatchSuccessful struct { + SuccessEntries []PublishBatchResultEntry `xml:"member"` +} + +type PublishBatchResult struct { + Failed PublishBatchFailed `xml:"Failed"` + Successful PublishBatchSuccessful `xml:"Successful"` +} + +type PublishBatchResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result PublishBatchResult `xml:"PublishBatchResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r PublishBatchResponse) GetResult() interface{} { + return r.Result +} + +func (r PublishBatchResponse) GetRequestId() string { + return r.Metadata.RequestId +} + /*** List Topics ***/ type TopicArnResult struct { TopicArn string `xml:"TopicArn"` diff --git a/app/models/sns.go b/app/models/sns.go index 5cbdf105..ec4686cc 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -248,3 +248,26 @@ type DeleteTopicRequest struct { } func (r *DeleteTopicRequest) SetAttributesFromForm(values url.Values) {} + +// PublishBatchV1 + +func NewPublishBatchRequest() *PublishBatchRequest { + return &PublishBatchRequest{} +} + +type PublishBatchRequest struct { + PublishBatchRequestEntries []*PublishBatchRequestEntry `json:"PublishBatchRequestEntries" schema:"PublishBatchRequestEntries"` + TopicArn string `json:"TopicArn" schema:"TopicArn"` +} + +type PublishBatchRequestEntry struct { + ID string `json:"Id" schema:"Id"` + Message string `json:"Message" schema:"Message"` + MessageAttributes map[string]MessageAttributeValue `json:"MessageAttributes" schema:"MessageAttributes"` + MessageDeduplicationId string `json:"MessageDeduplicationId" schema:"MessageDeduplicationId"` // Not implemented + MessageGroupId string `json:"MessageGroupId" schema:"MessageGroupId"` // Not implemented + MessageStructure string `json:"MessageStructure" schema:"MessageStructure"` + Subject string `json:"Subject" schema:"Subject"` +} + +func (r *PublishBatchRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index b624f208..9e298020 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, + "PublishBatch": sns.PublishBatchV1, } var routingTable = map[string]http.HandlerFunc{ @@ -93,7 +94,6 @@ var routingTable = map[string]http.HandlerFunc{ "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, "ListSubscriptions": sns.ListSubscriptions, - "PublishBatch": sns.PublishBatch, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/router/router_test.go b/app/router/router_test.go index 10d66b72..ccb3b712 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, + "PublishBatch": sns.PublishBatchV1, } routingTable = map[string]http.HandlerFunc{ diff --git a/app/servertest/server_test.go b/app/servertest/server_test.go index 724bfc3f..271047c0 100644 --- a/app/servertest/server_test.go +++ b/app/servertest/server_test.go @@ -7,7 +7,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sqs" "github.com/aws/aws-sdk-go/service/sqs/sqsiface" "github.com/stretchr/testify/assert" @@ -103,42 +102,6 @@ func TestNewIntegration(t *testing.T) { } } -func TestSNSRoutes(t *testing.T) { - // Consume address - srv, err := NewSNSTest("localhost:4100", &snsTest{t: t}) - - noSetupError(t, err) - defer srv.Quit() - - creds := credentials.NewStaticCredentials("id", "secret", "token") - - awsConfig := aws.NewConfig(). - WithRegion("us-east-1"). - WithEndpoint(srv.URL()). - WithCredentials(creds) - - session1 := session.New(awsConfig) - client := sns.New(session1) - - publishBatchParams := &sns.PublishBatchInput{ - TopicArn: response.TopicArn, - PublishBatchRequestEntries: []*sns.PublishBatchRequestEntry{ - { - Id: aws.String("1"), - Message: aws.String("Cool"), - }, - { - Id: aws.String("2"), - Message: aws.String("Dog"), - }, - }, - } - publishBatchResponse, err := client.PublishBatch(publishBatchParams) - require.NoError(t, err, "SNS PublishBatch Failed") - assert.Empty(t, publishBatchResponse.Failed) - assert.Equal(t, 2, len(publishBatchResponse.Successful)) -} - func newSQS(t *testing.T, region string, endpoint string) *sqs.SQS { creds := credentials.NewStaticCredentials("id", "secret", "token") diff --git a/app/sns_messages.go b/app/sns_messages.go index 9395a7a5..06465ef1 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -61,29 +61,3 @@ type ListSubscriptionsByTopicResponse struct { Result ListSubscriptionsByTopicResult `xml:"ListSubscriptionsByTopicResult"` Metadata ResponseMetadata `xml:"ResponseMetadata"` } - -/*** Publish ***/ - -type PublishBatchFailed struct { - ErrorEntries []BatchResultErrorEntry `xml:"member"` -} - -type PublishBatchResultEntry struct { - Id string `xml:"Id"` - MessageId string `xml:"MessageId"` -} - -type PublishBatchSuccessful struct { - SuccessEntries []PublishBatchResultEntry `xml:"member"` -} - -type PublishBatchResult struct { - Failed PublishBatchFailed `xml:"Failed"` - Successful PublishBatchSuccessful `xml:"Successful"` -} - -type PublishBatchResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result PublishBatchResult `xml:"PublishBatchResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} From 1759c4dbb0f996cb61bc660d17f6a80bdfb42779 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:45:15 +0100 Subject: [PATCH 07/41] review comments --- app/gosns/publish.go | 4 ++-- app/gosns/publish_batch.go | 35 +++++++++++++---------------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/app/gosns/publish.go b/app/gosns/publish.go index 28d27f08..17e0d7ff 100644 --- a/app/gosns/publish.go +++ b/app/gosns/publish.go @@ -87,7 +87,7 @@ func publishSQS(subscription *app.Subscription, message string, messageAttribute queueName = arnSegments[len(arnSegments)-1] if _, ok := app.SyncQueues.Queues[queueName]; !ok { - log.Infof("%s: Queue %s does not exist, message discarded\n", time.Now().Format("2006-01-02 15:04:05"), queueName) + log.Infof("Queue %s does not exist, message discarded\n", queueName) return nil } @@ -112,7 +112,7 @@ func publishSQS(subscription *app.Subscription, message string, messageAttribute } msg.MD5OfMessageBody = common.GetMD5Hash(message) - msg.Uuid, _ = common.NewUUID() + msg.Uuid = uuid.NewString() app.SyncQueues.Lock() app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) app.SyncQueues.Unlock() diff --git a/app/gosns/publish_batch.go b/app/gosns/publish_batch.go index 6f747b41..1ccff04f 100644 --- a/app/gosns/publish_batch.go +++ b/app/gosns/publish_batch.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/common" "github.com/Admiral-Piett/goaws/app/interfaces" "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/utils" @@ -25,6 +24,13 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { return utils.CreateErrorResponseV1("InvalidParameterValue", false) } + if len(requestBody.PublishBatchRequestEntries) == 0 { + return utils.CreateErrorResponseV1("EmptyBatchRequest", false) + } + if len(requestBody.PublishBatchRequestEntries) > 10 { + return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", false) + } + topicArn := requestBody.TopicArn arnSegments := strings.Split(topicArn, ":") topicName := arnSegments[len(arnSegments)-1] @@ -37,10 +43,9 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { for _, entry := range requestBody.PublishBatchRequestEntries { if entry.ID == "" { - // This is a required field, its absence likely indicates there are no further entries. - // It is unclear from the AWS docs if an error is returned if there are other fields - // present for PublishBatchRequestEntries.member.N where N is some integer in range [1,10]. - break + // This is a required field for the PublishBatchRequestEntry entity but doesn't seem required in the request. + // If it's not present in the request then assume we should generate one. + entry.ID = uuid.NewString() } if seen[entry.ID] { return utils.CreateErrorResponseV1("BatchEntryIdsNotDistinct", false) @@ -48,23 +53,10 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { seen[entry.ID] = true } - if len(requestBody.PublishBatchRequestEntries) == 0 { - return utils.CreateErrorResponseV1("EmptyBatchRequest", false) - } - if len(requestBody.PublishBatchRequestEntries) > 10 { - return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", false) - } - successfulEntries := []models.PublishBatchResultEntry{} failedEntries := []models.BatchResultErrorEntry{} for _, entry := range requestBody.PublishBatchRequestEntries { - // we now know all the entry.IDs are unique - if entry.ID == "" { - // This is a required field, its absence likely indicates there are no further entries. - // It is unclear from the AWS docs if an error is returned if there are other fields - // present for PublishBatchRequestEntries.member.N where N is some integer in range [1,10]. - break - } + // we now know all the entry.IDs are unique and non-blank for _, sub := range topic.Subscriptions { switch app.Protocol(sub.Protocol) { case app.ProtocolSQS: @@ -78,7 +70,7 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { SenderFault: true, }) } else { - msgId, _ := common.NewUUID() + msgId := uuid.NewString() successfulEntries = append(successfulEntries, models.PublishBatchResultEntry{ Id: entry.ID, MessageId: msgId, @@ -89,7 +81,7 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { case app.ProtocolHTTPS: oldMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(entry.MessageAttributes) publishHTTP(sub, entry.Message, oldMessageAttributes, entry.Subject, topicArn) - msgId, _ := common.NewUUID() + msgId := uuid.NewString() successfulEntries = append(successfulEntries, models.PublishBatchResultEntry{ Id: entry.ID, MessageId: msgId, @@ -99,7 +91,6 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { } respStruct := models.PublishBatchResponse{ - // "https://sns.amazonaws.com/doc/2010-03-31/", Xmlns: models.BASE_XMLNS, Result: models.PublishBatchResult{ Successful: models.PublishBatchSuccessful{SuccessEntries: successfulEntries}, From 4b9e312871966fd8b3bf9f015255ae715fd582dc Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:48:16 +0100 Subject: [PATCH 08/41] use message creator interface --- app/gosns/publish.go | 44 ++++++++++++++------------ app/gosns/publish_batch.go | 6 ++-- app/gosns/publish_test.go | 18 ++++------- app/models/sns.go | 64 ++++++++++++++++++++++++++++++++++++++ app/utils/utils.go | 31 ------------------ 5 files changed, 96 insertions(+), 67 deletions(-) diff --git a/app/gosns/publish.go b/app/gosns/publish.go index 17e0d7ff..64aa3c03 100644 --- a/app/gosns/publish.go +++ b/app/gosns/publish.go @@ -47,18 +47,17 @@ func PublishV1(req *http.Request) (int, interfaces.AbstractResponseBody) { "topicArn": requestBody.TopicArn, "subject": requestBody.Subject, }).Debug("Publish to Topic") - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(requestBody.MessageAttributes) for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { switch app.Protocol(subscription.Protocol) { case app.ProtocolSQS: - err := publishSQS(subscription, requestBody.Message, messageAttributes, requestBody.Subject, topicName, requestBody.MessageStructure) + err := publishSQS(subscription, topicName, requestBody) if err != nil { utils.CreateErrorResponseV1(err.Error(), false) } case app.ProtocolHTTP: fallthrough case app.ProtocolHTTPS: - publishHTTP(subscription, requestBody.Message, messageAttributes, requestBody.Subject, requestBody.TopicArn) + publishHTTP(subscription, requestBody.TopicArn, requestBody) } } @@ -75,8 +74,15 @@ func PublishV1(req *http.Request) (int, interfaces.AbstractResponseBody) { return http.StatusOK, respStruct } -func publishSQS(subscription *app.Subscription, message string, messageAttributes map[string]app.MessageAttributeValue, subject string, topicName string, messageStructure string) error { - if subscription.FilterPolicy != nil && !subscription.FilterPolicy.IsSatisfiedBy(messageAttributes) { +type MessageCreator interface { + GetMessageAttributes() map[string]app.MessageAttributeValue + GetMessage() string + GetSubject() string + GetMessageStructure() string +} + +func publishSQS(subscription *app.Subscription, topicName string, messager MessageCreator) error { + if subscription.FilterPolicy != nil && !subscription.FilterPolicy.IsSatisfiedBy(messager.GetMessageAttributes()) { return nil } @@ -90,47 +96,45 @@ func publishSQS(subscription *app.Subscription, message string, messageAttribute log.Infof("Queue %s does not exist, message discarded\n", queueName) return nil } - msg := app.Message{} - if subscription.Raw == false { - m, err := createMessageBody(subscription, message, subject, messageStructure, messageAttributes) + m, err := createMessageBody(subscription, messager.GetMessage(), messager.GetSubject(), messager.GetMessageStructure(), messager.GetMessageAttributes()) if err != nil { return err } - msg.MessageBody = m } else { - msg.MessageAttributes = messageAttributes - msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) - m, err := extractMessageFromJSON(message, subscription.Protocol) + msg.MessageAttributes = messager.GetMessageAttributes() + msg.MD5OfMessageAttributes = common.HashAttributes(msg.MessageAttributes) + m, err := extractMessageFromJSON(messager.GetMessage(), subscription.Protocol) if err == nil { msg.MessageBody = []byte(m) } else { - msg.MessageBody = []byte(message) + msg.MessageBody = []byte(messager.GetMessage()) } } - - msg.MD5OfMessageBody = common.GetMD5Hash(message) + msg.MD5OfMessageBody = common.GetMD5Hash(messager.GetMessage()) msg.Uuid = uuid.NewString() + app.SyncQueues.Lock() app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) app.SyncQueues.Unlock() - log.Infof("%s: Topic: %s(%s), Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), topicName, queueName, msg.MessageBody) + log.Infof("Topic: %s(%s), Message: %s\n", topicName, queueName, msg.MessageBody) return nil } -func publishHTTP(subs *app.Subscription, message string, messageAttributes map[string]app.MessageAttributeValue, subject string, topicArn string) { +func publishHTTP(subs *app.Subscription, topicArn string, messager MessageCreator) { + messageAttributes := messager.GetMessageAttributes() id := uuid.NewString() msg := app.SNSMessage{ Type: "Notification", MessageId: id, + Message: messager.GetMessage(), + TopicArn: topicArn, + Subject: messager.GetSubject(), Timestamp: time.Now().UTC().Format(time.RFC3339), SignatureVersion: "1", - Message: message, - TopicArn: topicArn, - Subject: subject, SigningCertURL: fmt.Sprintf("http://%s:%s/SimpleNotificationService/%s.pem", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, id), UnsubscribeURL: fmt.Sprintf("http://%s:%s/?Action=Unsubscribe&SubscriptionArn=%s", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, subs.SubscriptionArn), MessageAttributes: formatAttributes(messageAttributes), diff --git a/app/gosns/publish_batch.go b/app/gosns/publish_batch.go index 1ccff04f..775262e2 100644 --- a/app/gosns/publish_batch.go +++ b/app/gosns/publish_batch.go @@ -60,8 +60,7 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { for _, sub := range topic.Subscriptions { switch app.Protocol(sub.Protocol) { case app.ProtocolSQS: - oldMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(entry.MessageAttributes) - if err := publishSQS(sub, entry.Message, oldMessageAttributes, entry.Subject, topicName, entry.MessageStructure); err != nil { + if err := publishSQS(sub, topicName, entry); err != nil { er := models.SnsErrors[err.Error()] failedEntries = append(failedEntries, models.BatchResultErrorEntry{ Code: er.Code, @@ -79,8 +78,7 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { case app.ProtocolHTTP: fallthrough case app.ProtocolHTTPS: - oldMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(entry.MessageAttributes) - publishHTTP(sub, entry.Message, oldMessageAttributes, entry.Subject, topicArn) + publishHTTP(sub, topicArn, entry) msgId := uuid.NewString() successfulEntries = append(successfulEntries, models.PublishBatchResultEntry{ Id: entry.ID, diff --git a/app/gosns/publish_test.go b/app/gosns/publish_test.go index fbe17873..0c6e09c2 100644 --- a/app/gosns/publish_test.go +++ b/app/gosns/publish_test.go @@ -249,8 +249,7 @@ func Test_publishSQS_success_raw(t *testing.T) { TopicArn: topicArn, Message: message, } - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) - err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) + err := publishSQS(sub, "unit-topic1", &request) assert.Nil(t, err) @@ -278,8 +277,7 @@ func Test_publishSQS_success_json(t *testing.T) { TopicArn: topicArn, Message: message, } - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) - err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) + err := publishSQS(sub, "unit-topic1", &request) assert.Nil(t, err) @@ -322,8 +320,7 @@ func Test_publishSQS_filter_policy_not_satisfied_by_attributes(t *testing.T) { }, }, } - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) - err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) + err := publishSQS(sub, "unit-topic1", &request) assert.Nil(t, err) } @@ -347,8 +344,7 @@ func Test_publishSQS_missing_queue_returns_nil(t *testing.T) { TopicArn: topicArn, Message: message, } - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) - err := publishSQS(sub, request.Message, messageAttributes, request.Subject, "unit-topic1", request.MessageStructure) + err := publishSQS(sub, "unit-topic1", &request) assert.Nil(t, err) } @@ -380,8 +376,7 @@ func Test_publishHTTP_success(t *testing.T) { Message: message, } - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) - publishHTTP(sub, request.Message, messageAttributes, request.Subject, request.TopicArn) + publishHTTP(sub, request.TopicArn, &request) assert.True(t, called) } @@ -404,8 +399,7 @@ func Test_publishHTTP_callEndpoint_failure(t *testing.T) { Message: message, } - messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(request.MessageAttributes) - publishHTTP(sub, request.Message, messageAttributes, request.Subject, request.TopicArn) + publishHTTP(sub, request.TopicArn, &request) // swallows all errors } diff --git a/app/models/sns.go b/app/models/sns.go index ec4686cc..1718d993 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -199,6 +199,54 @@ type PublishRequest struct { TopicArn string `json:"TopicArn" schema:"TopicArn"` } +// TODO: +// Refactor internal model for MessageAttribute between SendMessage and ReceiveMessage +// from app.MessageAttributeValue(old) to models.MessageAttributeValue(new) and remove this temporary function. +func convertToOldMessageAttributeValueStructure(newValues map[string]MessageAttributeValue) map[string]app.MessageAttributeValue { + attributes := make(map[string]app.MessageAttributeValue) + + for name, entry := range newValues { + // StringListValue and BinaryListValue is currently not implemented + // Please refer app/gosqs/message_attributes.go + value := "" + valueKey := "" + if entry.StringValue != "" { + value = entry.StringValue + valueKey = "StringValue" + } else if entry.BinaryValue != "" { + value = entry.BinaryValue + valueKey = "BinaryValue" + } + attributes[name] = app.MessageAttributeValue{ + Name: name, + DataType: entry.DataType, + Value: value, + ValueKey: valueKey, + } + } + + return attributes +} +func (r *PublishRequest) GetMessageAttributes() map[string]app.MessageAttributeValue { + return convertToOldMessageAttributeValueStructure(r.MessageAttributes) +} + +func (r *PublishRequest) GetMessage() string { + return r.Message +} + +func (r *PublishRequest) GetSubject() string { + return r.Subject +} + +func (r *PublishRequest) GetMessageStructure() string { + return r.MessageStructure +} + +func (r *PublishRequest) GetTopicArn() string { + return r.TopicArn +} + func (r *PublishRequest) SetAttributesFromForm(values url.Values) { for i := 1; true; i++ { nameKey := fmt.Sprintf("MessageAttributes.entry.%d.Name", i) @@ -271,3 +319,19 @@ type PublishBatchRequestEntry struct { } func (r *PublishBatchRequest) SetAttributesFromForm(values url.Values) {} + +func (e *PublishBatchRequestEntry) GetMessage() string { + return e.Message +} + +func (e *PublishBatchRequestEntry) GetSubject() string { + return e.Subject +} + +func (e *PublishBatchRequestEntry) GetMessageStructure() string { + return e.MessageStructure +} + +func (e *PublishBatchRequestEntry) GetMessageAttributes() map[string]app.MessageAttributeValue { + return convertToOldMessageAttributeValueStructure(e.MessageAttributes) +} diff --git a/app/utils/utils.go b/app/utils/utils.go index e5b7305d..0ed52dda 100644 --- a/app/utils/utils.go +++ b/app/utils/utils.go @@ -7,8 +7,6 @@ import ( "net/http" "net/url" - "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -91,32 +89,3 @@ func CreateErrorResponseV1(errKey string, isSqs bool) (int, interfaces.AbstractR } return err.StatusCode(), respStruct } - -// TODO: -// Refactor internal model for MessageAttribute between SendMessage and ReceiveMessage -// from app.MessageAttributeValue(old) to models.MessageAttributeValue(new) and remove this temporary function. -func ConvertToOldMessageAttributeValueStructure(newValues map[string]models.MessageAttributeValue) map[string]app.MessageAttributeValue { - attributes := make(map[string]app.MessageAttributeValue) - - for name, entry := range newValues { - // StringListValue and BinaryListValue is currently not implemented - // Please refer app/gosqs/message_attributes.go - value := "" - valueKey := "" - if entry.StringValue != "" { - value = entry.StringValue - valueKey = "StringValue" - } else if entry.BinaryValue != "" { - value = entry.BinaryValue - valueKey = "BinaryValue" - } - attributes[name] = app.MessageAttributeValue{ - Name: name, - DataType: entry.DataType, - Value: value, - ValueKey: valueKey, - } - } - - return attributes -} From e4c58c909cbf8c251daa522a6bec26e9b5602c7b Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:56:35 +0100 Subject: [PATCH 09/41] add publish batch smoke test --- smoke_tests/sns_publish_batch_test.go | 616 ++++++++++++++++++++++++++ 1 file changed, 616 insertions(+) create mode 100644 smoke_tests/sns_publish_batch_test.go diff --git a/smoke_tests/sns_publish_batch_test.go b/smoke_tests/sns_publish_batch_test.go new file mode 100644 index 00000000..d3522f38 --- /dev/null +++ b/smoke_tests/sns_publish_batch_test.go @@ -0,0 +1,616 @@ +package smoke_tests + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/conf" + "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/aws/aws-sdk-go-v2/service/sns/types" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Publish_batch_sqs_json_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + response, err := snsClient.PublishBatch(context.TODO(), &sns.PublishBatchInput{ + TopicArn: &topicArn, + PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ + { + Message: &message, + Subject: &subject, + }, + { + Message: &message, + Subject: &subject, + }, + }, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + require.Len(t, messages, 2) + assert.Equal(t, message, string(messages[0].MessageBody)) + assert.Equal(t, message, string(messages[1].MessageBody)) +} + +func Test_Publish_batch_sqs_json_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + topicArn := app.SyncTopics.Topics["unit-topic3"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + response, err := snsClient.PublishBatch(context.TODO(), &sns.PublishBatchInput{ + TopicArn: &topicArn, + PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ + { + Message: &message, + Subject: &subject, + }, + { + Message: &message, + Subject: &subject, + }, + }, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + messages := app.SyncQueues.Queues["subscribed-queue3"].Messages + assert.Len(t, messages, 2) + + body := string(messages[0].MessageBody) + assert.Contains(t, body, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, body, "Type") + assert.Contains(t, body, "MessageId") + assert.Contains(t, body, "TopicArn") + assert.Contains(t, body, subject) + assert.Contains(t, body, "Signature") + assert.Contains(t, body, "SigningCertURL") + assert.Contains(t, body, "UnsubscribeURL") + assert.Contains(t, body, "SubscribeURL") + assert.Contains(t, body, "MessageAttributes") +} + +func Test_Publish_batch_http_json(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + response, err := snsClient.PublishBatch(context.TODO(), &sns.PublishBatchInput{ + TopicArn: &topicArn, + + PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ + { + Message: &message, + }, + { + Message: &message, + }, + }, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_batch_https_json_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + response, err := snsClient.PublishBatch(context.TODO(), &sns.PublishBatchInput{ + TopicArn: &topicArn, + + PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ + { + Message: &message, + }, + { + Message: &message, + }, + }, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_batch_https_json_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Raw = false + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + response, err := snsClient.PublishBatch(context.TODO(), &sns.PublishBatchInput{ + TopicArn: &topicArn, + PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ + { + Message: &message, + Subject: &subject, + }, + { + Message: &message, + Subject: &subject, + }, + }, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + assert.True(t, called) + assert.Contains(t, httpMessage, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, httpMessage, "Type") + assert.Contains(t, httpMessage, "MessageId") + assert.Contains(t, httpMessage, "TopicArn") + assert.Contains(t, httpMessage, subject) + assert.Contains(t, httpMessage, "Signature") + assert.Contains(t, httpMessage, "SigningCertURL") + assert.Contains(t, httpMessage, "UnsubscribeURL") + assert.Contains(t, httpMessage, "SubscribeURL") + assert.Contains(t, httpMessage, "MessageAttributes") +} + +func Test_Publish_batch_sqs_xml_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + PublishBatchRequestEntries []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"PublishBatchRequestEntries"` + }{ + Action: "PublishBatch", + TopicArn: topicArn, + PublishBatchRequestEntries: []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + { + Message: message, + Subject: subject, + }, + { + Message: message, + Subject: subject, + }, + }, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 2) + assert.Equal(t, message, string(messages[0].MessageBody)) + assert.Equal(t, message, string(messages[1].MessageBody)) +} + +func Test_Publish_batch_sqs_xml_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + topicArn := app.SyncTopics.Topics["unit-topic3"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + PublishBatchRequestEntries []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"PublishBatchRequestEntries"` + }{ + Action: "PublishBatch", + TopicArn: topicArn, + PublishBatchRequestEntries: []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + { + Message: message, + Subject: subject, + }, + { + Message: message, + Subject: subject, + }, + }, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + messages := app.SyncQueues.Queues["subscribed-queue3"].Messages + assert.Len(t, messages, 2) + + body := string(messages[0].MessageBody) + assert.Contains(t, body, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, body, "Type") + assert.Contains(t, body, "MessageId") + assert.Contains(t, body, "TopicArn") + assert.Contains(t, body, subject) + assert.Contains(t, body, "Signature") + assert.Contains(t, body, "SigningCertURL") + assert.Contains(t, body, "UnsubscribeURL") + assert.Contains(t, body, "SubscribeURL") + assert.Contains(t, body, "MessageAttributes") +} + +func Test_Publish_batch_http_xml(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + PublishBatchRequestEntries []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"PublishBatchRequestEntries"` + }{ + Action: "PublishBatch", + TopicArn: topicArn, + PublishBatchRequestEntries: []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + { + Message: message, + Subject: subject, + }, + { + Message: message, + Subject: subject, + }, + }, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_batch_https_xml_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + PublishBatchRequestEntries []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"PublishBatchRequestEntries"` + }{ + Action: "PublishBatch", + TopicArn: topicArn, + PublishBatchRequestEntries: []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + { + Message: message, + Subject: subject, + }, + { + Message: message, + Subject: subject, + }, + }, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_batch_https_xml_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Raw = false + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + PublishBatchRequestEntries []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"PublishBatchRequestEntries"` + }{ + Action: "PublishBatch", + TopicArn: topicArn, + PublishBatchRequestEntries: []struct { + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + { + Message: message, + Subject: subject, + }, + { + Message: message, + Subject: subject, + }, + }, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + assert.True(t, called) + assert.Contains(t, httpMessage, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, httpMessage, "Type") + assert.Contains(t, httpMessage, "MessageId") + assert.Contains(t, httpMessage, "TopicArn") + assert.Contains(t, httpMessage, subject) + assert.Contains(t, httpMessage, "Signature") + assert.Contains(t, httpMessage, "SigningCertURL") + assert.Contains(t, httpMessage, "UnsubscribeURL") + assert.Contains(t, httpMessage, "SubscribeURL") + assert.Contains(t, httpMessage, "MessageAttributes") +} From 07b9b9b92bbe426bf53275137e78d130d4590922 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Wed, 28 Aug 2024 17:10:07 +0100 Subject: [PATCH 10/41] fix send message --- app/gosns/gosns.go | 1 - app/gosqs/send_message.go | 2 +- app/gosqs/send_message_batch.go | 2 +- app/models/sqs.go | 8 ++++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 61fecf58..649d824c 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -270,7 +270,6 @@ func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. - func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { log.WithFields(log.Fields{ "sns": msg, diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index cc180f1a..8915978e 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -60,7 +60,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { log.Debugf("Putting Message in Queue: [%s]", queueName) msg := app.Message{MessageBody: []byte(messageBody)} if len(messageAttributes) > 0 { - oldStyleMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(messageAttributes) + oldStyleMessageAttributes := requestBody.GetMessageAttributes() msg.MessageAttributes = oldStyleMessageAttributes msg.MD5OfMessageAttributes = common.HashAttributes(oldStyleMessageAttributes) } diff --git a/app/gosqs/send_message_batch.go b/app/gosqs/send_message_batch.go index e670c0d4..9a8cf411 100644 --- a/app/gosqs/send_message_batch.go +++ b/app/gosqs/send_message_batch.go @@ -62,7 +62,7 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody for _, sendEntry := range sendEntries { msg := app.Message{MessageBody: []byte(sendEntry.MessageBody)} if len(sendEntry.MessageAttributes) > 0 { - oldStyleMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(sendEntry.MessageAttributes) + oldStyleMessageAttributes := sendEntry.GetMessageAttributes() msg.MessageAttributes = oldStyleMessageAttributes msg.MD5OfMessageAttributes = common.HashAttributes(oldStyleMessageAttributes) } diff --git a/app/models/sqs.go b/app/models/sqs.go index 5bc9f464..e26f0efd 100644 --- a/app/models/sqs.go +++ b/app/models/sqs.go @@ -186,6 +186,10 @@ type SendMessageRequest struct { QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` } +func (r *SendMessageRequest) GetMessageAttributes() map[string]app.MessageAttributeValue { + return convertToOldMessageAttributeValueStructure(r.MessageAttributes) +} + func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { for i := 1; true; i++ { nameKey := fmt.Sprintf("MessageAttribute.%d.Name", i) @@ -282,6 +286,10 @@ type SendMessageBatchRequestEntry struct { MessageSystemAttributes map[string]MessageAttributeValue `json:"MessageSystemAttributes" schema:"MessageSystemAttributes"` // NOTE: not implemented } +func (e *SendMessageBatchRequestEntry) GetMessageAttributes() map[string]app.MessageAttributeValue { + return convertToOldMessageAttributeValueStructure(e.MessageAttributes) +} + // Get Queue Url Request func NewGetQueueUrlRequest() *GetQueueUrlRequest { return &GetQueueUrlRequest{} From a3fed242a7c2144e26eeac3ae9408c8f293ca1fc Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:18:19 +0100 Subject: [PATCH 11/41] add id (required) to publish entries --- smoke_tests/sns_publish_batch_test.go | 83 ++++++++++++++++++--------- smoke_tests/sns_publish_test.go | 26 ++++----- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/smoke_tests/sns_publish_batch_test.go b/smoke_tests/sns_publish_batch_test.go index d3522f38..0f6b81df 100644 --- a/smoke_tests/sns_publish_batch_test.go +++ b/smoke_tests/sns_publish_batch_test.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" "github.com/gavv/httpexpect/v2" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -41,18 +42,20 @@ func Test_Publish_batch_sqs_json_raw(t *testing.T) { TopicArn: &topicArn, PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ { - Message: &message, - Subject: &subject, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), + Subject: aws.String(subject), }, { - Message: &message, - Subject: &subject, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), + Subject: aws.String(subject), }, }, }) - assert.Nil(t, err) - assert.NotNil(t, response) + require.Nil(t, err) + require.NotNil(t, response) messages := app.SyncQueues.Queues["subscribed-queue1"].Messages require.Len(t, messages, 2) @@ -81,18 +84,20 @@ func Test_Publish_batch_sqs_json_not_raw(t *testing.T) { TopicArn: &topicArn, PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ { - Message: &message, - Subject: &subject, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), + Subject: aws.String(subject), }, { - Message: &message, - Subject: &subject, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), + Subject: aws.String(subject), }, }, }) - assert.Nil(t, err) - assert.NotNil(t, response) + require.Nil(t, err) + require.NotNil(t, response) messages := app.SyncQueues.Queues["subscribed-queue3"].Messages assert.Len(t, messages, 2) @@ -148,16 +153,18 @@ func Test_Publish_batch_http_json(t *testing.T) { PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ { - Message: &message, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), }, { - Message: &message, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), }, }, }) - assert.Nil(t, err) - assert.NotNil(t, response) + require.Nil(t, err) + require.NotNil(t, response) assert.True(t, called) assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) @@ -202,16 +209,18 @@ func Test_Publish_batch_https_json_raw(t *testing.T) { PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ { - Message: &message, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), }, { - Message: &message, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), }, }, }) - assert.Nil(t, err) - assert.NotNil(t, response) + require.Nil(t, err) + require.NotNil(t, response) assert.True(t, called) assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) @@ -257,18 +266,20 @@ func Test_Publish_batch_https_json_not_raw(t *testing.T) { TopicArn: &topicArn, PublishBatchRequestEntries: []types.PublishBatchRequestEntry{ { - Message: &message, - Subject: &subject, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), + Subject: aws.String(subject), }, { - Message: &message, - Subject: &subject, + Id: aws.String(uuid.NewString()), + Message: aws.String(message), + Subject: aws.String(subject), }, }, }) - assert.Nil(t, err) - assert.NotNil(t, response) + require.Nil(t, err) + require.NotNil(t, response) assert.True(t, called) assert.Contains(t, httpMessage, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") @@ -303,6 +314,7 @@ func Test_Publish_batch_sqs_xml_raw(t *testing.T) { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` PublishBatchRequestEntries []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` } `schema:"PublishBatchRequestEntries"` @@ -310,14 +322,17 @@ func Test_Publish_batch_sqs_xml_raw(t *testing.T) { Action: "PublishBatch", TopicArn: topicArn, PublishBatchRequestEntries: []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` }{ { + Id: uuid.NewString(), Message: message, Subject: subject, }, { + Id: uuid.NewString(), Message: message, Subject: subject, }, @@ -356,6 +371,7 @@ func Test_Publish_batch_sqs_xml_not_raw(t *testing.T) { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` PublishBatchRequestEntries []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` } `schema:"PublishBatchRequestEntries"` @@ -363,14 +379,17 @@ func Test_Publish_batch_sqs_xml_not_raw(t *testing.T) { Action: "PublishBatch", TopicArn: topicArn, PublishBatchRequestEntries: []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` }{ { + Id: uuid.NewString(), Message: message, Subject: subject, }, { + Id: uuid.NewString(), Message: message, Subject: subject, }, @@ -436,6 +455,7 @@ func Test_Publish_batch_http_xml(t *testing.T) { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` PublishBatchRequestEntries []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` } `schema:"PublishBatchRequestEntries"` @@ -443,14 +463,17 @@ func Test_Publish_batch_http_xml(t *testing.T) { Action: "PublishBatch", TopicArn: topicArn, PublishBatchRequestEntries: []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` }{ { + Id: uuid.NewString(), Message: message, Subject: subject, }, { + Id: uuid.NewString(), Message: message, Subject: subject, }, @@ -505,6 +528,7 @@ func Test_Publish_batch_https_xml_raw(t *testing.T) { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` PublishBatchRequestEntries []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` } `schema:"PublishBatchRequestEntries"` @@ -512,14 +536,17 @@ func Test_Publish_batch_https_xml_raw(t *testing.T) { Action: "PublishBatch", TopicArn: topicArn, PublishBatchRequestEntries: []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` }{ { + Id: uuid.NewString(), Message: message, Subject: subject, }, { + Id: uuid.NewString(), Message: message, Subject: subject, }, @@ -575,6 +602,7 @@ func Test_Publish_batch_https_xml_not_raw(t *testing.T) { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` PublishBatchRequestEntries []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` } `schema:"PublishBatchRequestEntries"` @@ -582,14 +610,17 @@ func Test_Publish_batch_https_xml_not_raw(t *testing.T) { Action: "PublishBatch", TopicArn: topicArn, PublishBatchRequestEntries: []struct { + Id string `schema:"Id"` Message string `schema:"Message"` Subject string `schema:"Subject"` }{ { + Id: uuid.NewString(), Message: message, Subject: subject, }, { + Id: uuid.NewString(), Message: message, Subject: subject, }, diff --git a/smoke_tests/sns_publish_test.go b/smoke_tests/sns_publish_test.go index 30082e57..22231639 100644 --- a/smoke_tests/sns_publish_test.go +++ b/smoke_tests/sns_publish_test.go @@ -39,9 +39,9 @@ func Test_Publish_sqs_json_raw(t *testing.T) { message := "{\"IAm\": \"aMessage\"}" subject := "I am a subject" response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ - TopicArn: &topicArn, - Message: &message, - Subject: &subject, + TopicArn: aws.String(topicArn), + Message: aws.String(message), + Subject: aws.String(subject), }) assert.Nil(t, err) @@ -70,9 +70,9 @@ func Test_Publish_sqs_json_not_raw(t *testing.T) { message := "{\"IAm\": \"aMessage\"}" subject := "I am a subject" response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ - TopicArn: &topicArn, - Message: &message, - Subject: &subject, + TopicArn: aws.String(topicArn), + Message: aws.String(message), + Subject: aws.String(subject), }) assert.Nil(t, err) @@ -128,8 +128,8 @@ func Test_Publish_http_json(t *testing.T) { topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn message := "{\"IAm\": \"aMessage\"}" response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ - TopicArn: &topicArn, - Message: &message, + TopicArn: aws.String(topicArn), + Message: aws.String(message), }) assert.Nil(t, err) @@ -174,8 +174,8 @@ func Test_Publish_https_json_raw(t *testing.T) { topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn message := "{\"IAm\": \"aMessage\"}" response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ - TopicArn: &topicArn, - Message: &message, + TopicArn: aws.String(topicArn), + Message: aws.String(message), }) assert.Nil(t, err) @@ -222,9 +222,9 @@ func Test_Publish_https_json_not_raw(t *testing.T) { message := "{\"IAm\": \"aMessage\"}" subject := "I am a subject" response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ - TopicArn: &topicArn, - Message: &message, - Subject: &subject, + TopicArn: aws.String(topicArn), + Message: aws.String(message), + Subject: aws.String(subject), }) assert.Nil(t, err) From faf53c418d01f1fba6d3426618ae17cea5fa0078 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:14:43 +0100 Subject: [PATCH 12/41] fix smoke tests --- app/gosns/publish_batch.go | 14 +- app/models/sns.go | 6 +- smoke_tests/sns_publish_batch_test.go | 223 ++++++++++++++++---------- 3 files changed, 150 insertions(+), 93 deletions(-) diff --git a/app/gosns/publish_batch.go b/app/gosns/publish_batch.go index 775262e2..07e25ce6 100644 --- a/app/gosns/publish_batch.go +++ b/app/gosns/publish_batch.go @@ -24,10 +24,10 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { return utils.CreateErrorResponseV1("InvalidParameterValue", false) } - if len(requestBody.PublishBatchRequestEntries) == 0 { + if len(requestBody.PublishBatchRequestEntries.Member) == 0 { return utils.CreateErrorResponseV1("EmptyBatchRequest", false) } - if len(requestBody.PublishBatchRequestEntries) > 10 { + if len(requestBody.PublishBatchRequestEntries.Member) > 10 { return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", false) } @@ -41,7 +41,10 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { seen := make(map[string]bool) - for _, entry := range requestBody.PublishBatchRequestEntries { + for _, entry := range requestBody.PublishBatchRequestEntries.Member { + if entry == nil { // we use gorilla schema to parse value params. Indexing on the aws client starts at 1 but gorilla schema starts at 0 so we may have a nil entry at the start of the slice + continue + } if entry.ID == "" { // This is a required field for the PublishBatchRequestEntry entity but doesn't seem required in the request. // If it's not present in the request then assume we should generate one. @@ -55,7 +58,10 @@ func PublishBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { successfulEntries := []models.PublishBatchResultEntry{} failedEntries := []models.BatchResultErrorEntry{} - for _, entry := range requestBody.PublishBatchRequestEntries { + for _, entry := range requestBody.PublishBatchRequestEntries.Member { + if entry == nil { // we use gorilla schema to parse value params. Indexing on the aws client starts at 1 but gorilla schema starts at 0 so we may have a nil entry at the start of the slice + continue + } // we now know all the entry.IDs are unique and non-blank for _, sub := range topic.Subscriptions { switch app.Protocol(sub.Protocol) { diff --git a/app/models/sns.go b/app/models/sns.go index aad8fc1f..91a9eba5 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -316,8 +316,10 @@ func NewPublishBatchRequest() *PublishBatchRequest { } type PublishBatchRequest struct { - PublishBatchRequestEntries []*PublishBatchRequestEntry `json:"PublishBatchRequestEntries" schema:"PublishBatchRequestEntries"` - TopicArn string `json:"TopicArn" schema:"TopicArn"` + PublishBatchRequestEntries struct { + Member []*PublishBatchRequestEntry `json:"member" schema:"member"` + } `json:"PublishBatchRequestEntries" schema:"PublishBatchRequestEntries"` + TopicArn string `json:"TopicArn" schema:"TopicArn"` } type PublishBatchRequestEntry struct { diff --git a/smoke_tests/sns_publish_batch_test.go b/smoke_tests/sns_publish_batch_test.go index 0f6b81df..159e35e7 100644 --- a/smoke_tests/sns_publish_batch_test.go +++ b/smoke_tests/sns_publish_batch_test.go @@ -313,32 +313,41 @@ func Test_Publish_batch_sqs_xml_raw(t *testing.T) { requestBody := struct { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` - PublishBatchRequestEntries []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` } `schema:"PublishBatchRequestEntries"` }{ Action: "PublishBatch", TopicArn: topicArn, - PublishBatchRequestEntries: []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries: struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` }{ - { - Id: uuid.NewString(), - Message: message, - Subject: subject, - }, - { - Id: uuid.NewString(), - Message: message, - Subject: subject, + Member: []struct { + Id string "schema:\"Id\"" + Message string "schema:\"Message\"" + Subject string "schema:\"Subject\"" + }{ + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, }, }, } - e.POST("/"). WithForm(requestBody). Expect(). @@ -346,7 +355,7 @@ func Test_Publish_batch_sqs_xml_raw(t *testing.T) { Body().Raw() messages := app.SyncQueues.Queues["subscribed-queue1"].Messages - assert.Len(t, messages, 2) + require.Len(t, messages, 2) assert.Equal(t, message, string(messages[0].MessageBody)) assert.Equal(t, message, string(messages[1].MessageBody)) } @@ -370,28 +379,38 @@ func Test_Publish_batch_sqs_xml_not_raw(t *testing.T) { requestBody := struct { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` - PublishBatchRequestEntries []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` } `schema:"PublishBatchRequestEntries"` }{ Action: "PublishBatch", TopicArn: topicArn, - PublishBatchRequestEntries: []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries: struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` }{ - { - Id: uuid.NewString(), - Message: message, - Subject: subject, - }, - { - Id: uuid.NewString(), - Message: message, - Subject: subject, + Member: []struct { + Id string "schema:\"Id\"" + Message string "schema:\"Message\"" + Subject string "schema:\"Subject\"" + }{ + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, }, }, } @@ -454,28 +473,38 @@ func Test_Publish_batch_http_xml(t *testing.T) { requestBody := struct { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` - PublishBatchRequestEntries []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` } `schema:"PublishBatchRequestEntries"` }{ Action: "PublishBatch", TopicArn: topicArn, - PublishBatchRequestEntries: []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries: struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` }{ - { - Id: uuid.NewString(), - Message: message, - Subject: subject, - }, - { - Id: uuid.NewString(), - Message: message, - Subject: subject, + Member: []struct { + Id string "schema:\"Id\"" + Message string "schema:\"Message\"" + Subject string "schema:\"Subject\"" + }{ + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, }, }, } @@ -527,28 +556,38 @@ func Test_Publish_batch_https_xml_raw(t *testing.T) { requestBody := struct { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` - PublishBatchRequestEntries []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` } `schema:"PublishBatchRequestEntries"` }{ Action: "PublishBatch", TopicArn: topicArn, - PublishBatchRequestEntries: []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries: struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` }{ - { - Id: uuid.NewString(), - Message: message, - Subject: subject, - }, - { - Id: uuid.NewString(), - Message: message, - Subject: subject, + Member: []struct { + Id string "schema:\"Id\"" + Message string "schema:\"Message\"" + Subject string "schema:\"Subject\"" + }{ + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, }, }, } @@ -601,28 +640,38 @@ func Test_Publish_batch_https_xml_not_raw(t *testing.T) { requestBody := struct { Action string `schema:"Action"` TopicArn string `schema:"TopicArn"` - PublishBatchRequestEntries []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` } `schema:"PublishBatchRequestEntries"` }{ Action: "PublishBatch", TopicArn: topicArn, - PublishBatchRequestEntries: []struct { - Id string `schema:"Id"` - Message string `schema:"Message"` - Subject string `schema:"Subject"` + PublishBatchRequestEntries: struct { + Member []struct { + Id string `schema:"Id"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + } `schema:"member"` }{ - { - Id: uuid.NewString(), - Message: message, - Subject: subject, - }, - { - Id: uuid.NewString(), - Message: message, - Subject: subject, + Member: []struct { + Id string "schema:\"Id\"" + Message string "schema:\"Message\"" + Subject string "schema:\"Subject\"" + }{ + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, + { + Id: uuid.NewString(), + Message: message, + Subject: subject, + }, }, }, } From 0e7bfeba8d2530d8ce30f44accead4e7871659b9 Mon Sep 17 00:00:00 2001 From: rgodden <7768980+goddenrich@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:34:37 +0100 Subject: [PATCH 13/41] fix publish batch unit tests --- app/gosns/publish_batch_test.go | 42 ++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/app/gosns/publish_batch_test.go b/app/gosns/publish_batch_test.go index 178a080d..731f249b 100644 --- a/app/gosns/publish_batch_test.go +++ b/app/gosns/publish_batch_test.go @@ -44,8 +44,10 @@ func TestBatchPublishV1_empty_topicArn_error(t *testing.T) { message := "{\"IAm\": \"aMessage\"}" e := &models.PublishBatchRequestEntry{ID: "1", Message: message} *v = models.PublishBatchRequest{ - TopicArn: "", - PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e}, + TopicArn: "", + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: []*models.PublishBatchRequestEntry{e}}, } return true } @@ -68,8 +70,10 @@ func TestBatchPublishV1_nonexistent_topicArn_error(t *testing.T) { message := "{\"IAm\": \"aMessage\"}" e := &models.PublishBatchRequestEntry{ID: "1", Message: message} *v = models.PublishBatchRequest{ - TopicArn: "non-existing", - PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e}, + TopicArn: "non-existing", + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: []*models.PublishBatchRequestEntry{e}}, } return true } @@ -91,8 +95,10 @@ func TestBatchPublishV1_zero_entries_error(t *testing.T) { utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.PublishBatchRequest) *v = models.PublishBatchRequest{ - TopicArn: topicArn, - PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{}, + TopicArn: topicArn, + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: []*models.PublishBatchRequestEntry{}}, } return true } @@ -120,8 +126,10 @@ func TestBatchPublishV1_too_many_entries_error(t *testing.T) { entries = append(entries, e) } *v = models.PublishBatchRequest{ - TopicArn: topicArn, - PublishBatchRequestEntries: entries, + TopicArn: topicArn, + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: entries}, } return true } @@ -146,8 +154,10 @@ func TestBatchPublishV1_success_sqs(t *testing.T) { v := resultingStruct.(*models.PublishBatchRequest) e := &models.PublishBatchRequestEntry{ID: "1", Message: message} *v = models.PublishBatchRequest{ - TopicArn: topicArn, - PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e}, + TopicArn: topicArn, + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: []*models.PublishBatchRequestEntry{e}}, } return true } @@ -178,8 +188,10 @@ func TestBatchPublishV1_duplicate_id_errors(t *testing.T) { v := resultingStruct.(*models.PublishBatchRequest) e := &models.PublishBatchRequestEntry{ID: "1", Message: message} *v = models.PublishBatchRequest{ - TopicArn: topicArn, - PublishBatchRequestEntries: []*models.PublishBatchRequestEntry{e, e}, + TopicArn: topicArn, + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: []*models.PublishBatchRequestEntry{e, e}}, } return true } @@ -210,8 +222,10 @@ func TestBatchPublishV1_multiple_success_sqs(t *testing.T) { utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.PublishBatchRequest) *v = models.PublishBatchRequest{ - TopicArn: topicArn, - PublishBatchRequestEntries: entries, + TopicArn: topicArn, + PublishBatchRequestEntries: struct { + Member []*models.PublishBatchRequestEntry `json:"member" schema:"member"` + }{Member: entries}, } return true } From 93f700c6f03cbbc7bee5fa60c9e0bc3d0941a55c Mon Sep 17 00:00:00 2001 From: Koji Saiki Date: Wed, 27 Dec 2023 15:02:35 +0900 Subject: [PATCH 14/41] add sample test --- app/gosqs/gosqs_test.go | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index b7b398be..071ce46c 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -1,7 +1,9 @@ package gosqs import ( + "bytes" "context" + "encoding/json" "encoding/xml" "net/http" "net/http/httptest" @@ -145,6 +147,62 @@ func TestCreateQueuehandler_POST_CreateQueue(t *testing.T) { } } +type CreateQueueRequest struct { + QueueName string `json: QueueName` + VisibilityTimeout int `json: VisibilityTimeout` + MaximumMessageSize int `json: MaximumMessageSize` +} + +func TestCreateQueuehandler_POST_CreateQueue_aws_json(t *testing.T) { + queueName := "UnitTestQueue1" + requestObj := new(CreateQueueRequest) + requestObj.QueueName = queueName + requestObj.VisibilityTimeout = 60 + requestObj.MaximumMessageSize = 2048 + requestJson, _ := json.Marshal(requestObj) + // 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", "/", bytes.NewBuffer(requestJson)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-Amz-Target", "AmazonSQS.CreateQueue") + req.Header.Set("Content-Type", "application/x-amz-json-1.0") + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(CreateQueue) + + // 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 := queueName + if !strings.Contains(rr.Body.String(), expected) { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } + expectedQueue := &app.Queue{ + Name: queueName, + URL: "http://://" + queueName, + Arn: "arn:aws:sqs:::" + queueName, + TimeoutSecs: 60, + MaximumMessageSize: 2048, + Duplicates: make(map[string]time.Time), + } + actualQueue := app.SyncQueues.Queues[queueName] + if !reflect.DeepEqual(expectedQueue, actualQueue) { + t.Fatalf("expected %+v, got %+v", expectedQueue, actualQueue) + } +} + func TestCreateFIFOQueuehandler_POST_CreateQueue(t *testing.T) { req, err := http.NewRequest("POST", "/", nil) if err != nil { From 681ddcd6316ff952071c82197d50e5b653f0b411 Mon Sep 17 00:00:00 2001 From: Koji Saiki Date: Wed, 27 Dec 2023 15:52:47 +0900 Subject: [PATCH 15/41] super roughly passed --- app/gosqs/gosqs.go | 52 ++++++++++++++++++++++++++++++++--- app/gosqs/gosqs_test.go | 10 +++---- app/gosqs/queue_attributes.go | 5 ++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 63e1ee40..3126a638 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -1,6 +1,7 @@ package gosqs import ( + "encoding/json" "encoding/xml" "fmt" "net/http" @@ -114,9 +115,45 @@ func ListQueues(w http.ResponseWriter, req *http.Request) { } } +type CreateQueueRequest struct { + QueueName string `json: QueueName` + VisibilityTimeout int `json: VisibilityTimeout` + MaximumMessageSize int `json: MaximumMessageSize` +} + +type Attributes map[string]string + +func parseCreateQueueRequestBody(w http.ResponseWriter, req *http.Request) (bool, CreateQueueRequest, Attributes) { + requestBody := new(CreateQueueRequest) + attributes := map[string]string{} + + byJson := false + + switch req.Header.Get("Content-Type") { + case "application/x-amz-json-1.0": + //Read body data to parse json + decoder := json.NewDecoder(req.Body) + err := decoder.Decode(&requestBody) + if err != nil { + panic(err) + } + // TODO: parse from json, and find actual attribute format in aws-json protocol. + attributes["VisibilityTimeout"] = "60" + attributes["MaximumMessageSize"] = "2048" + byJson = true + default: + requestBody.QueueName = req.FormValue("QueueName") + attributes = extractQueueAttributes(req.Form) + } + + return byJson, *requestBody, attributes +} + func CreateQueue(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/xml") - queueName := req.FormValue("QueueName") + byJson, requestBody, attr := parseCreateQueueRequestBody(w, req) + + queueName := requestBody.QueueName queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/" + app.CurrentEnvironment.AccountID + "/" + queueName @@ -139,9 +176,16 @@ func CreateQueue(w http.ResponseWriter, req *http.Request) { EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, Duplicates: make(map[string]time.Time), } - if err := validateAndSetQueueAttributes(queue, req.Form); err != nil { - createErrorResponse(w, req, err.Error()) - return + if byJson { + if err := validateAndSetQueueAttributesJson(queue, attr); err != nil { + createErrorResponse(w, req, err.Error()) + return + } + } else { + if err := validateAndSetQueueAttributes(queue, req.Form); err != nil { + createErrorResponse(w, req, err.Error()) + return + } } app.SyncQueues.Lock() app.SyncQueues.Queues[queueName] = queue diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 071ce46c..6268d2a6 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -147,11 +147,11 @@ func TestCreateQueuehandler_POST_CreateQueue(t *testing.T) { } } -type CreateQueueRequest struct { - QueueName string `json: QueueName` - VisibilityTimeout int `json: VisibilityTimeout` - MaximumMessageSize int `json: MaximumMessageSize` -} +// type CreateQueueRequest struct { +// QueueName string `json: QueueName` +// VisibilityTimeout int `json: VisibilityTimeout` +// MaximumMessageSize int `json: MaximumMessageSize` +// } func TestCreateQueuehandler_POST_CreateQueue_aws_json(t *testing.T) { queueName := "UnitTestQueue1" diff --git a/app/gosqs/queue_attributes.go b/app/gosqs/queue_attributes.go index 312c7b0d..5fce3b1a 100644 --- a/app/gosqs/queue_attributes.go +++ b/app/gosqs/queue_attributes.go @@ -31,6 +31,11 @@ var ( // TODO Currently it only supports VisibilityTimeout, MaximumMessageSize, DelaySeconds, RedrivePolicy and ReceiveMessageWaitTimeSeconds attributes. func validateAndSetQueueAttributes(q *app.Queue, u url.Values) error { attr := extractQueueAttributes(u) + + return validateAndSetQueueAttributesJson(q, attr) +} + +func validateAndSetQueueAttributesJson(q *app.Queue, attr Attributes) error { visibilityTimeout, _ := strconv.Atoi(attr["VisibilityTimeout"]) if visibilityTimeout != 0 { q.TimeoutSecs = visibilityTimeout From 1dcdb3649432db40c123b6bed0e066094c0e47db Mon Sep 17 00:00:00 2001 From: Koji Saiki Date: Wed, 27 Dec 2023 16:05:48 +0900 Subject: [PATCH 16/41] update attribute format on aws-json protocol --- app/gosqs/gosqs.go | 21 +++++++++------------ app/gosqs/gosqs_test.go | 11 +++-------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 3126a638..91f1bf82 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -116,17 +116,17 @@ func ListQueues(w http.ResponseWriter, req *http.Request) { } type CreateQueueRequest struct { - QueueName string `json: QueueName` - VisibilityTimeout int `json: VisibilityTimeout` - MaximumMessageSize int `json: MaximumMessageSize` + QueueName string `json: QueueName` + Attributes map[string]string `json: Attributes` + Tags map[string]string `json: Tags` } type Attributes map[string]string -func parseCreateQueueRequestBody(w http.ResponseWriter, req *http.Request) (bool, CreateQueueRequest, Attributes) { +func parseCreateQueueRequestBody(w http.ResponseWriter, req *http.Request) (bool, CreateQueueRequest) { requestBody := new(CreateQueueRequest) - attributes := map[string]string{} + // Should remove this flag after validateAndSetQueueAttributes was updated byJson := false switch req.Header.Get("Content-Type") { @@ -137,21 +137,18 @@ func parseCreateQueueRequestBody(w http.ResponseWriter, req *http.Request) (bool if err != nil { panic(err) } - // TODO: parse from json, and find actual attribute format in aws-json protocol. - attributes["VisibilityTimeout"] = "60" - attributes["MaximumMessageSize"] = "2048" byJson = true default: requestBody.QueueName = req.FormValue("QueueName") - attributes = extractQueueAttributes(req.Form) + requestBody.Attributes = extractQueueAttributes(req.Form) } - return byJson, *requestBody, attributes + return byJson, *requestBody } func CreateQueue(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/xml") - byJson, requestBody, attr := parseCreateQueueRequestBody(w, req) + byJson, requestBody := parseCreateQueueRequestBody(w, req) queueName := requestBody.QueueName @@ -177,7 +174,7 @@ func CreateQueue(w http.ResponseWriter, req *http.Request) { Duplicates: make(map[string]time.Time), } if byJson { - if err := validateAndSetQueueAttributesJson(queue, attr); err != nil { + if err := validateAndSetQueueAttributesJson(queue, requestBody.Attributes); err != nil { createErrorResponse(w, req, err.Error()) return } diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 6268d2a6..4e9ec334 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -147,18 +147,13 @@ func TestCreateQueuehandler_POST_CreateQueue(t *testing.T) { } } -// type CreateQueueRequest struct { -// QueueName string `json: QueueName` -// VisibilityTimeout int `json: VisibilityTimeout` -// MaximumMessageSize int `json: MaximumMessageSize` -// } - func TestCreateQueuehandler_POST_CreateQueue_aws_json(t *testing.T) { queueName := "UnitTestQueue1" requestObj := new(CreateQueueRequest) requestObj.QueueName = queueName - requestObj.VisibilityTimeout = 60 - requestObj.MaximumMessageSize = 2048 + requestObj.Attributes = map[string]string{} + requestObj.Attributes["VisibilityTimeout"] = "60" + requestObj.Attributes["MaximumMessageSize"] = "2048" requestJson, _ := json.Marshal(requestObj) // 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. From 8c191197914c9e5dac3ac523e84b3cd67d284cf6 Mon Sep 17 00:00:00 2001 From: Koji Saiki Date: Wed, 3 Jan 2024 10:22:03 +0900 Subject: [PATCH 17/41] Try routing refactoring --- app/router/router.go | 41 ++++++++++++++++++++++++++++++++++++--- app/router/router_test.go | 24 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/router/router.go b/app/router/router.go index 699b6ab0..91273adc 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -3,6 +3,7 @@ package router import ( "io" "net/http" + "strings" log "github.com/sirupsen/logrus" @@ -65,14 +66,15 @@ func health(w http.ResponseWriter, req *http.Request) { } func actionHandler(w http.ResponseWriter, req *http.Request) { + action := extractAction(req) log.WithFields( log.Fields{ - "action": req.FormValue("Action"), + "action": action, "url": req.URL, }).Debug("Handling URL request") - fn, ok := routingTable[req.FormValue("Action")] + fn, ok := routingTable[action] if !ok { - log.Println("Bad Request - Action:", req.FormValue("Action")) + log.Println("Bad Request - Action:", action) w.WriteHeader(http.StatusBadRequest) io.WriteString(w, "Bad Request") return @@ -85,3 +87,36 @@ func pemHandler(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) w.Write(sns.PemKEY) } + +type AwsProtocol int + +const ( + AwsJsonProtocol AwsProtocol = iota + AwsQueryProtocol AwsProtocol = iota +) + +// Extract target Action from the request. +// How contains the Action name is different with aws-query protocol and aws-json protocol. +func extractAction(req *http.Request) string { + protocol := resolveProtocol(req) + switch protocol { + case AwsJsonProtocol: + // Get action from X-Amz-Target header + action := req.Header.Get("X-Amz-Target") + // Action value will be like as "AmazonSQS.CreateQueue". + // After dot should be the action name. + return strings.Split(action, ".")[1] + case AwsQueryProtocol: + return req.FormValue("Action") + } + return "" +} + +// Determine which protocol is used. +func resolveProtocol(req *http.Request) AwsProtocol { + // Use content-type to determine protocol + if req.Header.Get("Content-Type") == "application/x-amz-json-1.0" { + return AwsJsonProtocol + } + return AwsQueryProtocol +} diff --git a/app/router/router_test.go b/app/router/router_test.go index 41288d70..b255073a 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -1,6 +1,8 @@ package router import ( + "bytes" + "encoding/json" "net/http" "net/http/httptest" "net/url" @@ -91,6 +93,28 @@ func TestIndexServerhandler_POST_GoodRequest_With_URL(t *testing.T) { } } +func TestIndexServerhandler_POST_GoodRequest_With_URL_And_Aws_Json_Protocol(t *testing.T) { + json, _ := json.Marshal(map[string]string{ + "QueueName": "local-queue1", + }) + req, err := http.NewRequest("POST", "/100010001000/local-queue1", bytes.NewBuffer(json)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-Amz-Target", "AmazonSQS.CreateQueue") + req.Header.Set("Content-Type", "application/x-amz-json-1.0") + + rr := httptest.NewRecorder() + + New().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) + } +} + func TestIndexServerhandler_GET_GoodRequest_Pem_cert(t *testing.T) { req, err := http.NewRequest("GET", "/SimpleNotificationService/100010001000.pem", nil) From a86ab6c3486f618964e48726a05c0958ffa3e1dc Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Wed, 17 Jan 2024 17:57:50 -0500 Subject: [PATCH 18/41] Refactor for CreateQueue for V1 JSON support --- app/cmd/goaws.go | 4 + app/common.go | 24 +- app/conf/config.go | 53 +-- app/conf/config_test.go | 75 ++-- app/conf/mock-data/mock-config.yaml | 2 + app/fixtures/environment.go | 127 +++++++ app/fixtures/fixtures.go | 7 + app/fixtures/request.go | 7 + app/fixtures/sqs.go | 44 +++ app/gosns/gosns.go | 6 +- app/gosns/gosns_test.go | 20 +- app/gosqs/gosqs.go | 114 +++--- app/gosqs/gosqs_test.go | 499 ++++++++++++++++----------- app/gosqs/queue_attributes.go | 71 ++-- app/gosqs/queue_attributes_test.go | 151 ++++++-- app/interfaces/interfaces.go | 14 + app/mocks/mocks.go | 46 +++ app/models/conversions.go | 36 ++ app/models/conversions_test.go | 75 ++++ app/models/models.go | 172 +++++++++ app/models/models_test.go | 228 ++++++++++++ app/models/responses.go | 45 +++ app/router/router.go | 46 ++- app/router/router_test.go | 186 ++++++++++ app/servertest/server_test.go | 7 + app/sqs.go | 32 +- app/sqs_messages.go | 11 - app/utils/tests.go | 84 +++++ app/utils/utils.go | 70 ++++ app/utils/utils_test.go | 103 ++++++ go.mod | 48 ++- go.sum | 156 ++++++++- smoke_tests/fixtures/fixtures.go | 3 + smoke_tests/fixtures/requests.go | 44 +++ smoke_tests/fixtures/responses.go | 52 +++ smoke_tests/sqs_create_queue_test.go | 447 ++++++++++++++++++++++++ smoke_tests/utils.go | 11 + 37 files changed, 2684 insertions(+), 436 deletions(-) create mode 100644 app/fixtures/environment.go create mode 100644 app/fixtures/fixtures.go create mode 100644 app/fixtures/request.go create mode 100644 app/fixtures/sqs.go create mode 100644 app/interfaces/interfaces.go create mode 100644 app/mocks/mocks.go create mode 100644 app/models/conversions.go create mode 100644 app/models/conversions_test.go create mode 100644 app/models/models.go create mode 100644 app/models/models_test.go create mode 100644 app/models/responses.go create mode 100644 app/utils/tests.go create mode 100644 app/utils/utils.go create mode 100644 app/utils/utils_test.go create mode 100644 smoke_tests/fixtures/fixtures.go create mode 100644 smoke_tests/fixtures/requests.go create mode 100644 smoke_tests/fixtures/responses.go create mode 100644 smoke_tests/sqs_create_queue_test.go create mode 100644 smoke_tests/utils.go diff --git a/app/cmd/goaws.go b/app/cmd/goaws.go index 709fe0a9..4e37de01 100644 --- a/app/cmd/goaws.go +++ b/app/cmd/goaws.go @@ -6,6 +6,8 @@ import ( "os" "time" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/Admiral-Piett/goaws/app" log "github.com/sirupsen/logrus" @@ -61,6 +63,8 @@ func main() { quit := make(chan struct{}, 0) go gosqs.PeriodicTasks(1*time.Second, quit) + utils.InitializeDecoders() + if len(portNumbers) == 1 { log.Warnf("GoAws listening on: 0.0.0.0:%s", portNumbers[0]) err := http.ListenAndServe("0.0.0.0:"+portNumbers[0], r) diff --git a/app/common.go b/app/common.go index 9de0b89a..147a79b5 100644 --- a/app/common.go +++ b/app/common.go @@ -21,12 +21,14 @@ type EnvQueue struct { RedrivePolicy string MaximumMessageSize int VisibilityTimeout int + MessageRetentionPeriod int } type EnvQueueAttributes struct { VisibilityTimeout int ReceiveMessageWaitTimeSeconds int MaximumMessageSize int + MessageRetentionPeriod int // seconds } type Environment struct { @@ -45,25 +47,21 @@ type Environment struct { RandomLatency RandomLatency } -var CurrentEnvironment Environment +// CurrentEnvironment should get overwritten when the app starts up and loads the config. For the +// sake of generating "partial" apps piece-meal during test automation we'll slap these placeholder +// values in here so the resource URLs aren't wonky like `http://://new-queue`. +var CurrentEnvironment = Environment{ + Host: "host", + Port: "port", + Region: "region", + AccountID: "accountID", +} /*** Common ***/ type ResponseMetadata struct { RequestId string `xml:"RequestId"` } -/*** Error Responses ***/ -type ErrorResult struct { - Type string `xml:"Type,omitempty"` - Code string `xml:"Code,omitempty"` - Message string `xml:"Message,omitempty"` -} - -type ErrorResponse struct { - Result ErrorResult `xml:"Error"` - RequestId string `xml:"RequestId"` -} - type RandomLatency struct { Min int Max int diff --git a/app/conf/config.go b/app/conf/config.go index 176682eb..b0052fb3 100644 --- a/app/conf/config.go +++ b/app/conf/config.go @@ -73,14 +73,22 @@ func LoadYamlConfig(filename string, env string) []string { } } - if app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout == 0 { + if app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout <= 0 { app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout = 30 } - if app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize == 0 { + if app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize <= 0 { app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize = 262144 // 256K } + if app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod <= 0 { + app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod = 345600 // 4 days + } + + if app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds <= 0 { + app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds = 0 + } + if app.CurrentEnvironment.AccountID == "" { app.CurrentEnvironment.AccountID = "queue" } @@ -113,16 +121,21 @@ func LoadYamlConfig(filename string, env string) []string { queue.VisibilityTimeout = app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout } + if queue.MessageRetentionPeriod == 0 { + queue.MessageRetentionPeriod = app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod + } + app.SyncQueues.Queues[queue.Name] = &app.Queue{ - Name: queue.Name, - TimeoutSecs: queue.VisibilityTimeout, - Arn: queueArn, - URL: queueUrl, - ReceiveWaitTimeSecs: queue.ReceiveMessageWaitTimeSeconds, - MaximumMessageSize: queue.MaximumMessageSize, - IsFIFO: app.HasFIFOQueueName(queue.Name), - EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, - Duplicates: make(map[string]time.Time), + Name: queue.Name, + VisibilityTimeout: queue.VisibilityTimeout, + Arn: queueArn, + URL: queueUrl, + ReceiveMessageWaitTimeSeconds: queue.ReceiveMessageWaitTimeSeconds, + MaximumMessageSize: queue.MaximumMessageSize, + MessageRetentionPeriod: queue.MessageRetentionPeriod, + IsFIFO: app.HasFIFOQueueName(queue.Name), + EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, + Duplicates: make(map[string]time.Time), } } @@ -192,15 +205,15 @@ func createSqsSubscription(configSubscription app.EnvSubsciption, topicArn strin } queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + configSubscription.QueueName app.SyncQueues.Queues[configSubscription.QueueName] = &app.Queue{ - Name: configSubscription.QueueName, - TimeoutSecs: app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout, - Arn: queueArn, - URL: queueUrl, - ReceiveWaitTimeSecs: app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds, - MaximumMessageSize: app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize, - IsFIFO: app.HasFIFOQueueName(configSubscription.QueueName), - EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, - Duplicates: make(map[string]time.Time), + Name: configSubscription.QueueName, + VisibilityTimeout: app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout, + Arn: queueArn, + URL: queueUrl, + ReceiveMessageWaitTimeSeconds: app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds, + MaximumMessageSize: app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize, + IsFIFO: app.HasFIFOQueueName(configSubscription.QueueName), + EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, + Duplicates: make(map[string]time.Time), } } qArn := app.SyncQueues.Queues[configSubscription.QueueName].Arn diff --git a/app/conf/config_test.go b/app/conf/config_test.go index f4aefe7c..7923f997 100644 --- a/app/conf/config_test.go +++ b/app/conf/config_test.go @@ -61,71 +61,62 @@ func TestConfig_CreateQueuesTopicsAndSubscriptions(t *testing.T) { } func TestConfig_QueueAttributes(t *testing.T) { + var emptyQueue *app.Queue env := "Local" port := LoadYamlConfig("./mock-data/mock-config.yaml", env) if port[0] != "4100" { t.Errorf("Expected port number 4100 but got %s\n", port) } - receiveWaitTime := app.SyncQueues.Queues["local-queue1"].ReceiveWaitTimeSecs - if receiveWaitTime != 10 { - t.Errorf("Expected local-queue1 Queue to be configured with ReceiveMessageWaitTimeSeconds: 10 but got %d\n", receiveWaitTime) - } - timeoutSecs := app.SyncQueues.Queues["local-queue1"].TimeoutSecs - if timeoutSecs != 10 { - t.Errorf("Expected local-queue1 Queue to be configured with VisibilityTimeout: 10 but got %d\n", timeoutSecs) - } - maximumMessageSize := app.SyncQueues.Queues["local-queue1"].MaximumMessageSize - if maximumMessageSize != 1024 { - t.Errorf("Expected local-queue1 Queue to be configured with MaximumMessageSize: 1024 but got %d\n", maximumMessageSize) - } - - if app.SyncQueues.Queues["local-queue1"].DeadLetterQueue != nil { - t.Errorf("Expected local-queue1 Queue to be configured without redrive policy\n") - } - if app.SyncQueues.Queues["local-queue1"].MaxReceiveCount != 0 { - t.Errorf("Expected local-queue1 Queue to be configured without redrive policy and therefore MaxReceiveCount: 0 \n") - } + assert.Equal(t, 10, app.SyncQueues.Queues["local-queue1"].ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 10, app.SyncQueues.Queues["local-queue1"].VisibilityTimeout) + assert.Equal(t, 1024, app.SyncQueues.Queues["local-queue1"].MaximumMessageSize) + assert.Equal(t, emptyQueue, app.SyncQueues.Queues["local-queue1"].DeadLetterQueue) + assert.Equal(t, 0, app.SyncQueues.Queues["local-queue1"].MaxReceiveCount) + assert.Equal(t, 445600, app.SyncQueues.Queues["local-queue1"].MessageRetentionPeriod) + assert.Equal(t, 100, app.SyncQueues.Queues["local-queue3"].MaxReceiveCount) - maxReceiveCount := app.SyncQueues.Queues["local-queue3"].MaxReceiveCount - if maxReceiveCount != 100 { - t.Errorf("Expected local-queue2 Queue to be configured with MaxReceiveCount: 3 from RedrivePolicy but got %d\n", maxReceiveCount) - } - dlq := app.SyncQueues.Queues["local-queue3"].DeadLetterQueue - if dlq == nil { - t.Errorf("Expected local-queue3 to have one dead letter queue to redrive to\n") - } - if dlq.Name != "local-queue3-dlq" { - t.Errorf("Expected local-queue3 to have dead letter queue local-queue3-dlq but got %s\n", dlq.Name) - } - maximumMessageSize = app.SyncQueues.Queues["local-queue2"].MaximumMessageSize - if maximumMessageSize != 128 { - t.Errorf("Expected local-queue2 Queue to be configured with MaximumMessageSize: 128 but got %d\n", maximumMessageSize) - } - - timeoutSecs = app.SyncQueues.Queues["local-queue2"].TimeoutSecs - if timeoutSecs != 150 { - t.Errorf("Expected local-queue2 Queue to be configured with VisibilityTimeout: 150 but got %d\n", timeoutSecs) - } + assert.Equal(t, "local-queue3-dlq", app.SyncQueues.Queues["local-queue3"].DeadLetterQueue.Name) + assert.Equal(t, 128, app.SyncQueues.Queues["local-queue2"].MaximumMessageSize) + assert.Equal(t, 150, app.SyncQueues.Queues["local-queue2"].VisibilityTimeout) + assert.Equal(t, 245600, app.SyncQueues.Queues["local-queue2"].MessageRetentionPeriod) } func TestConfig_NoQueueAttributeDefaults(t *testing.T) { env := "NoQueueAttributeDefaults" LoadYamlConfig("./mock-data/mock-config.yaml", env) - receiveWaitTime := app.SyncQueues.Queues["local-queue1"].ReceiveWaitTimeSecs + receiveWaitTime := app.SyncQueues.Queues["local-queue1"].ReceiveMessageWaitTimeSeconds if receiveWaitTime != 0 { t.Errorf("Expected local-queue1 Queue to be configured with ReceiveMessageWaitTimeSeconds: 0 but got %d\n", receiveWaitTime) } - timeoutSecs := app.SyncQueues.Queues["local-queue1"].TimeoutSecs + timeoutSecs := app.SyncQueues.Queues["local-queue1"].VisibilityTimeout if timeoutSecs != 30 { t.Errorf("Expected local-queue1 Queue to be configured with VisibilityTimeout: 30 but got %d\n", timeoutSecs) } - receiveWaitTime = app.SyncQueues.Queues["local-queue2"].ReceiveWaitTimeSecs + receiveWaitTime = app.SyncQueues.Queues["local-queue2"].ReceiveMessageWaitTimeSeconds if receiveWaitTime != 20 { t.Errorf("Expected local-queue2 Queue to be configured with ReceiveMessageWaitTimeSeconds: 20 but got %d\n", receiveWaitTime) } + + messageRetentionPeriod := app.SyncQueues.Queues["local-queue1"].MessageRetentionPeriod + if messageRetentionPeriod != 345600 { + t.Errorf("Expected local-queue2 Queue to be configured with VisibilityTimeout: 150 but got %d\n", timeoutSecs) + } +} + +func TestConfig_invalid_config_resorts_to_default_queue_attributes(t *testing.T) { + env := "missing" + port := LoadYamlConfig("./mock-data/mock-config.yaml", env) + if port[0] != "4100" { + t.Errorf("Expected port number 4100 but got %s\n", port) + } + + assert.Equal(t, 262144, app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize) + assert.Equal(t, 345600, app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod) + assert.Equal(t, 0, app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 30, app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout) } func TestConfig_LoadYamlConfig_finds_default_config(t *testing.T) { diff --git a/app/conf/mock-data/mock-config.yaml b/app/conf/mock-data/mock-config.yaml index 8f5dfe5e..702e9607 100644 --- a/app/conf/mock-data/mock-config.yaml +++ b/app/conf/mock-data/mock-config.yaml @@ -10,12 +10,14 @@ Local: # Environment name that can be passed on the VisibilityTimeout: 10 # message visibility timeout ReceiveMessageWaitTimeSeconds: 10 # receive message max wait time MaximumMessageSize: 1024 # maximum message size (bytes) + MessageRetentionPeriod: 445600 # time period to retain messages (seconds) NOTE: Functionality not implemented Queues: # List of queues to create at startup - Name: local-queue1 # Queue name - Name: local-queue2 # Queue name ReceiveMessageWaitTimeSeconds: 20 # Queue receive message max wait time MaximumMessageSize: 128 # Queue maximum message size (bytes) VisibilityTimeout: 150 # Queue visibility timeout + MessageRetentionPeriod: 245600 - Name: local-queue3 # Queue name RedrivePolicy: '{"maxReceiveCount": 100, "deadLetterTargetArn":"arn:aws:sqs:us-east-1:100010001000:local-queue3-dlq"}' - Name: local-queue3-dlq # Queue name diff --git a/app/fixtures/environment.go b/app/fixtures/environment.go new file mode 100644 index 00000000..154d74ba --- /dev/null +++ b/app/fixtures/environment.go @@ -0,0 +1,127 @@ +package fixtures + +import "github.com/Admiral-Piett/goaws/app" + +var ENV_SUBSCRIPTION_QUEUE_4 = app.EnvSubsciption{ + Protocol: "", + EndPoint: "", + TopicArn: "", + QueueName: "local-queue4", + Raw: false, + FilterPolicy: "", +} + +var ENV_SUBSCRIPTION_QUEUE_5 = app.EnvSubsciption{ + Protocol: "", + EndPoint: "", + TopicArn: "", + QueueName: "local-queue5", + Raw: true, + FilterPolicy: "{\"foo\":[\"bar\"]}", +} + +var LOCAL_ENV_TOPIC_1 = app.EnvTopic{ + Name: "local-topic1", + Subscriptions: []app.EnvSubsciption{ + ENV_SUBSCRIPTION_QUEUE_4, + ENV_SUBSCRIPTION_QUEUE_5, + }, +} + +var LOCAL_ENV_TOPIC_2 = app.EnvTopic{ + Name: "local-topic2", + Subscriptions: []app.EnvSubsciption(nil), +} + +var LOCAL_ENV_QUEUE_1 = app.EnvQueue{ + Name: "local-queue1", + ReceiveMessageWaitTimeSeconds: 0, + RedrivePolicy: "", + MaximumMessageSize: 0, +} + +var LOCAL_ENV_QUEUE_2 = app.EnvQueue{ + Name: "local-queue2", + ReceiveMessageWaitTimeSeconds: 20, + RedrivePolicy: "", + MaximumMessageSize: 128, +} + +var LOCAL_ENV_QUEUE_3 = app.EnvQueue{ + Name: "local-queue3", + ReceiveMessageWaitTimeSeconds: 0, + RedrivePolicy: "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:local-queue3-dlq\"}", + MaximumMessageSize: 0, +} + +var LOCAL_ENV_QUEUE_3_DLQ = app.EnvQueue{ + Name: "local-queue3-dlq", + ReceiveMessageWaitTimeSeconds: 0, + RedrivePolicy: "", + MaximumMessageSize: 0, +} + +var DEFAULT_ENVIRONMENT = app.Environment{ + Host: "localhost", + Port: "4100", + Region: "local", + AccountID: "queue", + QueueAttributeDefaults: app.EnvQueueAttributes{ + VisibilityTimeout: 30, + ReceiveMessageWaitTimeSeconds: 0, + MaximumMessageSize: 262144, + }, + RandomLatency: app.RandomLatency{ + Min: 0, + Max: 0, + }, +} + +var NO_QUEUES_NO_TOPICS_ENVIRONEMENT = app.Environment{ + Host: "localhost", + Port: "4100", + Region: "eu-west-1", + LogFile: "./goaws_messages.log", + AccountID: "queue", + QueueAttributeDefaults: app.EnvQueueAttributes{ + VisibilityTimeout: 30, + ReceiveMessageWaitTimeSeconds: 0, + MaximumMessageSize: 262144, + }, + RandomLatency: app.RandomLatency{ + Min: 0, + Max: 0, + }, +} + +var LOCAL_ENVIRONMENT = app.Environment{ + Host: "localhost", + Port: "4200", + SqsPort: "", + SnsPort: "", + Region: "us-east-1", + AccountID: "100010001000", + LogToFile: false, + LogFile: "./goaws_messages.log", + EnableDuplicates: false, + Topics: []app.EnvTopic{ + LOCAL_ENV_TOPIC_1, + LOCAL_ENV_TOPIC_2, + }, + Queues: []app.EnvQueue{ + LOCAL_ENV_QUEUE_1, + LOCAL_ENV_QUEUE_2, + LOCAL_ENV_QUEUE_3, + LOCAL_ENV_QUEUE_3_DLQ, + }, + QueueAttributeDefaults: app.EnvQueueAttributes{ + VisibilityTimeout: 10, + ReceiveMessageWaitTimeSeconds: 11, + MaximumMessageSize: 1024, + MessageRetentionPeriod: 1000, + }, + RandomLatency: app.RandomLatency{ + Min: 0, + Max: 0, + }, +} diff --git a/app/fixtures/fixtures.go b/app/fixtures/fixtures.go new file mode 100644 index 00000000..c88ce736 --- /dev/null +++ b/app/fixtures/fixtures.go @@ -0,0 +1,7 @@ +package fixtures + +var BASE_URL = "http://region.host:port/accountID" +var BASE_ARN = "arn:aws:sqs:region:accountID" + +var XMLNS = "http://queue.amazonaws.com/doc/2012-11-05/" +var REQUEST_ID = "request-id" diff --git a/app/fixtures/request.go b/app/fixtures/request.go new file mode 100644 index 00000000..c83d4744 --- /dev/null +++ b/app/fixtures/request.go @@ -0,0 +1,7 @@ +package fixtures + +var JSONRequestBody = struct { + RequestField string `json:"field"` +}{ + RequestField: "mock-value", +} diff --git a/app/fixtures/sqs.go b/app/fixtures/sqs.go new file mode 100644 index 00000000..afd5c479 --- /dev/null +++ b/app/fixtures/sqs.go @@ -0,0 +1,44 @@ +package fixtures + +import ( + "fmt" + + "github.com/Admiral-Piett/goaws/app" + + "github.com/Admiral-Piett/goaws/app/models" +) + +var QueueName = "new-queue-1" +var DeadLetterQueueName = "dead-letter-queue-1" + +var CreateQueueRequest = models.CreateQueueRequest{ + QueueName: QueueName, + Attributes: CreateQueueAttributes, + Tags: map[string]string{"my": "tag"}, +} + +var CreateQueueAttributes = models.Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + Policy: map[string]interface{}{"this-is": "the-policy"}, //IAM Policy + ReceiveMessageWaitTimeSeconds: 4, + VisibilityTimeout: 5, + //RedrivePolicy: models.RedrivePolicy{ + // MaxReceiveCount: 100, + // DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", DeadLetterQueueName), + //}, + RedriveAllowPolicy: map[string]interface{}{"this-is": "the-redrive-allow-policy"}, +} + +var CreateQueueResult = models.CreateQueueResult{ + QueueUrl: fmt.Sprintf("http://us-east-1.localhost:4200/100010001000/%s", QueueName), +} + +var CreateQueueResponse = models.CreateQueueResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: CreateQueueResult, + Metadata: app.ResponseMetadata{ + RequestId: "00000000-0000-0000-0000-000000000000", + }, +} diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 76a836f7..52b8e8bc 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/Admiral-Piett/goaws/app/models" + "bytes" "crypto" "crypto/rand" @@ -769,8 +771,8 @@ func extractMessageFromJSON(msg string, protocol string) (string, error) { func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { er := app.SnsErrors[err] - respStruct := app.ErrorResponse{ - Result: app.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, + respStruct := models.ErrorResponse{ + Result: models.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, RequestId: "00000000-0000-0000-0000-000000000000", } diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index eb6c6609..4308d89f 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -127,11 +127,11 @@ func TestPublishHandler_POST_FilterPolicyRejectsTheMessage(t *testing.T) { queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/queue/" + queueName queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":000000000000:" + queueName app.SyncQueues.Queues[queueName] = &app.Queue{ - Name: queueName, - TimeoutSecs: 30, - Arn: queueArn, - URL: queueUrl, - IsFIFO: app.HasFIFOQueueName(queueName), + Name: queueName, + VisibilityTimeout: 30, + Arn: queueArn, + URL: queueUrl, + IsFIFO: app.HasFIFOQueueName(queueName), } // We set up a topic with the corresponding Subscription including FilterPolicy @@ -198,11 +198,11 @@ func TestPublishHandler_POST_FilterPolicyPassesTheMessage(t *testing.T) { queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/queue/" + queueName queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":000000000000:" + queueName app.SyncQueues.Queues[queueName] = &app.Queue{ - Name: queueName, - TimeoutSecs: 30, - Arn: queueArn, - URL: queueUrl, - IsFIFO: app.HasFIFOQueueName(queueName), + Name: queueName, + VisibilityTimeout: 30, + Arn: queueArn, + URL: queueUrl, + IsFIFO: app.HasFIFOQueueName(queueName), } // We set up a topic with the corresponding Subscription including FilterPolicy diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 91f1bf82..018e9286 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -1,7 +1,6 @@ package gosqs import ( - "encoding/json" "encoding/xml" "fmt" "net/http" @@ -10,6 +9,12 @@ import ( "strings" "time" + "github.com/Admiral-Piett/goaws/app/interfaces" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/Admiral-Piett/goaws/app/utils" + log "github.com/sirupsen/logrus" "github.com/Admiral-Piett/goaws/app" @@ -115,41 +120,13 @@ func ListQueues(w http.ResponseWriter, req *http.Request) { } } -type CreateQueueRequest struct { - QueueName string `json: QueueName` - Attributes map[string]string `json: Attributes` - Tags map[string]string `json: Tags` -} - -type Attributes map[string]string - -func parseCreateQueueRequestBody(w http.ResponseWriter, req *http.Request) (bool, CreateQueueRequest) { - requestBody := new(CreateQueueRequest) - - // Should remove this flag after validateAndSetQueueAttributes was updated - byJson := false - - switch req.Header.Get("Content-Type") { - case "application/x-amz-json-1.0": - //Read body data to parse json - decoder := json.NewDecoder(req.Body) - err := decoder.Decode(&requestBody) - if err != nil { - panic(err) - } - byJson = true - default: - requestBody.QueueName = req.FormValue("QueueName") - requestBody.Attributes = extractQueueAttributes(req.Form) +func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewCreateQueueRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - CreateQueueV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) } - - return byJson, *requestBody -} - -func CreateQueue(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - byJson, requestBody := parseCreateQueueRequestBody(w, req) - queueName := requestBody.QueueName queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + @@ -163,38 +140,27 @@ func CreateQueue(w http.ResponseWriter, req *http.Request) { if _, ok := app.SyncQueues.Queues[queueName]; !ok { log.Println("Creating Queue:", queueName) queue := &app.Queue{ - Name: queueName, - URL: queueUrl, - Arn: queueArn, - TimeoutSecs: app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout, - ReceiveWaitTimeSecs: app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds, - MaximumMessageSize: app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize, - IsFIFO: app.HasFIFOQueueName(queueName), - EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, - Duplicates: make(map[string]time.Time), + Name: queueName, + URL: queueUrl, + Arn: queueArn, + IsFIFO: app.HasFIFOQueueName(queueName), + EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, + Duplicates: make(map[string]time.Time), } - if byJson { - if err := validateAndSetQueueAttributesJson(queue, requestBody.Attributes); err != nil { - createErrorResponse(w, req, err.Error()) - return - } - } else { - if err := validateAndSetQueueAttributes(queue, req.Form); err != nil { - createErrorResponse(w, req, err.Error()) - return - } + if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { + return createErrorResponseV1(err.Error()) } app.SyncQueues.Lock() app.SyncQueues.Queues[queueName] = queue app.SyncQueues.Unlock() } - respStruct := app.CreateQueueResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.CreateQueueResult{QueueUrl: queueUrl}, app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) + respStruct := models.CreateQueueResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: models.CreateQueueResult{QueueUrl: queueUrl}, + Metadata: app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}, } + return http.StatusOK, respStruct } func SendMessage(w http.ResponseWriter, req *http.Request) { @@ -229,7 +195,7 @@ func SendMessage(w http.ResponseWriter, req *http.Request) { return } - delaySecs := app.SyncQueues.Queues[queueName].DelaySecs + delaySecs := app.SyncQueues.Queues[queueName].DelaySeconds if mv := req.FormValue("DelaySeconds"); mv != "" { delaySecs, _ = strconv.Atoi(mv) } @@ -455,7 +421,7 @@ func ReceiveMessage(w http.ResponseWriter, req *http.Request) { if waitTimeSeconds == 0 { app.SyncQueues.RLock() - waitTimeSeconds = app.SyncQueues.Queues[queueName].ReceiveWaitTimeSecs + waitTimeSeconds = app.SyncQueues.Queues[queueName].ReceiveMessageWaitTimeSeconds app.SyncQueues.RUnlock() } @@ -508,7 +474,7 @@ func ReceiveMessage(w http.ResponseWriter, req *http.Request) { } msg.ReceiptHandle = msg.Uuid + "#" + uuid msg.ReceiptTime = time.Now().UTC() - msg.VisibilityTimeout = time.Now().Add(time.Duration(app.SyncQueues.Queues[queueName].TimeoutSecs) * time.Second) + msg.VisibilityTimeout = time.Now().Add(time.Duration(app.SyncQueues.Queues[queueName].VisibilityTimeout) * time.Second) if app.SyncQueues.Queues[queueName].IsFIFO { // If we got messages here it means we have not processed it yet, so get next @@ -590,7 +556,7 @@ func ChangeMessageVisibility(w http.ResponseWriter, req *http.Request) { queue := app.SyncQueues.Queues[queueName] msgs := queue.Messages if msgs[i].ReceiptHandle == receiptHandle { - timeout := app.SyncQueues.Queues[queueName].TimeoutSecs + timeout := app.SyncQueues.Queues[queueName].VisibilityTimeout if visibilityTimeout == 0 { msgs[i].ReceiptTime = time.Now().UTC() msgs[i].ReceiptHandle = "" @@ -899,15 +865,15 @@ func GetQueueAttributes(w http.ResponseWriter, req *http.Request) { // Create, encode/xml and send response attribs := make([]app.Attribute, 0, 0) if include_attr("VisibilityTimeout") { - attr := app.Attribute{Name: "VisibilityTimeout", Value: strconv.Itoa(queue.TimeoutSecs)} + attr := app.Attribute{Name: "VisibilityTimeout", Value: strconv.Itoa(queue.VisibilityTimeout)} attribs = append(attribs, attr) } if include_attr("DelaySeconds") { - attr := app.Attribute{Name: "DelaySeconds", Value: strconv.Itoa(queue.DelaySecs)} + attr := app.Attribute{Name: "DelaySeconds", Value: strconv.Itoa(queue.DelaySeconds)} attribs = append(attribs, attr) } if include_attr("ReceiveMessageWaitTimeSeconds") { - attr := app.Attribute{Name: "ReceiveMessageWaitTimeSeconds", Value: strconv.Itoa(queue.ReceiveWaitTimeSecs)} + attr := app.Attribute{Name: "ReceiveMessageWaitTimeSeconds", Value: strconv.Itoa(queue.ReceiveMessageWaitTimeSeconds)} attribs = append(attribs, attr) } if include_attr("ApproximateNumberOfMessages") { @@ -931,6 +897,7 @@ func GetQueueAttributes(w http.ResponseWriter, req *http.Request) { attribs = append(attribs, attr) } + // TODO - why do we just return the name and NOT the actual ARN here? deadLetterTargetArn := "" if queue.DeadLetterQueue != nil { deadLetterTargetArn = queue.DeadLetterQueue.Name @@ -972,7 +939,7 @@ func SetQueueAttributes(w http.ResponseWriter, req *http.Request) { log.Println("Set Queue Attributes:", queueName) app.SyncQueues.Lock() if queue, ok := app.SyncQueues.Queues[queueName]; ok { - if err := validateAndSetQueueAttributes(queue, req.Form); err != nil { + if err := validateAndSetQueueAttributesFromForm(queue, req.Form); err != nil { createErrorResponse(w, req, err.Error()) app.SyncQueues.Unlock() return @@ -1036,8 +1003,8 @@ func getQueueFromPath(formVal string, theUrl string) string { func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { er := app.SqsErrors[err] - respStruct := app.ErrorResponse{ - Result: app.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, + respStruct := models.ErrorResponse{ + Result: models.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, RequestId: "00000000-0000-0000-0000-000000000000", } @@ -1048,3 +1015,12 @@ func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { log.Printf("error: %v\n", err) } } + +func createErrorResponseV1(err string) (int, interfaces.AbstractResponseBody) { + er := app.SqsErrors[err] + respStruct := models.ErrorResponse{ + Result: models.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, + RequestId: "00000000-0000-0000-0000-000000000000", // TODO - fix + } + return er.HttpError, respStruct +} diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 4e9ec334..3511cb64 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -1,22 +1,36 @@ package gosqs import ( - "bytes" "context" - "encoding/json" "encoding/xml" + "fmt" "net/http" "net/http/httptest" "net/url" - "reflect" "strings" "sync" "testing" "time" + "github.com/mitchellh/copystructure" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/Admiral-Piett/goaws/app/interfaces" + + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" + + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app" ) +func TestMain(m *testing.M) { + utils.InitializeDecoders() + m.Run() +} + func TestListQueues_POST_NoQueues(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. @@ -95,155 +109,263 @@ func TestListQueues_POST_Success(t *testing.T) { } } -func TestCreateQueuehandler_POST_CreateQueue(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) +func TestCreateQueueV1_success(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.CreateQueueRequest) + *v = fixtures.CreateQueueRequest + return true } - queueName := "UnitTestQueue1" - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "UnitTestQueue1") - form.Add("Attribute.1.Name", "VisibilityTimeout") - form.Add("Attribute.1.Value", "60") - form.Add("Attribute.2.Name", "MaximumMessageSize") - form.Add("Attribute.2.Value", "2048") - req.PostForm = form + expectedQueue := &app.Queue{ + Name: fixtures.QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + VisibilityTimeout: 5, + ReceiveMessageWaitTimeSeconds: 4, + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + Duplicates: make(map[string]time.Time), + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, expectedQueue, actualQueue) +} - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(CreateQueue) +func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() - // Our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. - handler.ServeHTTP(rr, req) + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes.RedrivePolicy = models.RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", fixtures.DeadLetterQueueName), + } - // 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) + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true } - // Check the response body is what we expect. - expected := queueName - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) + dlq := &app.Queue{ + Name: fixtures.DeadLetterQueueName, } + app.SyncQueues.Queues[fixtures.DeadLetterQueueName] = dlq + expectedQueue := &app.Queue{ - Name: queueName, - URL: "http://://" + queueName, - Arn: "arn:aws:sqs:::" + queueName, - TimeoutSecs: 60, - MaximumMessageSize: 2048, - Duplicates: make(map[string]time.Time), - } - actualQueue := app.SyncQueues.Queues[queueName] - if !reflect.DeepEqual(expectedQueue, actualQueue) { - t.Fatalf("expected %+v, got %+v", expectedQueue, actualQueue) - } + Name: fixtures.QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + VisibilityTimeout: 5, + ReceiveMessageWaitTimeSeconds: 4, + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + DeadLetterQueue: dlq, + MaxReceiveCount: 100, + Duplicates: make(map[string]time.Time), + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, expectedQueue, actualQueue) } -func TestCreateQueuehandler_POST_CreateQueue_aws_json(t *testing.T) { - queueName := "UnitTestQueue1" - requestObj := new(CreateQueueRequest) - requestObj.QueueName = queueName - requestObj.Attributes = map[string]string{} - requestObj.Attributes["VisibilityTimeout"] = "60" - requestObj.Attributes["MaximumMessageSize"] = "2048" - requestJson, _ := json.Marshal(requestObj) - // 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", "/", bytes.NewBuffer(requestJson)) - if err != nil { - t.Fatal(err) +func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.CreateQueueRequest) + *v = fixtures.CreateQueueRequest + return true } - req.Header.Set("X-Amz-Target", "AmazonSQS.CreateQueue") - req.Header.Set("Content-Type", "application/x-amz-json-1.0") - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(CreateQueue) + q := &app.Queue{ + Name: fixtures.QueueName, + } + app.SyncQueues.Queues[fixtures.QueueName] = q - // Our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. - handler.ServeHTTP(rr, req) + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) - // 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) - } + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) - // Check the response body is what we expect. - expected := queueName - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, q, actualQueue) +} + +func TestCreateQueueV1_success_with_no_request_attributes_falls_back_to_default(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes = models.Attributes{} + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true } + expectedQueue := &app.Queue{ - Name: queueName, - URL: "http://://" + queueName, - Arn: "arn:aws:sqs:::" + queueName, - TimeoutSecs: 60, - MaximumMessageSize: 2048, - Duplicates: make(map[string]time.Time), - } - actualQueue := app.SyncQueues.Queues[queueName] - if !reflect.DeepEqual(expectedQueue, actualQueue) { - t.Fatalf("expected %+v, got %+v", expectedQueue, actualQueue) - } + Name: fixtures.QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + VisibilityTimeout: 0, + ReceiveMessageWaitTimeSeconds: 0, + DelaySeconds: 0, + MaximumMessageSize: 0, + MessageRetentionPeriod: 0, + Duplicates: make(map[string]time.Time), + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, expectedQueue, actualQueue) } -func TestCreateFIFOQueuehandler_POST_CreateQueue(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) +func TestCreateQueueV1_success_no_configured_region_for_queue_url(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + app.CurrentEnvironment.Region = "" + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes = models.Attributes{} + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true } - queueName := "UnitTestQueue1.fifo" - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "UnitTestQueue1.fifo") - form.Add("Attribute.1.Name", "VisibilityTimeout") - form.Add("Attribute.1.Value", "60") - req.PostForm = form + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := CreateQueueV1(r) - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(CreateQueue) + assert.Equal(t, http.StatusOK, code) - // Our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. - handler.ServeHTTP(rr, req) + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, + fmt.Sprintf("http://%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + actualQueue.URL, + ) +} - // 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) - } +func TestCreateQueueV1_request_transformer_error(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() - // Check the response body is what we expect. - expected := queueName - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } - expectedQueue := &app.Queue{ - Name: queueName, - URL: "http://://" + queueName, - Arn: "arn:aws:sqs:::" + queueName, - TimeoutSecs: 60, - IsFIFO: true, - Duplicates: make(map[string]time.Time), + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + return false } - actualQueue := app.SyncQueues.Queues[queueName] - if !reflect.DeepEqual(expectedQueue, actualQueue) { - t.Fatalf("expected %+v, got %+v", expectedQueue, actualQueue) + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := CreateQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestCreateQueueV1_invalid_dead_letter_queue_error(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes.RedrivePolicy = models.RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", "garbage"), + } + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := CreateQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) } func TestSendMessage_MaximumMessageSize_Success(t *testing.T) { @@ -692,21 +814,24 @@ func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { t.Fatal(err) } - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "requeue") - form.Add("Attribute.1.Name", "VisibilityTimeout") - form.Add("Attribute.1.Value", "1") - form.Add("Version", "2012-11-05") - req.PostForm = form + //form := url.Values{} + //form.Add("Action", "CreateQueue") + //form.Add("QueueName", "requeue") + //form.Add("Attribute.1.Name", "VisibilityTimeout") + //form.Add("Attribute.1.Value", "1") + //form.Add("Version", "2012-11-05") + req.PostForm = url.Values{ + "Action": []string{"CreateQueue"}, + "QueueName": []string{"requeue"}, + "Attribute.1.Name": []string{"VisibilityTimeout"}, + "Attribute.1.Value": []string{"1"}, + "Version": []string{"2012-11-05"}, + } rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, http.StatusOK, status) // send a message req, err = http.NewRequest("POST", "/", nil) @@ -714,7 +839,7 @@ func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { t.Fatal(err) } - form = url.Values{} + form := url.Values{} form.Add("Action", "SendMessage") form.Add("QueueUrl", "http://localhost:4100/queue/requeue") form.Add("MessageBody", "1") @@ -820,12 +945,9 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // send a message req, err = http.NewRequest("POST", "/", nil) @@ -976,12 +1098,9 @@ func TestDeadLetterQueue(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // send a message req, err = http.NewRequest("POST", "/", nil) @@ -1087,12 +1206,9 @@ func TestReceiveMessageWaitTimeEnforced(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // receive message ensure delay req, err = http.NewRequest("POST", "/", nil) @@ -1189,7 +1305,9 @@ func TestReceiveMessage_CanceledByClient(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) + + assert.Equal(t, status, http.StatusOK) var wg sync.WaitGroup ctx, cancelReceive := context.WithCancel(context.Background()) @@ -1296,7 +1414,9 @@ func TestReceiveMessage_WithConcurrentDeleteQueue(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) + + assert.Equal(t, status, http.StatusOK) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", @@ -1382,12 +1502,9 @@ func TestReceiveMessageDelaySeconds(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // send a message req, err = http.NewRequest("POST", "/", nil) @@ -1510,12 +1627,9 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // send a message req, err = http.NewRequest("POST", "/", nil) @@ -1705,12 +1819,9 @@ func TestSendMessage_POST_DuplicatationNotAppliedToStandardQueue(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) req, err = http.NewRequest("POST", "/", nil) if err != nil { @@ -1775,12 +1886,9 @@ func TestSendMessage_POST_DuplicatationDisabledOnFifoQueue(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) req, err = http.NewRequest("POST", "/", nil) if err != nil { @@ -1845,12 +1953,9 @@ func TestSendMessage_POST_DuplicatationEnabledOnFifoQueue(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) req, err = http.NewRequest("POST", "/", nil) if err != nil { @@ -1916,12 +2021,9 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // send a message req, err = http.NewRequest("POST", "/", nil) @@ -2009,12 +2111,9 @@ func TestGetQueueAttributes_GetAllAttributes(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // get queue attributes req, err = http.NewRequest("GET", "/queue/get-queue-attributes?Action=GetQueueAttributes&AttributeName.1=All", nil) @@ -2079,12 +2178,9 @@ func TestGetQueueAttributes_GetSelectedAttributes(t *testing.T) { req.PostForm = form rr := httptest.NewRecorder() - http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) + status, _ := CreateQueueV1(req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) // get queue attributes req, err = http.NewRequest("GET", "/queue/get-queue-attributes?Action=GetQueueAttributes&AttributeName.1=ApproximateNumberOfMessages&AttributeName.2=ApproximateNumberOfMessagesNotVisible&AttributeName.2=ApproximateNumberOfMessagesNotVisible", nil) @@ -2137,6 +2233,21 @@ func TestGetQueueAttributes_GetSelectedAttributes(t *testing.T) { done <- struct{}{} } +func TestCreateErrorResponseV1(t *testing.T) { + expectedResponse := models.ErrorResponse{ + Result: models.ErrorResult{ + Type: "Not Found", + Code: "AWS.SimpleQueueService.NonExistentQueue", + Message: "The specified queue does not exist for this wsdl version.", + }, + RequestId: "00000000-0000-0000-0000-000000000000", + } + status, response := createErrorResponseV1("QueueNotFound") + + assert.Equal(t, http.StatusBadRequest, status) + assert.Equal(t, expectedResponse, response) +} + // waitTimeout waits for the waitgroup for the specified max timeout. // Returns true if waiting timed out. // credits: https://stackoverflow.com/questions/32840687/timeout-for-waitgroup-wait diff --git a/app/gosqs/queue_attributes.go b/app/gosqs/queue_attributes.go index 5fce3b1a..2d608abc 100644 --- a/app/gosqs/queue_attributes.go +++ b/app/gosqs/queue_attributes.go @@ -8,6 +8,12 @@ import ( "strconv" "strings" + log "github.com/sirupsen/logrus" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/Admiral-Piett/goaws/app" ) @@ -26,23 +32,23 @@ var ( } ) -// validateAndSetQueueAttributes applies the requested queue attributes to the given +// validateAndSetQueueAttributesFromForm applies the requested queue attributes to the given // queue. -// TODO Currently it only supports VisibilityTimeout, MaximumMessageSize, DelaySeconds, RedrivePolicy and ReceiveMessageWaitTimeSeconds attributes. -func validateAndSetQueueAttributes(q *app.Queue, u url.Values) error { - attr := extractQueueAttributes(u) +// TODO Currently it only supports VisibilityTimeout, MaximumMessageSize, DelaySeconds, RedrivePolicy and ReceiveMessageWaitTimeSeconds attributes. +func validateAndSetQueueAttributesFromForm(q *app.Queue, u url.Values) error { + attr := utils.ExtractQueueAttributes(u) - return validateAndSetQueueAttributesJson(q, attr) + return validateAndSetQueueAttributes(q, attr) } -func validateAndSetQueueAttributesJson(q *app.Queue, attr Attributes) error { +func validateAndSetQueueAttributes(q *app.Queue, attr map[string]string) error { visibilityTimeout, _ := strconv.Atoi(attr["VisibilityTimeout"]) if visibilityTimeout != 0 { - q.TimeoutSecs = visibilityTimeout + q.VisibilityTimeout = visibilityTimeout } receiveWaitTime, _ := strconv.Atoi(attr["ReceiveMessageWaitTimeSeconds"]) if receiveWaitTime != 0 { - q.ReceiveWaitTimeSecs = receiveWaitTime + q.ReceiveMessageWaitTimeSeconds = receiveWaitTime } maximumMessageSize, _ := strconv.Atoi(attr["MaximumMessageSize"]) if maximumMessageSize != 0 { @@ -85,26 +91,45 @@ func validateAndSetQueueAttributesJson(q *app.Queue, attr Attributes) error { } delaySecs, _ := strconv.Atoi(attr["DelaySeconds"]) if delaySecs != 0 { - q.DelaySecs = delaySecs + q.DelaySeconds = delaySecs } return nil } -func extractQueueAttributes(u url.Values) map[string]string { - attr := map[string]string{} - for i := 1; true; i++ { - nameKey := fmt.Sprintf("Attribute.%d.Name", i) - attrName := u.Get(nameKey) - if attrName == "" { - break - } - - valueKey := fmt.Sprintf("Attribute.%d.Value", i) - attrValue := u.Get(valueKey) - if attrValue != "" { - attr[attrName] = attrValue +// TODO - Support: +// - attr.MessageRetentionPeriod +// - attr.Policy +// - attr.RedriveAllowPolicy +func setQueueAttributesV1(q *app.Queue, attr models.Attributes) error { + // FIXME - are there better places to put these bottom-limit validations? + if attr.DelaySeconds >= 0 { + q.DelaySeconds = attr.DelaySeconds.Int() + } + if attr.MaximumMessageSize >= 0 { + q.MaximumMessageSize = attr.MaximumMessageSize.Int() + } + // TODO - bottom limit should be the AWS limits + // The following 2 don't support zero values + if attr.MessageRetentionPeriod > 0 { + q.MessageRetentionPeriod = attr.MessageRetentionPeriod.Int() + } + if attr.ReceiveMessageWaitTimeSeconds > 0 { + q.ReceiveMessageWaitTimeSeconds = attr.ReceiveMessageWaitTimeSeconds.Int() + } + if attr.VisibilityTimeout >= 0 { + q.VisibilityTimeout = attr.VisibilityTimeout.Int() + } + if attr.RedrivePolicy != (models.RedrivePolicy{}) { + arnArray := strings.Split(attr.RedrivePolicy.DeadLetterTargetArn, ":") + queueName := arnArray[len(arnArray)-1] + deadLetterQueue, ok := app.SyncQueues.Queues[queueName] + if !ok { + log.Error("Invalid RedrivePolicy Attribute") + return fmt.Errorf(ErrInvalidAttributeValue.Type) } + q.DeadLetterQueue = deadLetterQueue + q.MaxReceiveCount = attr.RedrivePolicy.MaxReceiveCount.Int() } - return attr + return nil } diff --git a/app/gosqs/queue_attributes_test.go b/app/gosqs/queue_attributes_test.go index f84f5d09..7401f6b0 100644 --- a/app/gosqs/queue_attributes_test.go +++ b/app/gosqs/queue_attributes_test.go @@ -1,10 +1,17 @@ package gosqs import ( + "fmt" "net/url" "reflect" "testing" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/stretchr/testify/assert" + + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app" ) @@ -14,7 +21,7 @@ func TestApplyQueueAttributes(t *testing.T) { app.SyncQueues.Lock() app.SyncQueues.Queues["failed-messages"] = deadLetterQueue app.SyncQueues.Unlock() - q := &app.Queue{TimeoutSecs: 30} + q := &app.Queue{VisibilityTimeout: 30} u := url.Values{} u.Add("Attribute.1.Name", "DelaySeconds") u.Add("Attribute.1.Value", "25") @@ -25,55 +32,151 @@ func TestApplyQueueAttributes(t *testing.T) { u.Add("Attribute.4.Value", `{"maxReceiveCount": "4", "deadLetterTargetArn":"arn:aws:sqs::000000000000:failed-messages"}`) u.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") u.Add("Attribute.5.Value", "20") - if err := validateAndSetQueueAttributes(q, u); err != nil { + if err := validateAndSetQueueAttributesFromForm(q, u); err != nil { t.Fatalf("expected nil, got %s", err) } expected := &app.Queue{ - TimeoutSecs: 60, - ReceiveWaitTimeSecs: 20, - DelaySecs: 25, - MaxReceiveCount: 4, - DeadLetterQueue: deadLetterQueue, + VisibilityTimeout: 60, + ReceiveMessageWaitTimeSeconds: 20, + DelaySeconds: 25, + MaxReceiveCount: 4, + DeadLetterQueue: deadLetterQueue, } if ok := reflect.DeepEqual(q, expected); !ok { t.Fatalf("expected %+v, got %+v", expected, q) } }) t.Run("missing_deadletter_arn", func(t *testing.T) { - q := &app.Queue{TimeoutSecs: 30} + q := &app.Queue{VisibilityTimeout: 30} u := url.Values{} u.Add("Attribute.1.Name", "RedrivePolicy") u.Add("Attribute.1.Value", `{"maxReceiveCount": "4"}`) - err := validateAndSetQueueAttributes(q, u) + err := validateAndSetQueueAttributesFromForm(q, u) if err != ErrInvalidParameterValue { t.Fatalf("expected %s, got %s", ErrInvalidParameterValue, err) } }) t.Run("invalid_redrive_policy", func(t *testing.T) { - q := &app.Queue{TimeoutSecs: 30} + q := &app.Queue{VisibilityTimeout: 30} u := url.Values{} u.Add("Attribute.1.Name", "RedrivePolicy") u.Add("Attribute.1.Value", `{invalidinput}`) - err := validateAndSetQueueAttributes(q, u) + err := validateAndSetQueueAttributesFromForm(q, u) if err != ErrInvalidAttributeValue { t.Fatalf("expected %s, got %s", ErrInvalidAttributeValue, err) } }) } -func TestExtractQueueAttributes(t *testing.T) { - u := url.Values{} - u.Add("Attribute.1.Name", "DelaySeconds") - u.Add("Attribute.1.Value", "20") - u.Add("Attribute.2.Name", "VisibilityTimeout") - u.Add("Attribute.2.Value", "30") - u.Add("Attribute.3.Name", "Policy") - attr := extractQueueAttributes(u) - expected := map[string]string{ - "DelaySeconds": "20", - "VisibilityTimeout": "30", +func TestSetQueueAttributesV1_success_no_redrive_policy(t *testing.T) { + var emptyQueue *app.Queue + q := &app.Queue{} + attrs := models.Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + ReceiveMessageWaitTimeSeconds: 4, + VisibilityTimeout: 5, } - if ok := reflect.DeepEqual(attr, expected); !ok { - t.Fatalf("expected %+v, got %+v", expected, attr) + err := setQueueAttributesV1(q, attrs) + + assert.Nil(t, err) + assert.Equal(t, 1, q.DelaySeconds) + assert.Equal(t, 2, q.MaximumMessageSize) + assert.Equal(t, 3, q.MessageRetentionPeriod) + assert.Equal(t, 4, q.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 5, q.VisibilityTimeout) + assert.Equal(t, emptyQueue, q.DeadLetterQueue) + assert.Equal(t, 0, q.MaxReceiveCount) +} + +func TestSetQueueAttributesV1_success_no_request_attributes(t *testing.T) { + var emptyQueue *app.Queue + q := &app.Queue{} + attrs := models.Attributes{} + err := setQueueAttributesV1(q, attrs) + + assert.Nil(t, err) + assert.Equal(t, 0, q.DelaySeconds) + assert.Equal(t, 0, q.MaximumMessageSize) + assert.Equal(t, 0, q.MessageRetentionPeriod) + assert.Equal(t, 0, q.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 0, q.VisibilityTimeout) + assert.Equal(t, emptyQueue, q.DeadLetterQueue) + assert.Equal(t, 0, q.MaxReceiveCount) +} + +func TestSetQueueAttributesV1_success_can_set_0_values_where_applicable(t *testing.T) { + var emptyQueue *app.Queue + q := &app.Queue{ + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + ReceiveMessageWaitTimeSeconds: 4, + VisibilityTimeout: 5, } + attrs := models.Attributes{} + err := setQueueAttributesV1(q, attrs) + + assert.Nil(t, err) + assert.Equal(t, 0, q.DelaySeconds) + assert.Equal(t, 0, q.MaximumMessageSize) + assert.Equal(t, 3, q.MessageRetentionPeriod) + assert.Equal(t, 4, q.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 0, q.VisibilityTimeout) + assert.Equal(t, emptyQueue, q.DeadLetterQueue) + assert.Equal(t, 0, q.MaxReceiveCount) +} + +func TestSetQueueAttributesV1_success_with_redrive_policy(t *testing.T) { + defer func() { + utils.ResetApp() + }() + + existingQueueName := "existing-queue" + existingQueue := &app.Queue{Name: existingQueueName} + app.SyncQueues.Queues[existingQueueName] = existingQueue + + q := &app.Queue{} + attrs := models.Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + ReceiveMessageWaitTimeSeconds: 4, + VisibilityTimeout: 5, + RedrivePolicy: models.RedrivePolicy{ + MaxReceiveCount: 10, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:region:account-id:%s", existingQueueName), + }, + } + err := setQueueAttributesV1(q, attrs) + + assert.Nil(t, err) + assert.Equal(t, 1, q.DelaySeconds) + assert.Equal(t, 2, q.MaximumMessageSize) + assert.Equal(t, 3, q.MessageRetentionPeriod) + assert.Equal(t, 4, q.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 5, q.VisibilityTimeout) + assert.Equal(t, existingQueue, q.DeadLetterQueue) + assert.Equal(t, 10, q.MaxReceiveCount) +} + +func TestSetQueueAttributesV1_error_redrive_policy_targets_missing_queue(t *testing.T) { + existingQueueName := "existing-queue" + + q := &app.Queue{} + attrs := models.Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + ReceiveMessageWaitTimeSeconds: 4, + VisibilityTimeout: 5, + RedrivePolicy: models.RedrivePolicy{ + MaxReceiveCount: 10, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:region:account-id:%s", existingQueueName), + }, + } + err := setQueueAttributesV1(q, attrs) + + assert.Error(t, err) } diff --git a/app/interfaces/interfaces.go b/app/interfaces/interfaces.go new file mode 100644 index 00000000..fbe63f53 --- /dev/null +++ b/app/interfaces/interfaces.go @@ -0,0 +1,14 @@ +package interfaces + +import ( + "net/url" +) + +type AbstractRequestBody interface { + SetAttributesFromForm(values url.Values) +} + +type AbstractResponseBody interface { + GetResult() interface{} + GetRequestId() string +} diff --git a/app/mocks/mocks.go b/app/mocks/mocks.go new file mode 100644 index 00000000..6db051c3 --- /dev/null +++ b/app/mocks/mocks.go @@ -0,0 +1,46 @@ +package mocks + +import ( + "net/url" + + af "github.com/Admiral-Piett/goaws/app/fixtures" +) + +type MockRequestBody struct { + RequestFieldStr string `json:"field" schema:"field"` + RequestFieldInt int `json:"intField" schema:"intField"` + + SetAttributesFromFormCalled bool + SetAttributesFromFormCalledWith []interface{} + + MockSetAttributesFromFormCalledWith func(values url.Values) +} + +func (m *MockRequestBody) SetAttributesFromForm(values url.Values) { + m.SetAttributesFromFormCalled = true + m.SetAttributesFromFormCalledWith = append(m.SetAttributesFromFormCalledWith, values) + if m.MockSetAttributesFromFormCalledWith != nil { + m.MockSetAttributesFromFormCalledWith(values) + } +} + +type BaseResponse struct { + Message string `json:"Message" xml:"Message"` + + MockGetResult func() interface{} `json:"-" xml:"-"` + MockGetRequestId func() string `json:"-" xml:"-"` +} + +func (r BaseResponse) GetResult() interface{} { + if r.MockGetResult != nil { + return r.MockGetResult() + } + return r +} + +func (r BaseResponse) GetRequestId() string { + if r.MockGetRequestId != nil { + return r.GetRequestId() + } + return af.REQUEST_ID +} diff --git a/app/models/conversions.go b/app/models/conversions.go new file mode 100644 index 00000000..35c7530f --- /dev/null +++ b/app/models/conversions.go @@ -0,0 +1,36 @@ +package models + +import ( + "encoding/json" + "strconv" +) + +// StringToInt this is a custom type that will allow our request bodies to support either a string OR an int. +// It has its own UnmarshalJSON method to handle both types automatically and it can return an `int` +// from the `Int` method. +type StringToInt int + +func (s *StringToInt) UnmarshalJSON(data []byte) error { + var i int + err := json.Unmarshal(data, &i) + if err == nil { + *s = StringToInt(i) + return nil + } + + var str string + err = json.Unmarshal(data, &str) + if err != nil { + return err + } + tmp, err := strconv.Atoi(str) + if err != nil { + return err + } + *s = StringToInt(tmp) + return nil +} + +func (s *StringToInt) Int() int { + return int(*s) +} diff --git a/app/models/conversions_test.go b/app/models/conversions_test.go new file mode 100644 index 00000000..144c099a --- /dev/null +++ b/app/models/conversions_test.go @@ -0,0 +1,75 @@ +package models + +import ( + "encoding/json" + "testing" + + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +type StringToIntStruct struct { + Field1 StringToInt `json:"Field1"` + Field2 StringToInt `json:"Field2"` +} + +func TestStringToInt_unmarshalJSON_int(t *testing.T) { + body := struct { + Field1 int `json:"Field1"` + Field2 int `json:"Field2"` + }{ + Field1: 1, + Field2: 2, + } + _, r := utils.GenerateRequestInfo("POST", "/", body, true) + + result := &StringToIntStruct{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(result) + + assert.Nil(t, err) + assert.Equal(t, StringToInt(1), result.Field1) + assert.Equal(t, StringToInt(2), result.Field2) +} + +func TestStringToInt_unmarshalJSON_string(t *testing.T) { + body := struct { + Field1 string `json:"Field1"` + Field2 string `json:"Field2"` + }{ + Field1: "1", + Field2: "2", + } + _, r := utils.GenerateRequestInfo("POST", "/", body, true) + + result := &StringToIntStruct{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(result) + + assert.Nil(t, err) + assert.Equal(t, StringToInt(1), result.Field1) + assert.Equal(t, StringToInt(2), result.Field2) +} + +func TestStringToInt_unmarshalJSON_invalid_type_returns_error(t *testing.T) { + body := struct { + Field1 bool `json:"Field1"` + Field2 bool `json:"Field2"` + }{ + Field1: true, + Field2: false, + } + _, r := utils.GenerateRequestInfo("POST", "/", body, true) + + result := &StringToIntStruct{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(result) + + assert.Error(t, err) +} + +func TestStringToInt_int_returns_int_type(t *testing.T) { + s := StringToInt(1) + + assert.Equal(t, int(1), s.Int()) +} diff --git a/app/models/models.go b/app/models/models.go new file mode 100644 index 00000000..f8b42334 --- /dev/null +++ b/app/models/models.go @@ -0,0 +1,172 @@ +package models + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/Admiral-Piett/goaws/app" + log "github.com/sirupsen/logrus" +) + +func NewCreateQueueRequest() *CreateQueueRequest { + return &CreateQueueRequest{ + Attributes: Attributes{ + DelaySeconds: 0, + MaximumMessageSize: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize), + MessageRetentionPeriod: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod), + ReceiveMessageWaitTimeSeconds: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds), + VisibilityTimeout: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout), + }, + } +} + +type CreateQueueRequest struct { + QueueName string `json:"QueueName" schema:"QueueName"` + Attributes Attributes `json:"Attributes" schema:"Attribute"` + Tags map[string]string `json:"Tags" schema:"Tags"` + Version string `json:"Version" schema:"Version"` +} + +// TODO - is there an easier way to do this? Similar to the StringToInt type? +func (r *CreateQueueRequest) SetAttributesFromForm(values url.Values) { + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attribute.%d.Name", i) + attrName := values.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attribute.%d.Value", i) + attrValue := values.Get(valueKey) + if attrValue == "" { + continue + } + switch attrName { + case "DelaySeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.DelaySeconds = StringToInt(tmp) + case "MaximumMessageSize": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MaximumMessageSize = StringToInt(tmp) + case "MessageRetentionPeriod": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MessageRetentionPeriod = StringToInt(tmp) + case "Policy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.Policy = tmp + case "ReceiveMessageWaitTimeSeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.ReceiveMessageWaitTimeSeconds = StringToInt(tmp) + case "VisibilityTimeout": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.VisibilityTimeout = StringToInt(tmp) + case "RedrivePolicy": + tmp := RedrivePolicy{} + var decodedPolicy struct { + MaxReceiveCount interface{} `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + } + err := json.Unmarshal([]byte(attrValue), &decodedPolicy) + if err != nil || decodedPolicy.DeadLetterTargetArn == "" { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + // Support both int and string types (historic processing), set a default of 10 if not provided. + // Go will default into float64 for interface{} types when parsing numbers + receiveCount, ok := decodedPolicy.MaxReceiveCount.(float64) + if !ok { + receiveCount = 10 + t, ok := decodedPolicy.MaxReceiveCount.(string) + if ok { + r, err := strconv.ParseFloat(t, 64) + if err == nil { + receiveCount = r + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } + tmp.MaxReceiveCount = StringToInt(receiveCount) + tmp.DeadLetterTargetArn = decodedPolicy.DeadLetterTargetArn + r.Attributes.RedrivePolicy = tmp + case "RedriveAllowPolicy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.RedriveAllowPolicy = tmp + } + } + return +} + +// TODO - copy Attributes for SNS + +// TODO - there are FIFO attributes and things too +// Attributes - SQS Attributes Available in create/set attributes requests. +// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html#SQS-CreateQueue-request-attributes +type Attributes struct { + DelaySeconds StringToInt `json:"DelaySeconds"` + MaximumMessageSize StringToInt `json:"MaximumMessageSize"` + MessageRetentionPeriod StringToInt `json:"MessageRetentionPeriod"` // NOTE: not implemented + Policy map[string]interface{} `json:"Policy"` // NOTE: not implemented + ReceiveMessageWaitTimeSeconds StringToInt `json:"ReceiveMessageWaitTimeSeconds"` + VisibilityTimeout StringToInt `json:"VisibilityTimeout"` + // Dead Letter Queues Only + RedrivePolicy RedrivePolicy `json:"RedrivePolicy"` + RedriveAllowPolicy map[string]interface{} `json:"RedriveAllowPolicy"` // NOTE: not implemented +} + +type RedrivePolicy struct { + MaxReceiveCount StringToInt `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` +} + +// UnmarshalJSON this will convert a JSON string of a Redrive Policy sub-doc (escaped characters and all) or +// a regular json document into the appropriate resulting struct. +func (r *RedrivePolicy) UnmarshalJSON(data []byte) error { + type basicRequest RedrivePolicy + + err := json.Unmarshal(data, (*basicRequest)(r)) + if err == nil { + return nil + } + + tmp, _ := strconv.Unquote(string(data)) + err = json.Unmarshal([]byte(tmp), (*basicRequest)(r)) + if err != nil { + return err + } + return nil +} diff --git a/app/models/models_test.go b/app/models/models_test.go new file mode 100644 index 00000000..1ec5323f --- /dev/null +++ b/app/models/models_test.go @@ -0,0 +1,228 @@ +package models + +import ( + "encoding/json" + "fmt" + "net/url" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/stretchr/testify/assert" +) + +func TestNewCreateQueueRequest(t *testing.T) { + app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize = 262144 + app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod = 345600 + app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds = 10 + app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout = 30 + defer func() { + utils.ResetApp() + }() + + expectedCreateQueueRequest := &CreateQueueRequest{ + Attributes: Attributes{ + DelaySeconds: 0, + MaximumMessageSize: 262144, + MessageRetentionPeriod: 345600, + ReceiveMessageWaitTimeSeconds: 10, + VisibilityTimeout: 30, + }, + } + + result := NewCreateQueueRequest() + + assert.Equal(t, expectedCreateQueueRequest, result) +} + +func TestCreateQueueRequest_SetAttributesFromForm_success(t *testing.T) { + expectedRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Action", "CreateQueue") + form.Add("QueueName", "new-queue") + form.Add("Version", "2012-11-05") + form.Add("Attribute.1.Name", "DelaySeconds") + form.Add("Attribute.1.Value", "1") + form.Add("Attribute.2.Name", "MaximumMessageSize") + form.Add("Attribute.2.Value", "2") + form.Add("Attribute.3.Name", "MessageRetentionPeriod") + form.Add("Attribute.3.Value", "3") + form.Add("Attribute.4.Name", "Policy") + form.Add("Attribute.4.Value", "{\"i-am\":\"the-policy\"}") + form.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") + form.Add("Attribute.5.Value", "4") + form.Add("Attribute.6.Name", "VisibilityTimeout") + form.Add("Attribute.6.Value", "5") + form.Add("Attribute.7.Name", "RedrivePolicy") + form.Add("Attribute.7.Value", "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + form.Add("Attribute.8.Name", "RedriveAllowPolicy") + form.Add("Attribute.8.Value", "{\"i-am\":\"the-redrive-allow-policy\"}") + + cqr := &CreateQueueRequest{ + Attributes: Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 262144, + MessageRetentionPeriod: 345600, + ReceiveMessageWaitTimeSeconds: 10, + VisibilityTimeout: 30, + }, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, StringToInt(1), cqr.Attributes.DelaySeconds) + assert.Equal(t, StringToInt(2), cqr.Attributes.MaximumMessageSize) + assert.Equal(t, StringToInt(3), cqr.Attributes.MessageRetentionPeriod) + assert.Equal(t, map[string]interface{}{"i-am": "the-policy"}, cqr.Attributes.Policy) + assert.Equal(t, StringToInt(4), cqr.Attributes.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, StringToInt(5), cqr.Attributes.VisibilityTimeout) + assert.Equal(t, expectedRedrivePolicy, cqr.Attributes.RedrivePolicy) + assert.Equal(t, map[string]interface{}{"i-am": "the-redrive-allow-policy"}, cqr.Attributes.RedriveAllowPolicy) +} + +func TestCreateQueueRequest_SetAttributesFromForm_success_handles_redrive_recieve_count_int(t *testing.T) { + expectedRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Attribute.1.Name", "RedrivePolicy") + form.Add("Attribute.1.Value", "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + + cqr := &CreateQueueRequest{ + Attributes: Attributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, expectedRedrivePolicy, cqr.Attributes.RedrivePolicy) +} + +func TestCreateQueueRequest_SetAttributesFromForm_success_handles_redrive_recieve_count_string(t *testing.T) { + expectedRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Attribute.1.Name", "RedrivePolicy") + form.Add("Attribute.1.Value", "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + + cqr := &CreateQueueRequest{ + Attributes: Attributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, expectedRedrivePolicy, cqr.Attributes.RedrivePolicy) +} + +func TestCreateQueueRequest_SetAttributesFromForm_success_default_unparsable_redrive_recieve_count(t *testing.T) { + defaultRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 10, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Attribute.1.Name", "RedrivePolicy") + form.Add("Attribute.1.Value", "{\"maxReceiveCount\": null, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + + cqr := &CreateQueueRequest{ + Attributes: Attributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, defaultRedrivePolicy, cqr.Attributes.RedrivePolicy) +} + +func TestCreateQueueRequest_SetAttributesFromForm_success_skips_invalid_values(t *testing.T) { + form := url.Values{} + form.Add("Attribute.1.Name", "DelaySeconds") + form.Add("Attribute.1.Value", "garbage") + form.Add("Attribute.2.Name", "MaximumMessageSize") + form.Add("Attribute.2.Value", "garbage") + form.Add("Attribute.3.Name", "MessageRetentionPeriod") + form.Add("Attribute.3.Value", "garbage") + form.Add("Attribute.4.Name", "Policy") + form.Add("Attribute.4.Value", "garbage") + form.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") + form.Add("Attribute.5.Value", "garbage") + form.Add("Attribute.6.Name", "VisibilityTimeout") + form.Add("Attribute.6.Value", "garbage") + form.Add("Attribute.7.Name", "RedrivePolicy") + form.Add("Attribute.7.Value", "garbage") + form.Add("Attribute.8.Name", "RedriveAllowPolicy") + form.Add("Attribute.8.Value", "garbage") + + cqr := &CreateQueueRequest{ + Attributes: Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 262144, + MessageRetentionPeriod: 345600, + ReceiveMessageWaitTimeSeconds: 10, + VisibilityTimeout: 30, + }, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, StringToInt(1), cqr.Attributes.DelaySeconds) + assert.Equal(t, StringToInt(262144), cqr.Attributes.MaximumMessageSize) + assert.Equal(t, StringToInt(345600), cqr.Attributes.MessageRetentionPeriod) + assert.Equal(t, map[string]interface{}(nil), cqr.Attributes.Policy) + assert.Equal(t, StringToInt(10), cqr.Attributes.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, StringToInt(30), cqr.Attributes.VisibilityTimeout) + assert.Equal(t, RedrivePolicy{}, cqr.Attributes.RedrivePolicy) + assert.Equal(t, map[string]interface{}(nil), cqr.Attributes.RedriveAllowPolicy) +} + +func TestRedrivePolicy_UnmarshalJSON_handles_nested_json(t *testing.T) { + request := struct { + MaxReceiveCount int `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + }{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "arn:redrive-queue", + } + b, _ := json.Marshal(request) + var r = RedrivePolicy{} + err := r.UnmarshalJSON(b) + + assert.Nil(t, err) + assert.Equal(t, StringToInt(100), r.MaxReceiveCount) + assert.Equal(t, fmt.Sprintf("%s:%s", "arn", "redrive-queue"), r.DeadLetterTargetArn) +} + +func TestRedrivePolicy_UnmarshalJSON_handles_escaped_string(t *testing.T) { + request := `{"maxReceiveCount":"100","deadLetterTargetArn":"arn:redrive-queue"}` + b, _ := json.Marshal(request) + var r = RedrivePolicy{} + err := r.UnmarshalJSON(b) + + assert.Nil(t, err) + assert.Equal(t, StringToInt(100), r.MaxReceiveCount) + assert.Equal(t, fmt.Sprintf("%s:%s", "arn", "redrive-queue"), r.DeadLetterTargetArn) +} + +func TestRedrivePolicy_UnmarshalJSON_invalid_json_request_returns_error(t *testing.T) { + request := fmt.Sprintf(`{\"maxReceiveCount\":\"100\",\"deadLetterTargetArn\":\"arn:redrive-queue\"}`) + var r = RedrivePolicy{} + err := r.UnmarshalJSON([]byte(request)) + + assert.Error(t, err) + assert.Equal(t, StringToInt(0), r.MaxReceiveCount) + assert.Equal(t, "", r.DeadLetterTargetArn) +} + +func TestRedrivePolicy_UnmarshalJSON_invalid_type_returns_error(t *testing.T) { + request := `{"maxReceiveCount":true,"deadLetterTargetArn":"arn:redrive-queue"}` + b, _ := json.Marshal(request) + var r = RedrivePolicy{} + err := r.UnmarshalJSON(b) + + assert.Error(t, err) + assert.Equal(t, StringToInt(0), r.MaxReceiveCount) + assert.Equal(t, "", r.DeadLetterTargetArn) +} diff --git a/app/models/responses.go b/app/models/responses.go new file mode 100644 index 00000000..84b21a3e --- /dev/null +++ b/app/models/responses.go @@ -0,0 +1,45 @@ +package models + +import "github.com/Admiral-Piett/goaws/app" + +// NOTE: Every response in here MUST implement the `AbstractResponseBody` interface in order to be used +// in `encodeResponse` + +/*** Error Responses ***/ +type ErrorResult struct { + Type string `xml:"Type,omitempty"` + Code string `xml:"Code,omitempty"` + Message string `xml:"Message,omitempty"` +} + +type ErrorResponse struct { + Result ErrorResult `xml:"Error"` + RequestId string `xml:"RequestId"` +} + +func (r ErrorResponse) GetResult() interface{} { + return r.Result +} + +func (r ErrorResponse) GetRequestId() string { + return r.RequestId +} + +/*** Create Queue Response */ +type CreateQueueResult struct { + QueueUrl string `json:"QueueUrl" xml:"QueueUrl"` +} + +type CreateQueueResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result CreateQueueResult `xml:"CreateQueueResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r CreateQueueResponse) GetResult() interface{} { + return r.Result +} + +func (r CreateQueueResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index 91273adc..f7eb19be 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -1,10 +1,14 @@ package router import ( + "encoding/json" + "encoding/xml" "io" "net/http" "strings" + "github.com/Admiral-Piett/goaws/app/interfaces" + log "github.com/sirupsen/logrus" "fmt" @@ -28,10 +32,41 @@ func New() http.Handler { return r } +func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, body interfaces.AbstractResponseBody) { + protocol := resolveProtocol(req) + switch protocol { + case AwsJsonProtocol: + w.Header().Set("x-amzn-RequestId", body.GetRequestId()) + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + // Stupidly these `WriteHeader` calls have to be here, if they're at the start + // they lock the headers, at the end they're ignored. + w.WriteHeader(statusCode) + err := json.NewEncoder(w).Encode(body.GetResult()) + if err != nil { + log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) + http.Error(w, "General Error", http.StatusInternalServerError) + } + case AwsQueryProtocol: + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(statusCode) + result, err := xml.Marshal(body) + if err != nil { + log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) + http.Error(w, "General Error", http.StatusInternalServerError) + } + _, _ = w.Write(result) + } +} + +// V1 - includes JSON Support (and of course the old XML). +var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + "CreateQueue": sqs.CreateQueueV1, +} + var routingTable = map[string]http.HandlerFunc{ // SQS - "ListQueues": sqs.ListQueues, - "CreateQueue": sqs.CreateQueue, + "ListQueues": sqs.ListQueues, + //"CreateQueue": sqs.CreateQueue, "GetQueueAttributes": sqs.GetQueueAttributes, "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessage": sqs.SendMessage, @@ -72,6 +107,13 @@ func actionHandler(w http.ResponseWriter, req *http.Request) { "action": action, "url": req.URL, }).Debug("Handling URL request") + // If we don't find a match in this table, pass on to the existing flow. + jsonFn, ok := routingTableV1[action] + if ok { + statusCode, responseBody := jsonFn(req) + encodeResponse(w, req, statusCode, responseBody) + return + } fn, ok := routingTable[action] if !ok { log.Println("Bad Request - Action:", action) diff --git a/app/router/router_test.go b/app/router/router_test.go index b255073a..76c8608f 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -3,12 +3,31 @@ package router import ( "bytes" "encoding/json" + "encoding/xml" "net/http" "net/http/httptest" "net/url" + "strings" "testing" + + "github.com/Admiral-Piett/goaws/app/mocks" + + "github.com/Admiral-Piett/goaws/app/interfaces" + + sns "github.com/Admiral-Piett/goaws/app/gosns" + + sqs "github.com/Admiral-Piett/goaws/app/gosqs" + + "github.com/stretchr/testify/assert" + + "github.com/Admiral-Piett/goaws/app/utils" ) +func TestMain(m *testing.M) { + utils.InitializeDecoders() + m.Run() +} + func TestIndexServerhandler_POST_BadRequest(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. @@ -130,3 +149,170 @@ func TestIndexServerhandler_GET_GoodRequest_Pem_cert(t *testing.T) { status, http.StatusOK) } } + +func TestEncodeResponse_success_xml(t *testing.T) { + w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + + encodeResponse(w, r, http.StatusOK, mocks.BaseResponse{Message: "test"}) + + assert.Equal(t, http.StatusOK, w.Code) + + tmp := mocks.BaseResponse{} + xml.Unmarshal(w.Body.Bytes(), &tmp) + assert.Equal(t, mocks.BaseResponse{Message: "test"}, tmp) +} + +func TestEncodeResponse_success_skips_nil_body_xml(t *testing.T) { + w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + + encodeResponse(w, r, http.StatusOK, nil) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, &bytes.Buffer{}, w.Body) +} + +func TestEncodeResponse_success_json(t *testing.T) { + w, r := utils.GenerateRequestInfo("POST", "/url", nil, true) + + encodeResponse(w, r, http.StatusOK, mocks.BaseResponse{Message: "test"}) + + assert.Equal(t, http.StatusOK, w.Code) + + tmp := mocks.BaseResponse{} + json.Unmarshal(w.Body.Bytes(), &tmp) + assert.Equal(t, mocks.BaseResponse{Message: "test"}, tmp) +} + +func TestEncodeResponse_success_skips_malformed_body_json(t *testing.T) { + mock := mocks.BaseResponse{ + Message: "test", + } + mock.MockGetResult = func() interface{} { + return make(chan int) + } + w, r := utils.GenerateRequestInfo("POST", "/url", nil, true) + + encodeResponse(w, r, http.StatusOK, mock) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "General Error", strings.TrimSpace(string(w.Body.Bytes()))) +} + +func TestActionHandler_v1_json(t *testing.T) { + defer func() { + routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + "CreateQueue": sqs.CreateQueueV1, + } + }() + + mockCalled := false + mockFunction := func(req *http.Request) (int, interfaces.AbstractResponseBody) { + mockCalled = true + return http.StatusOK, mocks.BaseResponse{Message: "response-body"} + } + routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + "CreateQueue": mockFunction, + } + + w, r := utils.GenerateRequestInfo("POST", "/url", nil, true) + r.Header.Set("X-Amz-Target", "QueueService.CreateQueue") + + actionHandler(w, r) + + assert.True(t, mockCalled) + assert.Equal(t, http.StatusOK, w.Code) + + tmp := mocks.BaseResponse{} + json.Unmarshal(w.Body.Bytes(), &tmp) + assert.Equal(t, mocks.BaseResponse{Message: "response-body"}, tmp) +} + +func TestActionHandler_v1_xml(t *testing.T) { + defer func() { + routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + "CreateQueue": sqs.CreateQueueV1, + } + }() + + mockCalled := false + mockFunction := func(req *http.Request) (int, interfaces.AbstractResponseBody) { + mockCalled = true + return http.StatusOK, mocks.BaseResponse{Message: "response-body"} + } + routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + "CreateQueue": mockFunction, + } + + w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + form := url.Values{} + form.Add("Action", "CreateQueue") + r.PostForm = form + + actionHandler(w, r) + + assert.True(t, mockCalled) + assert.Equal(t, http.StatusOK, w.Code) + + tmp := mocks.BaseResponse{} + xml.Unmarshal(w.Body.Bytes(), &tmp) + assert.Equal(t, mocks.BaseResponse{Message: "response-body"}, tmp) +} + +func TestActionHandler_v0_xml(t *testing.T) { + defer func() { + routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + "CreateQueue": sqs.CreateQueueV1, + } + routingTable = map[string]http.HandlerFunc{ + // SQS + "ListQueues": sqs.ListQueues, + //"CreateQueue": sqs.CreateQueue, + "GetQueueAttributes": sqs.GetQueueAttributes, + "SetQueueAttributes": sqs.SetQueueAttributes, + "SendMessage": sqs.SendMessage, + "SendMessageBatch": sqs.SendMessageBatch, + "ReceiveMessage": sqs.ReceiveMessage, + "DeleteMessage": sqs.DeleteMessage, + "DeleteMessageBatch": sqs.DeleteMessageBatch, + "GetQueueUrl": sqs.GetQueueUrl, + "PurgeQueue": sqs.PurgeQueue, + "DeleteQueue": sqs.DeleteQueue, + "ChangeMessageVisibility": sqs.ChangeMessageVisibility, + + // SNS + "ListTopics": sns.ListTopics, + "CreateTopic": sns.CreateTopic, + "DeleteTopic": sns.DeleteTopic, + "Subscribe": sns.Subscribe, + "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, + "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, + "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, + "ListSubscriptions": sns.ListSubscriptions, + "Unsubscribe": sns.Unsubscribe, + "Publish": sns.Publish, + + // SNS Internal + "ConfirmSubscription": sns.ConfirmSubscription, + } + }() + + mockCalled := false + mockFunction := func(w http.ResponseWriter, req *http.Request) { + mockCalled = true + w.WriteHeader(http.StatusOK) + } + routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){} + routingTable = map[string]http.HandlerFunc{ + "CreateQueue": mockFunction, + } + + w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + form := url.Values{} + form.Add("Action", "CreateQueue") + r.PostForm = form + + actionHandler(w, r) + + assert.True(t, mockCalled) + assert.Equal(t, http.StatusOK, w.Code) +} diff --git a/app/servertest/server_test.go b/app/servertest/server_test.go index 48be85a0..38d4d1b9 100644 --- a/app/servertest/server_test.go +++ b/app/servertest/server_test.go @@ -4,6 +4,8 @@ import ( "errors" "testing" + "github.com/Admiral-Piett/goaws/app/utils" + "encoding/json" "fmt" "io/ioutil" @@ -26,6 +28,11 @@ import ( "github.com/stretchr/testify/require" ) +func TestMain(m *testing.M) { + utils.InitializeDecoders() + m.Run() +} + func TestNew(t *testing.T) { // Consume address srv, err := New("localhost:4100") diff --git a/app/sqs.go b/app/sqs.go index 564781bc..ff826aec 100644 --- a/app/sqs.go +++ b/app/sqs.go @@ -78,22 +78,24 @@ type MessageAttributeValue struct { ValueKey string } +// TODO - put all this in the models package type Queue struct { - Name string - URL string - Arn string - TimeoutSecs int - ReceiveWaitTimeSecs int - DelaySecs int - MaximumMessageSize int - Messages []Message - DeadLetterQueue *Queue - MaxReceiveCount int - IsFIFO bool - FIFOMessages map[string]int - FIFOSequenceNumbers map[string]int - EnableDuplicates bool - Duplicates map[string]time.Time + Name string + URL string + Arn string + VisibilityTimeout int // seconds + ReceiveMessageWaitTimeSeconds int + DelaySeconds int + MaximumMessageSize int + MessageRetentionPeriod int // seconds // TODO - not used in the code yet + Messages []Message + DeadLetterQueue *Queue + MaxReceiveCount int + IsFIFO bool + FIFOMessages map[string]int + FIFOSequenceNumbers map[string]int + EnableDuplicates bool + Duplicates map[string]time.Time } var SyncQueues = struct { diff --git a/app/sqs_messages.go b/app/sqs_messages.go index faf5df57..9b51b11c 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -11,17 +11,6 @@ type ListQueuesResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Create Queue Response */ -type CreateQueueResult struct { - QueueUrl string `xml:"QueueUrl"` -} - -type CreateQueueResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result CreateQueueResult `xml:"CreateQueueResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Send Message Response */ type SendMessageResult struct { diff --git a/app/utils/tests.go b/app/utils/tests.go new file mode 100644 index 00000000..d62c7e92 --- /dev/null +++ b/app/utils/tests.go @@ -0,0 +1,84 @@ +package utils + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + urlLib "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + + "github.com/Admiral-Piett/goaws/app" +) + +func ResetApp() { + app.CurrentEnvironment = app.Environment{} + ResetResources() +} + +func ResetResources() { + app.SyncQueues.Lock() + app.SyncQueues.Queues = make(map[string]*app.Queue) + app.SyncQueues.Unlock() + app.SyncTopics.Lock() + app.SyncTopics.Topics = make(map[string]*app.Topic) + app.SyncTopics.Unlock() +} + +func GenerateRequestInfo(method, url string, body interface{}, isJson bool) (*httptest.ResponseRecorder, *http.Request) { + if url == "" { + url = "/health-check" + } + if method == "" { + method = "GET" + } + + var req *http.Request + var err error + if isJson { + if body != nil { + b, _ := json.Marshal(body) + request_body := bytes.NewBuffer(b) + req, err = http.NewRequest(method, url, request_body) + } else { + req, err = http.NewRequest(method, url, nil) + } + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/x-amz-json-1.0") + } else { + req, err = http.NewRequest(method, url, nil) + req.Header.Set("Content-Type", "multipart/form-data") + body, _ := body.(urlLib.Values) + req.PostForm = body + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + return rr, req +} + +// GenerateLocalProxyConfig use this to create AWS config that can be plugged into your sqs client, and +// force calls onto a local proxy. This is helpful for testing directly with an HTTP inspection tool +// such as Charles or Proxyman. +func GenerateLocalProxyConfig(proxyPort int) aws.Config { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + } + proxyURL, _ := urlLib.Parse(fmt.Sprintf("http://127.0.0.1:%d", proxyPort)) + tr.Proxy = http.ProxyURL(proxyURL) + client := &http.Client{Transport: tr} + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO(), + config.WithHTTPClient(client), + ) + return sdkConfig +} diff --git a/app/utils/utils.go b/app/utils/utils.go new file mode 100644 index 00000000..3e1aa5fd --- /dev/null +++ b/app/utils/utils.go @@ -0,0 +1,70 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/Admiral-Piett/goaws/app/interfaces" + + log "github.com/sirupsen/logrus" + + "github.com/gorilla/schema" +) + +var XmlDecoder *schema.Decoder +var REQUEST_TRANSFORMER = TransformRequest + +func InitializeDecoders() { + XmlDecoder = schema.NewDecoder() + XmlDecoder.IgnoreUnknownKeys(true) +} + +// QUESTION - alternately we could have the router.actionHandler method call this, but then our router maps +// need to track the request type AND the function call. I think there'd be a lot of interface switching +// back and forth. +func TransformRequest(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + switch req.Header.Get("Content-Type") { + case "application/x-amz-json-1.0": + //Read body data to parse json + decoder := json.NewDecoder(req.Body) + err := decoder.Decode(resultingStruct) + if err != nil { + log.Debugf("TransformRequest Failure - %s", err.Error()) + return false + } + default: + err := req.ParseForm() + if err != nil { + log.Debugf("TransformRequest Failure - %s", err.Error()) + return false + } + err = XmlDecoder.Decode(resultingStruct, req.PostForm) + if err != nil { + log.Debugf("TransformRequest Failure - %s", err.Error()) + return false + } + resultingStruct.SetAttributesFromForm(req.PostForm) + } + + return true +} + +func ExtractQueueAttributes(u url.Values) map[string]string { + attr := map[string]string{} + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attribute.%d.Name", i) + attrName := u.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attribute.%d.Value", i) + attrValue := u.Get(valueKey) + if attrValue != "" { + attr[attrName] = attrValue + } + } + return attr +} diff --git a/app/utils/utils_test.go b/app/utils/utils_test.go new file mode 100644 index 00000000..ea35f45f --- /dev/null +++ b/app/utils/utils_test.go @@ -0,0 +1,103 @@ +package utils + +import ( + "net/url" + "testing" + + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/mocks" + + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + InitializeDecoders() + m.Run() +} + +func TestTransformRequest_success_json(t *testing.T) { + _, r := GenerateRequestInfo("POST", "url", fixtures.JSONRequestBody, true) + + mock := &mocks.MockRequestBody{} + + ok := TransformRequest(mock, r) + + assert.True(t, ok) + assert.Equal(t, "mock-value", mock.RequestFieldStr) + assert.False(t, mock.SetAttributesFromFormCalled) +} + +func TestTransformRequest_success_xml(t *testing.T) { + _, r := GenerateRequestInfo("POST", "url", nil, false) + form := url.Values{} + form.Add("Action", "CreateQueue") + form.Add("QueueName", "UnitTestQueue1") + form.Add("Attribute.1.Name", "VisibilityTimeout") + form.Add("Attribute.1.Value", "60") + form.Add("Attribute.2.Name", "MaximumMessageSize") + form.Add("Attribute.2.Value", "2048") + r.PostForm = form + + mock := &mocks.MockRequestBody{} + + ok := TransformRequest(mock, r) + + assert.True(t, ok) + assert.True(t, mock.SetAttributesFromFormCalled) + assert.Equal(t, []interface{}{form}, mock.SetAttributesFromFormCalledWith) +} + +func TestTransformRequest_error_invalid_request_body_json(t *testing.T) { + _, r := GenerateRequestInfo("POST", "url", "\"I-am-garbage", true) + + mock := &mocks.MockRequestBody{} + + ok := TransformRequest(mock, r) + + assert.False(t, ok) + assert.Equal(t, "", mock.RequestFieldStr) + assert.False(t, mock.SetAttributesFromFormCalled) +} + +func TestTransformRequest_error_failure_to_parse_form_xml(t *testing.T) { + _, r := GenerateRequestInfo("POST", "url", nil, false) + + mock := &mocks.MockRequestBody{} + + ok := TransformRequest(mock, r) + + assert.False(t, ok) + assert.False(t, mock.SetAttributesFromFormCalled) +} + +func TestTransformRequest_error_invalid_request_body_xml(t *testing.T) { + _, r := GenerateRequestInfo("POST", "url", nil, false) + + form := url.Values{} + form.Add("intField", "\"I-am-garbage") + r.PostForm = form + + mock := &mocks.MockRequestBody{} + + ok := TransformRequest(mock, r) + + assert.False(t, ok) + assert.False(t, mock.SetAttributesFromFormCalled) +} + +func TestExtractQueueAttributes_success(t *testing.T) { + u := url.Values{} + u.Add("Attribute.1.Name", "DelaySeconds") + u.Add("Attribute.1.Value", "20") + u.Add("Attribute.2.Name", "VisibilityTimeout") + u.Add("Attribute.2.Value", "30") + u.Add("Attribute.3.Name", "Policy") + + attr := ExtractQueueAttributes(u) + expected := map[string]string{ + "DelaySeconds": "20", + "VisibilityTimeout": "30", + } + + assert.Equal(t, expected, attr) +} diff --git a/go.mod b/go.mod index e5763b0e..3feb252f 100644 --- a/go.mod +++ b/go.mod @@ -4,21 +4,67 @@ go 1.18 require ( github.com/aws/aws-sdk-go v1.47.3 + github.com/aws/aws-sdk-go-v2 v1.25.2 + github.com/aws/aws-sdk-go-v2/config v1.27.4 + github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1 + github.com/gavv/httpexpect/v2 v2.16.0 github.com/ghodss/yaml v1.0.0 github.com/gorilla/mux v1.8.0 + github.com/gorilla/schema v1.2.1 + github.com/mitchellh/copystructure v1.2.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.7.0 ) require ( + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 // indirect + github.com/aws/smithy-go v1.20.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hpcloud/tail v1.0.0 // indirect + github.com/imkira/go-interpol v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.15.0 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.5.0 // indirect + github.com/sanity-io/litter v1.5.5 // indirect + github.com/sergi/go-diff v1.0.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.34.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect + github.com/yudai/gojsondiff v1.0.0 // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/fsnotify.v1 v1.4.7 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0 // indirect + moul.io/http2curl/v2 v2.3.0 // indirect ) retract ( diff --git a/go.sum b/go.sum index e399d3f6..1f37d935 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,178 @@ -github.com/aws/aws-sdk-go v1.34.0 h1:brux2dRrlwCF5JhTL7MUT3WUwo9zfDHZZp3+g3Mvlmo= -github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/aws/aws-sdk-go v1.47.3 h1:e0H6NFXiniCpR8Lu3lTphVdRaeRCDLAeRyTHd1tJSd8= github.com/aws/aws-sdk-go v1.47.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= +github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= +github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= +github.com/aws/aws-sdk-go-v2/config v1.27.4/go.mod h1:zq2FFXK3A416kiukwpsd+rD4ny6JC7QSkp4QdN1Mp2g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgpd9keKe2EAENgAuI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.4/go.mod h1:+30tpwrkOgvkJL1rUZuRLoxcJwtI/OkeBLYnHxJtVe0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 h1:5ffmXjPtwRExp1zc7gENLgCPyHFbhEPwVTkTiH9niSk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0= +github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1 h1:124rVNP6NbCfBZwiX1kfjMQrnsJtnpKeB0GalkuqSXo= +github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1/go.mod h1:YijRvM1SAmuiIQ9pjfwahIEE3HMHUkx9P5oplL/Jnj4= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1LHPqXYk/QGlu2ew= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/gavv/httpexpect/v2 v2.16.0 h1:Ty2favARiTYTOkCRZGX7ojXXjGyNAIohM1lZ3vqaEwI= +github.com/gavv/httpexpect/v2 v2.16.0/go.mod h1:uJLaO+hQ25ukBJtQi750PsztObHybNllN+t+MbbW8PY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= +github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= +github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -46,3 +180,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/smoke_tests/fixtures/fixtures.go b/smoke_tests/fixtures/fixtures.go new file mode 100644 index 00000000..5fb520b2 --- /dev/null +++ b/smoke_tests/fixtures/fixtures.go @@ -0,0 +1,3 @@ +package fixtures + +var REQUEST_ID = "00000000-0000-0000-0000-000000000000" diff --git a/smoke_tests/fixtures/requests.go b/smoke_tests/fixtures/requests.go new file mode 100644 index 00000000..6c18fb7f --- /dev/null +++ b/smoke_tests/fixtures/requests.go @@ -0,0 +1,44 @@ +package fixtures + +import ( + "fmt" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" +) + +var ListQueuesRequestBodyXML = struct { + Action string `xml:"Action"` + Version string `xml:"Version"` +}{ + Action: "ListQueues", + Version: "2012-11-05", +} + +var GetQueueAttributesRequestBodyXML = struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + Attribute1 string `xml:"AttributeName.1"` + QueueUrl string `xml:"QueueUrl"` +}{ + Action: "GetQueueAttributes", + Version: "2012-11-05", + Attribute1: "All", + QueueUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL), +} + +var CreateQueueV1RequestBodyJSON = models.CreateQueueRequest{ + QueueName: af.QueueName, + Version: "2012-11-05", + Attributes: af.CreateQueueAttributes, +} + +var CreateQueueV1RequestXML_NoAttributes = struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueName string `xml:"QueueName"` +}{ + Action: "CreateQueue", + Version: "2012-11-05", + QueueName: af.QueueName, +} diff --git a/smoke_tests/fixtures/responses.go b/smoke_tests/fixtures/responses.go new file mode 100644 index 00000000..62ed5a66 --- /dev/null +++ b/smoke_tests/fixtures/responses.go @@ -0,0 +1,52 @@ +package fixtures + +import ( + "fmt" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + + "github.com/Admiral-Piett/goaws/app" +) + +var BASE_GET_QUEUE_ATTRIBUTES_RESPONSE = app.GetQueueAttributesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: app.GetQueueAttributesResult{Attrs: []app.Attribute{ + { + Name: "VisibilityTimeout", + Value: "0", + }, + { + Name: "DelaySeconds", + Value: "0", + }, + { + Name: "ReceiveMessageWaitTimeSeconds", + Value: "0", + }, + { + Name: "ApproximateNumberOfMessages", + Value: "0", + }, + { + Name: "ApproximateNumberOfMessagesNotVisible", + Value: "0", + }, + { + Name: "CreatedTimestamp", + Value: "0000000000", + }, + { + Name: "LastModifiedTimestamp", + Value: "0000000000", + }, + { + Name: "QueueArn", + Value: fmt.Sprintf("%s:new-queue-1", af.BASE_ARN), + }, + { + Name: "RedrivePolicy", + Value: "{\"maxReceiveCount\": \"0\", \"deadLetterTargetArn\":\"\"}", + }, + }}, + Metadata: app.ResponseMetadata{RequestId: REQUEST_ID}, +} diff --git a/smoke_tests/sqs_create_queue_test.go b/smoke_tests/sqs_create_queue_test.go new file mode 100644 index 00000000..24989be9 --- /dev/null +++ b/smoke_tests/sqs_create_queue_test.go @@ -0,0 +1,447 @@ +package smoke_tests + +import ( + "context" + "encoding/json" + "encoding/xml" + "fmt" + "net/http" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/mitchellh/copystructure" + + "github.com/Admiral-Piett/goaws/app" + "github.com/stretchr/testify/assert" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + + "github.com/gavv/httpexpect/v2" +) + +// TODO - Is there a way to also capture the defaults we set and/or load from the config here? (review the xml +// code below) +// NOTE: Actually I think you can just adjust the app.CurrentEnvironment memory space...it travels across tests it seems. +func Test_CreateQueueV1_json_no_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *sdkResponse.QueueUrl) + + r := e.POST("/"). + WithForm(sf.ListQueuesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp2 := app.ListQueuesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + r2 := app.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Equal(t, exp2, r2) + + r = e.POST("/"). + WithForm(sf.GetQueueAttributesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + r3 := app.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r3) + assert.Equal(t, sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE, r3) +} + +func Test_CreateQueueV1_json_with_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + redriveQueue := "redrive-queue" + + //sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig := utils.GenerateLocalProxyConfig(9090) + sdkConfig.BaseEndpoint = aws.String(server.URL) + + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + + // TODO - HERE - Sub-Doc attributes not working for some reason? Check proxyman? + sdkResponse, err := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", + }, + }) + + assert.Nil(t, err) + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *sdkResponse.QueueUrl) + + r := e.POST("/"). + WithForm(sf.ListQueuesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp2 := app.ListQueuesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/%s", af.BASE_URL, redriveQueue), fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + r2 := app.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Equal(t, exp2, r2) + + r = e.POST("/"). + WithForm(sf.GetQueueAttributesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) + exp3, _ := dupe.(app.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "5" + exp3.Result.Attrs[1].Value = "1" + exp3.Result.Attrs[2].Value = "4" + exp3.Result.Attrs[8].Value = fmt.Sprintf(`{"maxReceiveCount": "100", "deadLetterTargetArn":"%s"}`, redriveQueue) + r3 := app.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r3) + assert.Equal(t, exp3, r3) +} + +func Test_CreateQueueV1_json_with_attributes_as_ints(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithHeaders(map[string]string{ + "Content-Type": "application/x-amz-json-1.0", + "X-Amz-Target": "AmazonSQS.CreateQueue", + }). + WithJSON(sf.CreateQueueV1RequestBodyJSON). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp1 := models.CreateQueueResult{QueueUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL)} + + r1 := models.CreateQueueResult{} + json.Unmarshal([]byte(r), &r1) + assert.Equal(t, exp1, r1) + + r = e.POST("/"). + WithForm(sf.ListQueuesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp2 := app.ListQueuesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + r2 := app.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Equal(t, exp2, r2) + + r = e.POST("/"). + WithForm(sf.GetQueueAttributesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) + exp3, _ := dupe.(app.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "5" + exp3.Result.Attrs[1].Value = "1" + exp3.Result.Attrs[2].Value = "4" + r3 := app.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r3) + assert.Equal(t, exp3, r3) +} + +// TODO - fix broken tests +func Test_CreateQueueV1_json_with_attributes_ints_as_strings(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + e.POST("/"). + WithHeaders(map[string]string{ + "Content-Type": "application/x-amz-json-1.0", + "X-Amz-Target": "AmazonSQS.CreateQueue", + }). + WithJSON(sf.CreateQueueV1RequestBodyJSON). + Expect(). + Status(http.StatusOK) + + cqr := struct { + Version string `json:"Version"` + QueueName string `json:"QueueName"` + Attributes struct { + DelaySeconds string `json:"DelaySeconds"` + MaximumMessageSize string `json:"MaximumMessageSize"` + MessageRetentionPeriod string `json:"MessageRetentionPeriod"` + //Policy string `json:"Policy"` + ReceiveMessageWaitTimeSeconds string `json:"ReceiveMessageWaitTimeSeconds"` + RedrivePolicy struct { + MaxReceiveCount string `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + } `json:"RedrivePolicy"` + VisibilityTimeout string `json:"VisibilityTimeout"` + } `json:"Attributes"` + }{ + Version: "2012-11-05", + QueueName: "new-string-queue", + Attributes: struct { + DelaySeconds string `json:"DelaySeconds"` + MaximumMessageSize string `json:"MaximumMessageSize"` + MessageRetentionPeriod string `json:"MessageRetentionPeriod"` + //Policy string `json:"Policy"` + ReceiveMessageWaitTimeSeconds string `json:"ReceiveMessageWaitTimeSeconds"` + RedrivePolicy struct { + MaxReceiveCount string `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + } `json:"RedrivePolicy"` + VisibilityTimeout string `json:"VisibilityTimeout"` + }{ + DelaySeconds: "1", + MaximumMessageSize: "2", + MessageRetentionPeriod: "3", + //Policy: "", + ReceiveMessageWaitTimeSeconds: "0", + RedrivePolicy: struct { + MaxReceiveCount string `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + }{ + MaxReceiveCount: "100", + DeadLetterTargetArn: fmt.Sprintf("%s:new-queue-1", af.BASE_ARN), + }, + VisibilityTimeout: "30"}, + } + r := e.POST("/"). + WithHeaders(map[string]string{ + "Content-Type": "application/x-amz-json-1.0", + "X-Amz-Target": "AmazonSQS.CreateQueue", + }). + WithJSON(cqr). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp1 := models.CreateQueueResult{QueueUrl: fmt.Sprintf("%s/new-string-queue", af.BASE_URL)} + + r1 := models.CreateQueueResult{} + json.Unmarshal([]byte(r), &r1) + assert.Equal(t, exp1, r1) + + gqar := struct { + Action string `xml:"Action"` + Attribute1 string `xml:"AttributeName.1"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "GetQueueAttributes", + Attribute1: "All", + QueueUrl: fmt.Sprintf("%s/new-string-queue", af.BASE_URL), + } + r = e.POST("/"). + WithForm(gqar). + Expect(). + Status(http.StatusOK). + Body().Raw() + + dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) + exp3, _ := dupe.(app.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "30" + exp3.Result.Attrs[1].Value = "1" + exp3.Result.Attrs[7].Value = fmt.Sprintf("%s:new-string-queue", af.BASE_ARN) + exp3.Result.Attrs[8].Value = "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"new-queue-1\"}" + r3 := app.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r3) + assert.Equal(t, exp3, r3) +} + +func Test_CreateQueueV1_xml_no_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + utils.InitializeDecoders() + + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithForm(sf.CreateQueueV1RequestXML_NoAttributes). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp1 := models.CreateQueueResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: models.CreateQueueResult{QueueUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + + r1 := models.CreateQueueResponse{} + xml.Unmarshal([]byte(r), &r1) + assert.Equal(t, exp1, r1) + + r = e.POST("/"). + WithForm(sf.ListQueuesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp2 := app.ListQueuesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + r2 := app.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Equal(t, exp2, r2) + + r = e.POST("/"). + WithForm(sf.GetQueueAttributesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + r3 := app.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r3) + assert.Equal(t, sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE, r3) +} + +func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + utils.InitializeDecoders() + + e := httpexpect.Default(t, server.URL) + + e.POST("/"). + WithHeaders(map[string]string{ + "Content-Type": "application/x-amz-json-1.0", + "X-Amz-Target": "AmazonSQS.CreateQueue", + }). + WithJSON(sf.CreateQueueV1RequestBodyJSON). + Expect(). + Status(http.StatusOK) + + request := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueName string `xml:"QueueName"` + }{ + Action: "CreateQueue", + Version: "2012-11-05", + QueueName: "new-queue-2", + } + r := e.POST("/"). + WithForm(request). + WithFormField("Attribute.1.Name", "VisibilityTimeout"). + WithFormField("Attribute.1.Value", "1"). + WithFormField("Attribute.2.Name", "MaximumMessageSize"). + WithFormField("Attribute.2.Value", "2"). + WithFormField("Attribute.3.Name", "DelaySeconds"). + WithFormField("Attribute.3.Value", "3"). + WithFormField("Attribute.4.Name", "MessageRetentionPeriod"). + WithFormField("Attribute.4.Value", "4"). + WithFormField("Attribute.5.Name", "Policy"). + WithFormField("Attribute.5.Value", "{\"this-is\": \"the-policy\"}"). + WithFormField("Attribute.6.Name", "ReceiveMessageWaitTimeSeconds"). + WithFormField("Attribute.6.Value", "5"). + WithFormField("Attribute.7.Name", "RedrivePolicy"). + WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:new-queue-1\"}", af.BASE_ARN)). + WithFormField("Attribute.8.Name", "RedriveAllowPolicy"). + WithFormField("Attribute.8.Value", "{\"this-is\": \"the-redrive-allow-policy\"}"). + Expect(). + Status(http.StatusOK). + Body().Raw() + + exp1 := models.CreateQueueResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: models.CreateQueueResult{QueueUrl: fmt.Sprintf("%s/new-queue-2", af.BASE_URL)}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + + r1 := models.CreateQueueResponse{} + xml.Unmarshal([]byte(r), &r1) + assert.Equal(t, exp1, r1) + + gqar := struct { + Action string `xml:"Action"` + Attribute1 string `xml:"AttributeName.1"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "GetQueueAttributes", + Attribute1: "All", + QueueUrl: fmt.Sprintf("%s/new-queue-2", af.BASE_URL), + } + r = e.POST("/"). + WithForm(gqar). + Expect(). + Status(http.StatusOK). + Body().Raw() + + dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) + exp3, _ := dupe.(app.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "1" + exp3.Result.Attrs[1].Value = "3" + exp3.Result.Attrs[2].Value = "5" + exp3.Result.Attrs[7].Value = fmt.Sprintf("%s:new-queue-2", af.BASE_ARN) + exp3.Result.Attrs[8].Value = "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"new-queue-1\"}" + r3 := app.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r3) + assert.Equal(t, exp3, r3) +} diff --git a/smoke_tests/utils.go b/smoke_tests/utils.go new file mode 100644 index 00000000..66845a40 --- /dev/null +++ b/smoke_tests/utils.go @@ -0,0 +1,11 @@ +package smoke_tests + +import ( + "net/http/httptest" + + "github.com/Admiral-Piett/goaws/app/router" +) + +func generateServer() *httptest.Server { + return httptest.NewServer(router.New()) +} From da44977203043ef4c62f137c7efdaf133a2507ab Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Wed, 24 Apr 2024 17:37:40 -0400 Subject: [PATCH 19/41] Add ListQueuesV1 for JSON support --- app/conf/mock-data/mock-config.yaml | 13 + app/fixtures/fixtures.go | 1 - app/fixtures/sqs.go | 21 +- app/gosqs/create_queue.go | 55 +++++ app/gosqs/create_queue_test.go | 280 ++++++++++++++++++++++ app/gosqs/gosqs.go | 68 ------ app/gosqs/gosqs_test.go | 344 --------------------------- app/gosqs/list_queues.go | 46 ++++ app/gosqs/list_queues_test.go | 111 +++++++++ app/models/models.go | 22 ++ app/models/models_test.go | 28 +++ app/models/responses.go | 20 ++ app/router/router.go | 3 +- app/router/router_test.go | 3 +- app/sqs_messages.go | 11 - app/utils/tests.go | 25 -- smoke_tests/main_test.go | 12 + smoke_tests/sqs_create_queue_test.go | 33 +-- smoke_tests/sqs_list_queues_test.go | 221 +++++++++++++++++ smoke_tests/utils.go | 31 +++ 20 files changed, 869 insertions(+), 479 deletions(-) create mode 100644 app/gosqs/create_queue.go create mode 100644 app/gosqs/create_queue_test.go create mode 100644 app/gosqs/list_queues.go create mode 100644 app/gosqs/list_queues_test.go create mode 100644 smoke_tests/main_test.go create mode 100644 smoke_tests/sqs_list_queues_test.go diff --git a/app/conf/mock-data/mock-config.yaml b/app/conf/mock-data/mock-config.yaml index 702e9607..c042fff2 100644 --- a/app/conf/mock-data/mock-config.yaml +++ b/app/conf/mock-data/mock-config.yaml @@ -48,3 +48,16 @@ NoQueueAttributeDefaults: - Name: local-queue1 - Name: local-queue2 ReceiveMessageWaitTimeSeconds: 20 + +BaseUnitTests: + # (i.e.: ./goaws [Local | Dev] -- defaults to 'Local') + Host: host # hostname of the goaws system (for docker-compose this is the tag name of the container) + Port: port # port to listen on. + Region: region + AccountId: accountID + LogMessages: true # Log messages (true/false) + LogFile: ./goaws_messages.log # Log filename (for message logging + Queues: # List of queues to create at startup + - Name: unit-queue1 # Queue name + - Name: unit-queue2 # Queue name + - Name: other-queue1 # Queue name diff --git a/app/fixtures/fixtures.go b/app/fixtures/fixtures.go index c88ce736..1db22736 100644 --- a/app/fixtures/fixtures.go +++ b/app/fixtures/fixtures.go @@ -3,5 +3,4 @@ package fixtures var BASE_URL = "http://region.host:port/accountID" var BASE_ARN = "arn:aws:sqs:region:accountID" -var XMLNS = "http://queue.amazonaws.com/doc/2012-11-05/" var REQUEST_ID = "request-id" diff --git a/app/fixtures/sqs.go b/app/fixtures/sqs.go index afd5c479..96f538aa 100644 --- a/app/fixtures/sqs.go +++ b/app/fixtures/sqs.go @@ -3,8 +3,6 @@ package fixtures import ( "fmt" - "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/models" ) @@ -36,9 +34,20 @@ var CreateQueueResult = models.CreateQueueResult{ } var CreateQueueResponse = models.CreateQueueResponse{ - Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: CreateQueueResult, - Metadata: app.ResponseMetadata{ - RequestId: "00000000-0000-0000-0000-000000000000", + Xmlns: models.BASE_XMLNS, + Result: CreateQueueResult, + Metadata: models.BASE_RESPONSE_METADATA, +} + +var ListQueuesResult = models.ListQueuesResult{ + QueueUrls: []string{ + fmt.Sprintf("%s/%s", BASE_URL, "unit-queue1"), + fmt.Sprintf("%s/%s", BASE_URL, "unit-queue2"), }, } + +var ListQueuesResponse = models.ListQueuesResponse{ + Xmlns: models.BASE_XMLNS, + Result: ListQueuesResult, + Metadata: models.BASE_RESPONSE_METADATA, +} diff --git a/app/gosqs/create_queue.go b/app/gosqs/create_queue.go new file mode 100644 index 00000000..aac0c5eb --- /dev/null +++ b/app/gosqs/create_queue.go @@ -0,0 +1,55 @@ +package gosqs + +import ( + "net/http" + "time" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + log "github.com/sirupsen/logrus" +) + +func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewCreateQueueRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - CreateQueueV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + queueName := requestBody.QueueName + + queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + + "/" + app.CurrentEnvironment.AccountID + "/" + queueName + if app.CurrentEnvironment.Region != "" { + queueUrl = "http://" + app.CurrentEnvironment.Region + "." + app.CurrentEnvironment.Host + ":" + + app.CurrentEnvironment.Port + "/" + app.CurrentEnvironment.AccountID + "/" + queueName + } + queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + queueName + + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + log.Println("Creating Queue:", queueName) + queue := &app.Queue{ + Name: queueName, + URL: queueUrl, + Arn: queueArn, + IsFIFO: app.HasFIFOQueueName(queueName), + EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, + Duplicates: make(map[string]time.Time), + } + if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { + return createErrorResponseV1(err.Error()) + } + app.SyncQueues.Lock() + app.SyncQueues.Queues[queueName] = queue + app.SyncQueues.Unlock() + } + + respStruct := models.CreateQueueResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.CreateQueueResult{QueueUrl: queueUrl}, + Metadata: models.BASE_RESPONSE_METADATA, + } + return http.StatusOK, respStruct +} diff --git a/app/gosqs/create_queue_test.go b/app/gosqs/create_queue_test.go new file mode 100644 index 00000000..427e6622 --- /dev/null +++ b/app/gosqs/create_queue_test.go @@ -0,0 +1,280 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/mitchellh/copystructure" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/Admiral-Piett/goaws/app/interfaces" + + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" + + "github.com/Admiral-Piett/goaws/app/fixtures" + + "github.com/Admiral-Piett/goaws/app" +) + +func TestCreateQueueV1_success(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.CreateQueueRequest) + *v = fixtures.CreateQueueRequest + return true + } + + expectedQueue := &app.Queue{ + Name: fixtures.QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + VisibilityTimeout: 5, + ReceiveMessageWaitTimeSeconds: 4, + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + Duplicates: make(map[string]time.Time), + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, expectedQueue, actualQueue) +} + +func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes.RedrivePolicy = models.RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", fixtures.DeadLetterQueueName), + } + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true + } + + dlq := &app.Queue{ + Name: fixtures.DeadLetterQueueName, + } + app.SyncQueues.Queues[fixtures.DeadLetterQueueName] = dlq + + expectedQueue := &app.Queue{ + Name: fixtures.QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + VisibilityTimeout: 5, + ReceiveMessageWaitTimeSeconds: 4, + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + DeadLetterQueue: dlq, + MaxReceiveCount: 100, + Duplicates: make(map[string]time.Time), + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, expectedQueue, actualQueue) +} + +func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.CreateQueueRequest) + *v = fixtures.CreateQueueRequest + return true + } + + q := &app.Queue{ + Name: fixtures.QueueName, + } + app.SyncQueues.Queues[fixtures.QueueName] = q + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, q, actualQueue) +} + +func TestCreateQueueV1_success_with_no_request_attributes_falls_back_to_default(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes = models.Attributes{} + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true + } + + expectedQueue := &app.Queue{ + Name: fixtures.QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + fixtures.LOCAL_ENVIRONMENT.Region, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + VisibilityTimeout: 0, + ReceiveMessageWaitTimeSeconds: 0, + DelaySeconds: 0, + MaximumMessageSize: 0, + MessageRetentionPeriod: 0, + Duplicates: make(map[string]time.Time), + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.CreateQueueResponse, response) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, expectedQueue, actualQueue) +} + +func TestCreateQueueV1_success_no_configured_region_for_queue_url(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + app.CurrentEnvironment.Region = "" + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes = models.Attributes{} + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := CreateQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + + actualQueue := app.SyncQueues.Queues[fixtures.QueueName] + assert.Equal(t, + fmt.Sprintf("http://%s:%s/%s/%s", + fixtures.LOCAL_ENVIRONMENT.Host, + fixtures.LOCAL_ENVIRONMENT.Port, + fixtures.LOCAL_ENVIRONMENT.AccountID, + fixtures.QueueName, + ), + actualQueue.URL, + ) +} + +func TestCreateQueueV1_request_transformer_error(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := CreateQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestCreateQueueV1_invalid_dead_letter_queue_error(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) + c, _ := dupe.(models.CreateQueueRequest) + c.Attributes.RedrivePolicy = models.RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", "garbage"), + } + + v := resultingStruct.(*models.CreateQueueRequest) + *v = c + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := CreateQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 018e9286..4dc0f9c6 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -13,8 +13,6 @@ import ( "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" - log "github.com/sirupsen/logrus" "github.com/Admiral-Piett/goaws/app" @@ -97,72 +95,6 @@ func PeriodicTasks(d time.Duration, quit <-chan struct{}) { } } -func ListQueues(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - respStruct := app.ListQueuesResponse{} - respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" - respStruct.Metadata = app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"} - respStruct.Result.QueueUrl = make([]string, 0) - queueNamePrefix := req.FormValue("QueueNamePrefix") - - log.Println("Listing Queues") - for _, queue := range app.SyncQueues.Queues { - app.SyncQueues.Lock() - if strings.HasPrefix(queue.Name, queueNamePrefix) { - respStruct.Result.QueueUrl = append(respStruct.Result.QueueUrl, queue.URL) - } - app.SyncQueues.Unlock() - } - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } -} - -func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { - requestBody := models.NewCreateQueueRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) - if !ok { - log.Error("Invalid Request - CreateQueueV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) - } - queueName := requestBody.QueueName - - queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + - "/" + app.CurrentEnvironment.AccountID + "/" + queueName - if app.CurrentEnvironment.Region != "" { - queueUrl = "http://" + app.CurrentEnvironment.Region + "." + app.CurrentEnvironment.Host + ":" + - app.CurrentEnvironment.Port + "/" + app.CurrentEnvironment.AccountID + "/" + queueName - } - queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + queueName - - if _, ok := app.SyncQueues.Queues[queueName]; !ok { - log.Println("Creating Queue:", queueName) - queue := &app.Queue{ - Name: queueName, - URL: queueUrl, - Arn: queueArn, - IsFIFO: app.HasFIFOQueueName(queueName), - EnableDuplicates: app.CurrentEnvironment.EnableDuplicates, - Duplicates: make(map[string]time.Time), - } - if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { - return createErrorResponseV1(err.Error()) - } - app.SyncQueues.Lock() - app.SyncQueues.Queues[queueName] = queue - app.SyncQueues.Unlock() - } - - respStruct := models.CreateQueueResponse{ - Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: models.CreateQueueResult{QueueUrl: queueUrl}, - Metadata: app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}, - } - return http.StatusOK, respStruct -} - func SendMessage(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/xml") req.ParseForm() diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 3511cb64..3d373b00 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -3,7 +3,6 @@ package gosqs import ( "context" "encoding/xml" - "fmt" "net/http" "net/http/httptest" "net/url" @@ -12,17 +11,11 @@ import ( "testing" "time" - "github.com/mitchellh/copystructure" - "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/interfaces" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/stretchr/testify/assert" - "github.com/Admiral-Piett/goaws/app/fixtures" - "github.com/Admiral-Piett/goaws/app" ) @@ -31,343 +24,6 @@ func TestMain(m *testing.M) { m.Run() } -func TestListQueues_POST_NoQueues(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(ListQueues) - - // 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) - } -} - -func TestListQueues_POST_Success(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(ListQueues) - - app.SyncQueues.Queues["foo"] = &app.Queue{Name: "foo", URL: "http://:/queue/foo"} - app.SyncQueues.Queues["bar"] = &app.Queue{Name: "bar", URL: "http://:/queue/bar"} - app.SyncQueues.Queues["foobar"] = &app.Queue{Name: "foobar", URL: "http://:/queue/foobar"} - - handler.ServeHTTP(rr, req) - - 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 := "http://:/queue/bar" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } - - // Filter lists by the given QueueNamePrefix - form := url.Values{} - form.Add("QueueNamePrefix", "fo") - req, _ = http.NewRequest("POST", "/", nil) - req.PostForm = form - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - 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. - unexpected := "http://:/queue/bar" - if strings.Contains(rr.Body.String(), unexpected) { - t.Errorf("handler returned unexpected body: got %v", - rr.Body.String()) - } -} - -func TestCreateQueueV1_success(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - v := resultingStruct.(*models.CreateQueueRequest) - *v = fixtures.CreateQueueRequest - return true - } - - expectedQueue := &app.Queue{ - Name: fixtures.QueueName, - URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.Host, - fixtures.LOCAL_ENVIRONMENT.Port, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - VisibilityTimeout: 5, - ReceiveMessageWaitTimeSeconds: 4, - DelaySeconds: 1, - MaximumMessageSize: 2, - MessageRetentionPeriod: 3, - Duplicates: make(map[string]time.Time), - } - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, response := CreateQueueV1(r) - - assert.Equal(t, http.StatusOK, code) - assert.Equal(t, fixtures.CreateQueueResponse, response) - - actualQueue := app.SyncQueues.Queues[fixtures.QueueName] - assert.Equal(t, expectedQueue, actualQueue) -} - -func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) - c, _ := dupe.(models.CreateQueueRequest) - c.Attributes.RedrivePolicy = models.RedrivePolicy{ - MaxReceiveCount: 100, - DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", fixtures.DeadLetterQueueName), - } - - v := resultingStruct.(*models.CreateQueueRequest) - *v = c - return true - } - - dlq := &app.Queue{ - Name: fixtures.DeadLetterQueueName, - } - app.SyncQueues.Queues[fixtures.DeadLetterQueueName] = dlq - - expectedQueue := &app.Queue{ - Name: fixtures.QueueName, - URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.Host, - fixtures.LOCAL_ENVIRONMENT.Port, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - VisibilityTimeout: 5, - ReceiveMessageWaitTimeSeconds: 4, - DelaySeconds: 1, - MaximumMessageSize: 2, - MessageRetentionPeriod: 3, - DeadLetterQueue: dlq, - MaxReceiveCount: 100, - Duplicates: make(map[string]time.Time), - } - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, response := CreateQueueV1(r) - - assert.Equal(t, http.StatusOK, code) - assert.Equal(t, fixtures.CreateQueueResponse, response) - - actualQueue := app.SyncQueues.Queues[fixtures.QueueName] - assert.Equal(t, expectedQueue, actualQueue) -} - -func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - v := resultingStruct.(*models.CreateQueueRequest) - *v = fixtures.CreateQueueRequest - return true - } - - q := &app.Queue{ - Name: fixtures.QueueName, - } - app.SyncQueues.Queues[fixtures.QueueName] = q - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, response := CreateQueueV1(r) - - assert.Equal(t, http.StatusOK, code) - assert.Equal(t, fixtures.CreateQueueResponse, response) - - actualQueue := app.SyncQueues.Queues[fixtures.QueueName] - assert.Equal(t, q, actualQueue) -} - -func TestCreateQueueV1_success_with_no_request_attributes_falls_back_to_default(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) - c, _ := dupe.(models.CreateQueueRequest) - c.Attributes = models.Attributes{} - - v := resultingStruct.(*models.CreateQueueRequest) - *v = c - return true - } - - expectedQueue := &app.Queue{ - Name: fixtures.QueueName, - URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.Host, - fixtures.LOCAL_ENVIRONMENT.Port, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - VisibilityTimeout: 0, - ReceiveMessageWaitTimeSeconds: 0, - DelaySeconds: 0, - MaximumMessageSize: 0, - MessageRetentionPeriod: 0, - Duplicates: make(map[string]time.Time), - } - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, response := CreateQueueV1(r) - - assert.Equal(t, http.StatusOK, code) - assert.Equal(t, fixtures.CreateQueueResponse, response) - - actualQueue := app.SyncQueues.Queues[fixtures.QueueName] - assert.Equal(t, expectedQueue, actualQueue) -} - -func TestCreateQueueV1_success_no_configured_region_for_queue_url(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - app.CurrentEnvironment.Region = "" - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) - c, _ := dupe.(models.CreateQueueRequest) - c.Attributes = models.Attributes{} - - v := resultingStruct.(*models.CreateQueueRequest) - *v = c - return true - } - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, _ := CreateQueueV1(r) - - assert.Equal(t, http.StatusOK, code) - - actualQueue := app.SyncQueues.Queues[fixtures.QueueName] - assert.Equal(t, - fmt.Sprintf("http://%s:%s/%s/%s", - fixtures.LOCAL_ENVIRONMENT.Host, - fixtures.LOCAL_ENVIRONMENT.Port, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - actualQueue.URL, - ) -} - -func TestCreateQueueV1_request_transformer_error(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - return false - } - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, _ := CreateQueueV1(r) - - assert.Equal(t, http.StatusBadRequest, code) -} - -func TestCreateQueueV1_invalid_dead_letter_queue_error(t *testing.T) { - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - utils.REQUEST_TRANSFORMER = utils.TransformRequest - }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { - dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) - c, _ := dupe.(models.CreateQueueRequest) - c.Attributes.RedrivePolicy = models.RedrivePolicy{ - MaxReceiveCount: 100, - DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", "garbage"), - } - - v := resultingStruct.(*models.CreateQueueRequest) - *v = c - return true - } - - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) - code, _ := CreateQueueV1(r) - - assert.Equal(t, http.StatusBadRequest, code) -} - func TestSendMessage_MaximumMessageSize_Success(t *testing.T) { req, err := http.NewRequest("POST", "/", nil) if err != nil { diff --git a/app/gosqs/list_queues.go b/app/gosqs/list_queues.go new file mode 100644 index 00000000..35a84698 --- /dev/null +++ b/app/gosqs/list_queues.go @@ -0,0 +1,46 @@ +package gosqs + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + log "github.com/sirupsen/logrus" +) + +// TODO - set up MaxResults, NextToken request params +// +// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ListQueues.html +func ListQueuesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewListQueuesRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - ListQueuesV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + log.Info("Listing Queues") + queueUrls := make([]string, 0) + app.SyncQueues.Lock() + for _, queue := range app.SyncQueues.Queues { + if strings.HasPrefix(queue.Name, requestBody.QueueNamePrefix) { + queueUrls = append(queueUrls, queue.URL) + } + } + app.SyncQueues.Unlock() + + respStruct := models.ListQueuesResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + Result: models.ListQueuesResult{ + QueueUrls: queueUrls, + }, + } + + return http.StatusOK, respStruct +} diff --git a/app/gosqs/list_queues_test.go b/app/gosqs/list_queues_test.go new file mode 100644 index 00000000..591e9fe0 --- /dev/null +++ b/app/gosqs/list_queues_test.go @@ -0,0 +1,111 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app/conf" + + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestListQueuesV1_success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.ListQueueRequest) + *v = models.ListQueueRequest{} + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := ListQueuesV1(r) + r1 := response.(models.ListQueuesResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Contains(t, r1.Result.QueueUrls, fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1")) + assert.Contains(t, r1.Result.QueueUrls, fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue2")) + assert.Contains(t, r1.Result.QueueUrls, fmt.Sprintf("%s/%s", fixtures.BASE_URL, "other-queue1")) +} + +func TestListQueuesV1_success_no_queues(t *testing.T) { + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.ListQueueRequest) + *v = models.ListQueueRequest{} + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := ListQueuesV1(r) + r1 := response.(models.ListQueuesResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, r1.Result.QueueUrls, []string{}) +} + +func TestListQueuesV1_success_with_queue_name_prefix(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.ListQueueRequest) + *v = models.ListQueueRequest{QueueNamePrefix: "other"} + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := ListQueuesV1(r) + r1 := response.(models.ListQueuesResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, []string{fmt.Sprintf("%s/%s", fixtures.BASE_URL, "other-queue1")}, r1.Result.QueueUrls) +} + +func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.ListQueueRequest) + *v = models.ListQueueRequest{QueueNamePrefix: "garbage"} + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := ListQueuesV1(r) + r1 := response.(models.ListQueuesResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, []string{}, r1.Result.QueueUrls) +} + +func TestListQueuesV1_request_transformer_error(t *testing.T) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := ListQueuesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/models/models.go b/app/models/models.go index f8b42334..cd6dcd2d 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -10,6 +10,9 @@ import ( log "github.com/sirupsen/logrus" ) +var BASE_XMLNS = "http://queue.amazonaws.com/doc/2012-11-05/" +var BASE_RESPONSE_METADATA = app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"} + func NewCreateQueueRequest() *CreateQueueRequest { return &CreateQueueRequest{ Attributes: Attributes{ @@ -131,6 +134,25 @@ func (r *CreateQueueRequest) SetAttributesFromForm(values url.Values) { return } +func NewListQueuesRequest() *ListQueueRequest { + return &ListQueueRequest{} +} + +type ListQueueRequest struct { + MaxResults int `json:"MaxResults" schema:"MaxResults"` + NextToken string `json:"NextToken" schema:"NextToken"` + QueueNamePrefix string `json:"QueueNamePrefix" schema:"QueueNamePrefix"` +} + +func (r *ListQueueRequest) SetAttributesFromForm(values url.Values) { + maxResults, err := strconv.Atoi(values.Get("MaxResults")) + if err == nil { + r.MaxResults = maxResults + } + r.NextToken = values.Get("NextToken") + r.QueueNamePrefix = values.Get("QueueNamePrefix") +} + // TODO - copy Attributes for SNS // TODO - there are FIFO attributes and things too diff --git a/app/models/models_test.go b/app/models/models_test.go index 1ec5323f..4808a20a 100644 --- a/app/models/models_test.go +++ b/app/models/models_test.go @@ -226,3 +226,31 @@ func TestRedrivePolicy_UnmarshalJSON_invalid_type_returns_error(t *testing.T) { assert.Equal(t, StringToInt(0), r.MaxReceiveCount) assert.Equal(t, "", r.DeadLetterTargetArn) } + +func TestNewListQueuesRequest_SetAttributesFromForm(t *testing.T) { + form := url.Values{} + form.Add("MaxResults", "1") + form.Add("NextToken", "next-token") + form.Add("QueueNamePrefix", "queue-name-prefix") + + lqr := &ListQueueRequest{} + lqr.SetAttributesFromForm(form) + + assert.Equal(t, 1, lqr.MaxResults) + assert.Equal(t, "next-token", lqr.NextToken) + assert.Equal(t, "queue-name-prefix", lqr.QueueNamePrefix) +} + +func TestNewListQueuesRequest_SetAttributesFromForm_invalid_max_results(t *testing.T) { + form := url.Values{} + form.Add("MaxResults", "1.0") + form.Add("NextToken", "next-token") + form.Add("QueueNamePrefix", "queue-name-prefix") + + lqr := &ListQueueRequest{} + lqr.SetAttributesFromForm(form) + + assert.Equal(t, 0, lqr.MaxResults) + assert.Equal(t, "next-token", lqr.NextToken) + assert.Equal(t, "queue-name-prefix", lqr.QueueNamePrefix) +} diff --git a/app/models/responses.go b/app/models/responses.go index 84b21a3e..e977da4f 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -43,3 +43,23 @@ func (r CreateQueueResponse) GetResult() interface{} { func (r CreateQueueResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** List Queues Response */ +type ListQueuesResult struct { + // NOTE: the old XML sdks depend on QueueUrl, and the new JSON ones need QueueUrls + QueueUrls []string `json:"QueueUrls" xml:"QueueUrl"` +} + +type ListQueuesResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result ListQueuesResult `xml:"ListQueuesResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r ListQueuesResponse) GetResult() interface{} { + return r.Result +} + +func (r ListQueuesResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index f7eb19be..f3ef1fb3 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -61,12 +61,11 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // V1 - includes JSON Support (and of course the old XML). var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, } var routingTable = map[string]http.HandlerFunc{ // SQS - "ListQueues": sqs.ListQueues, - //"CreateQueue": sqs.CreateQueue, "GetQueueAttributes": sqs.GetQueueAttributes, "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessage": sqs.SendMessage, diff --git a/app/router/router_test.go b/app/router/router_test.go index 76c8608f..8f3d975b 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -262,11 +262,10 @@ func TestActionHandler_v0_xml(t *testing.T) { defer func() { routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, } routingTable = map[string]http.HandlerFunc{ // SQS - "ListQueues": sqs.ListQueues, - //"CreateQueue": sqs.CreateQueue, "GetQueueAttributes": sqs.GetQueueAttributes, "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessage": sqs.SendMessage, diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 9b51b11c..f8693277 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -1,16 +1,5 @@ package app -/*** List Queues Response */ -type ListQueuesResult struct { - QueueUrl []string `xml:"QueueUrl"` -} - -type ListQueuesResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result ListQueuesResult `xml:"ListQueuesResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Send Message Response */ type SendMessageResult struct { diff --git a/app/utils/tests.go b/app/utils/tests.go index d62c7e92..bac67374 100644 --- a/app/utils/tests.go +++ b/app/utils/tests.go @@ -2,17 +2,11 @@ package utils import ( "bytes" - "context" - "crypto/tls" "encoding/json" - "fmt" "net/http" "net/http/httptest" urlLib "net/url" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/Admiral-Piett/goaws/app" ) @@ -63,22 +57,3 @@ func GenerateRequestInfo(method, url string, body interface{}, isJson bool) (*ht rr := httptest.NewRecorder() return rr, req } - -// GenerateLocalProxyConfig use this to create AWS config that can be plugged into your sqs client, and -// force calls onto a local proxy. This is helpful for testing directly with an HTTP inspection tool -// such as Charles or Proxyman. -func GenerateLocalProxyConfig(proxyPort int) aws.Config { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: false, - }, - } - proxyURL, _ := urlLib.Parse(fmt.Sprintf("http://127.0.0.1:%d", proxyPort)) - tr.Proxy = http.ProxyURL(proxyURL) - client := &http.Client{Transport: tr} - - sdkConfig, _ := config.LoadDefaultConfig(context.TODO(), - config.WithHTTPClient(client), - ) - return sdkConfig -} diff --git a/smoke_tests/main_test.go b/smoke_tests/main_test.go new file mode 100644 index 00000000..865a2c69 --- /dev/null +++ b/smoke_tests/main_test.go @@ -0,0 +1,12 @@ +package smoke_tests + +import ( + "testing" + + "github.com/Admiral-Piett/goaws/app/utils" +) + +func TestMain(m *testing.M) { + utils.InitializeDecoders() + m.Run() +} diff --git a/smoke_tests/sqs_create_queue_test.go b/smoke_tests/sqs_create_queue_test.go index 24989be9..6641f081 100644 --- a/smoke_tests/sqs_create_queue_test.go +++ b/smoke_tests/sqs_create_queue_test.go @@ -54,12 +54,12 @@ func Test_CreateQueueV1_json_no_attributes(t *testing.T) { Status(http.StatusOK). Body().Raw() - exp2 := app.ListQueuesResponse{ + exp2 := models.ListQueuesResponse{ Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Result: models.ListQueuesResult{QueueUrls: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, } - r2 := app.ListQueuesResponse{} + r2 := models.ListQueuesResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, exp2, r2) @@ -85,8 +85,7 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { redriveQueue := "redrive-queue" - //sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) - sdkConfig := utils.GenerateLocalProxyConfig(9090) + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) sdkConfig.BaseEndpoint = aws.String(server.URL) sqsClient := sqs.NewFromConfig(sdkConfig) @@ -94,7 +93,6 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { QueueName: &redriveQueue, }) - // TODO - HERE - Sub-Doc attributes not working for some reason? Check proxyman? sdkResponse, err := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ QueueName: &af.QueueName, Attributes: map[string]string{ @@ -118,12 +116,12 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { Status(http.StatusOK). Body().Raw() - exp2 := app.ListQueuesResponse{ + exp2 := models.ListQueuesResponse{ Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/%s", af.BASE_URL, redriveQueue), fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Result: models.ListQueuesResult{QueueUrls: []string{fmt.Sprintf("%s/%s", af.BASE_URL, redriveQueue), fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, } - r2 := app.ListQueuesResponse{} + r2 := models.ListQueuesResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, exp2, r2) @@ -175,12 +173,12 @@ func Test_CreateQueueV1_json_with_attributes_as_ints(t *testing.T) { Status(http.StatusOK). Body().Raw() - exp2 := app.ListQueuesResponse{ + exp2 := models.ListQueuesResponse{ Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Result: models.ListQueuesResult{QueueUrls: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, } - r2 := app.ListQueuesResponse{} + r2 := models.ListQueuesResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, exp2, r2) @@ -200,7 +198,6 @@ func Test_CreateQueueV1_json_with_attributes_as_ints(t *testing.T) { assert.Equal(t, exp3, r3) } -// TODO - fix broken tests func Test_CreateQueueV1_json_with_attributes_ints_as_strings(t *testing.T) { server := generateServer() defer func() { @@ -312,8 +309,6 @@ func Test_CreateQueueV1_xml_no_attributes(t *testing.T) { utils.ResetResources() }() - utils.InitializeDecoders() - e := httpexpect.Default(t, server.URL) r := e.POST("/"). @@ -338,12 +333,12 @@ func Test_CreateQueueV1_xml_no_attributes(t *testing.T) { Status(http.StatusOK). Body().Raw() - exp2 := app.ListQueuesResponse{ + exp2 := models.ListQueuesResponse{ Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: app.ListQueuesResult{QueueUrl: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, + Result: models.ListQueuesResult{QueueUrls: []string{fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, } - r2 := app.ListQueuesResponse{} + r2 := models.ListQueuesResponse{} xml.Unmarshal([]byte(r), &r2) assert.Equal(t, exp2, r2) @@ -365,8 +360,6 @@ func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { utils.ResetResources() }() - utils.InitializeDecoders() - e := httpexpect.Default(t, server.URL) e.POST("/"). diff --git a/smoke_tests/sqs_list_queues_test.go b/smoke_tests/sqs_list_queues_test.go new file mode 100644 index 00000000..58fd44b2 --- /dev/null +++ b/smoke_tests/sqs_list_queues_test.go @@ -0,0 +1,221 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + "github.com/stretchr/testify/assert" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + "github.com/gavv/httpexpect/v2" +) + +func Test_ListQueues_json_no_queues(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + sdkResponse, err := sqsClient.ListQueues(context.TODO(), &sqs.ListQueuesInput{}) + + assert.Nil(t, err) + assert.Equal(t, []string{}, sdkResponse.QueueUrls) +} + +func Test_ListQueues_json_multiple_queues(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + queueName2 := "new-queue-2" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName2, + }) + queueName3 := "new-queue-3" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName3, + }) + + sdkResponse, err := sqsClient.ListQueues(context.TODO(), &sqs.ListQueuesInput{}) + + assert.Nil(t, err) + + assert.Contains(t, sdkResponse.QueueUrls, fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName)) + assert.Contains(t, sdkResponse.QueueUrls, fmt.Sprintf("%s/new-queue-2", af.BASE_URL)) + assert.Contains(t, sdkResponse.QueueUrls, fmt.Sprintf("%s/new-queue-3", af.BASE_URL)) +} + +func Test_ListQueues_json_multiple_queues_with_queue_name_prefix(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + queueName1 := "old-queue-1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName1, + }) + queueName2 := "new-queue-2" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName2, + }) + queueName3 := "new-queue-3" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName3, + }) + + prefix := "old" + sdkResponse, err := sqsClient.ListQueues(context.TODO(), &sqs.ListQueuesInput{QueueNamePrefix: &prefix}) + + assert.Nil(t, err) + + assert.Equal(t, sdkResponse.QueueUrls, []string{fmt.Sprintf("%s/old-queue-1", af.BASE_URL)}) +} + +func Test_ListQueues_xml_no_queues(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithForm(sf.ListQueuesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + expected := models.ListQueuesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: models.ListQueuesResult{ + QueueUrls: []string(nil), + }, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + response := models.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &response) + assert.Equal(t, expected, response) +} + +func Test_ListQueues_xml_multiple_queues(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + queueName2 := "new-queue-2" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName2, + }) + queueName3 := "new-queue-3" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName3, + }) + + r := e.POST("/"). + WithForm(sf.ListQueuesRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + response := models.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &response) + assert.Contains(t, response.Result.QueueUrls, fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName)) + assert.Contains(t, response.Result.QueueUrls, fmt.Sprintf("%s/new-queue-2", af.BASE_URL)) + assert.Contains(t, response.Result.QueueUrls, fmt.Sprintf("%s/new-queue-3", af.BASE_URL)) +} + +func Test_ListQueues_xml_multiple_queues_with_queue_name_prefix(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + queueName1 := "old-queue-1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName1, + }) + queueName2 := "new-queue-2" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName2, + }) + queueName3 := "new-queue-3" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName3, + }) + + body := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueNamePrefix string `xml:"QueueNamePrefix"` + }{ + Action: "ListQueues", + Version: "2012-11-05", + QueueNamePrefix: "old", + } + r := e.POST("/"). + WithForm(body). + Expect(). + Status(http.StatusOK). + Body().Raw() + + expected := models.ListQueuesResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: models.ListQueuesResult{QueueUrls: []string{fmt.Sprintf("%s/%s", af.BASE_URL, queueName1)}}, + Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, + } + response := models.ListQueuesResponse{} + xml.Unmarshal([]byte(r), &response) + assert.Equal(t, expected, response) +} diff --git a/smoke_tests/utils.go b/smoke_tests/utils.go index 66845a40..c755f546 100644 --- a/smoke_tests/utils.go +++ b/smoke_tests/utils.go @@ -1,7 +1,15 @@ package smoke_tests import ( + "context" + "crypto/tls" + "fmt" + "net/http" "net/http/httptest" + urlLib "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" "github.com/Admiral-Piett/goaws/app/router" ) @@ -9,3 +17,26 @@ import ( func generateServer() *httptest.Server { return httptest.NewServer(router.New()) } + +// GenerateLocalProxyConfig use this to create AWS config that can be plugged into your sqs client, and +// force calls onto a local proxy. This is helpful for testing directly with an HTTP inspection tool +// such as Charles or Proxyman. +// USAGE: +// +// //sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) +// sdkConfig := utils.GenerateLocalProxyConfig(9090) +func GenerateLocalProxyConfig(proxyPort int) aws.Config { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + } + proxyURL, _ := urlLib.Parse(fmt.Sprintf("http://127.0.0.1:%d", proxyPort)) + tr.Proxy = http.ProxyURL(proxyURL) + client := &http.Client{Transport: tr} + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO(), + config.WithHTTPClient(client), + ) + return sdkConfig +} From 8ec7d7efbf8a44e340f3d79f6faf382748461b71 Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Wed, 1 May 2024 17:27:00 -0400 Subject: [PATCH 20/41] Add GetQueueAttributesV1 for JSON support --- app/conf/mock-data/mock-config.yaml | 1 + app/fixtures/sqs.go | 55 ++- app/gosqs/get_queue_attributes.go | 137 ++++++++ app/gosqs/get_queue_attributes_test.go | 178 ++++++++++ app/gosqs/gosqs.go | 100 +----- app/gosqs/gosqs_test.go | 139 -------- app/gosqs/queue_attributes_test.go | 4 +- app/models/models.go | 40 +++ app/models/models_test.go | 31 +- app/models/responses.go | 30 ++ app/models/responses_test.go | 28 ++ app/router/router.go | 6 +- app/router/router_test.go | 17 +- app/sqs_messages.go | 18 - smoke_tests/fixtures/responses.go | 22 +- smoke_tests/sqs_create_queue_test.go | 91 +++-- smoke_tests/sqs_get_queue_attributes_test.go | 335 +++++++++++++++++++ smoke_tests/utils.go | 2 +- 18 files changed, 911 insertions(+), 323 deletions(-) create mode 100644 app/gosqs/get_queue_attributes.go create mode 100644 app/gosqs/get_queue_attributes_test.go create mode 100644 app/models/responses_test.go create mode 100644 smoke_tests/sqs_get_queue_attributes_test.go diff --git a/app/conf/mock-data/mock-config.yaml b/app/conf/mock-data/mock-config.yaml index c042fff2..e5f1c31b 100644 --- a/app/conf/mock-data/mock-config.yaml +++ b/app/conf/mock-data/mock-config.yaml @@ -60,4 +60,5 @@ BaseUnitTests: Queues: # List of queues to create at startup - Name: unit-queue1 # Queue name - Name: unit-queue2 # Queue name + RedrivePolicy: '{"maxReceiveCount": 100, "deadLetterTargetArn":"arn:aws:sqs:us-east-1:100010001000:other-queue1"}' - Name: other-queue1 # Queue name diff --git a/app/fixtures/sqs.go b/app/fixtures/sqs.go index 96f538aa..6fbb9ec8 100644 --- a/app/fixtures/sqs.go +++ b/app/fixtures/sqs.go @@ -39,15 +39,54 @@ var CreateQueueResponse = models.CreateQueueResponse{ Metadata: models.BASE_RESPONSE_METADATA, } -var ListQueuesResult = models.ListQueuesResult{ - QueueUrls: []string{ - fmt.Sprintf("%s/%s", BASE_URL, "unit-queue1"), - fmt.Sprintf("%s/%s", BASE_URL, "unit-queue2"), - }, +var GetQueueAttributesRequest = models.GetQueueAttributesRequest{ + QueueUrl: fmt.Sprintf("%s/unit-queue1", BASE_URL), + AttributeNames: []string{"All"}, } -var ListQueuesResponse = models.ListQueuesResponse{ - Xmlns: models.BASE_XMLNS, - Result: ListQueuesResult, +var GetQueueAttributesResponse = models.GetQueueAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.GetQueueAttributesResult{Attrs: []models.Attribute{ + models.Attribute{ + Name: "DelaySeconds", + Value: "0", + }, + models.Attribute{ + Name: "MaximumMessageSize", + Value: "262144", + }, + models.Attribute{ + Name: "MessageRetentionPeriod", + Value: "345600", + }, + models.Attribute{ + Name: "ReceiveMessageWaitTimeSeconds", + Value: "0", + }, + models.Attribute{ + Name: "VisibilityTimeout", + Value: "30", + }, + models.Attribute{ + Name: "ApproximateNumberOfMessages", + Value: "0", + }, + models.Attribute{ + Name: "ApproximateNumberOfMessagesNotVisible", + Value: "0", + }, + models.Attribute{ + Name: "CreatedTimestamp", + Value: "0000000000", + }, + models.Attribute{ + Name: "LastModifiedTimestamp", + Value: "0000000000", + }, + models.Attribute{ + Name: "QueueArn", + Value: "arn:aws:sqs:region:accountID:unit-queue1", + }, + }}, Metadata: models.BASE_RESPONSE_METADATA, } diff --git a/app/gosqs/get_queue_attributes.go b/app/gosqs/get_queue_attributes.go new file mode 100644 index 00000000..63b1d116 --- /dev/null +++ b/app/gosqs/get_queue_attributes.go @@ -0,0 +1,137 @@ +package gosqs + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/mitchellh/copystructure" + + "github.com/Admiral-Piett/goaws/app/interfaces" + + log "github.com/sirupsen/logrus" +) + +func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewGetQueueAttributesRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - GetQueueAttributesV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + if requestBody.QueueUrl == "" { + log.Error("Missing QueueUrl - GetQueueAttributesV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + requestedAttributes := func() map[string]bool { + attrs := map[string]bool{} + if len(requestBody.AttributeNames) == 0 { + return map[string]bool{"All": true} + } + for _, attr := range requestBody.AttributeNames { + if "All" == attr { + return map[string]bool{"All": true} + } + attrs[attr] = true + } + return attrs + }() + + dupe, _ := copystructure.Copy(models.AVAILABLE_QUEUE_ATTRIBUTES) + includedAttributes, _ := dupe.(map[string]bool) + _, ok = requestedAttributes["All"] + if !ok { + for attr, _ := range includedAttributes { + _, ok := requestedAttributes[attr] + if !ok { + delete(includedAttributes, attr) + } + } + } + + uriSegments := strings.Split(requestBody.QueueUrl, "/") + queueName := uriSegments[len(uriSegments)-1] + + log.Infof("Get Queue Attributes: %s", queueName) + queueAttributes := make([]models.Attribute, 0, 0) + + app.SyncQueues.RLock() + queue, ok := app.SyncQueues.Queues[queueName] + if !ok { + log.Errorf("Get Queue URL: %s queue does not exist!!!", queueName) + app.SyncQueues.RUnlock() + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + if _, ok := includedAttributes["DelaySeconds"]; ok { + attr := models.Attribute{Name: "DelaySeconds", Value: strconv.Itoa(queue.DelaySeconds)} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["MaximumMessageSize"]; ok { + attr := models.Attribute{Name: "MaximumMessageSize", Value: strconv.Itoa(queue.MaximumMessageSize)} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["MessageRetentionPeriod"]; ok { + attr := models.Attribute{Name: "MessageRetentionPeriod", Value: strconv.Itoa(queue.MessageRetentionPeriod)} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["ReceiveMessageWaitTimeSeconds"]; ok { + attr := models.Attribute{Name: "ReceiveMessageWaitTimeSeconds", Value: strconv.Itoa(queue.ReceiveMessageWaitTimeSeconds)} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["VisibilityTimeout"]; ok { + attr := models.Attribute{Name: "VisibilityTimeout", Value: strconv.Itoa(queue.VisibilityTimeout)} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["ApproximateNumberOfMessages"]; ok { + attr := models.Attribute{Name: "ApproximateNumberOfMessages", Value: strconv.Itoa(len(queue.Messages))} + queueAttributes = append(queueAttributes, attr) + } + // TODO - implement + //if _, ok := includedAttributes["ApproximateNumberOfMessagesDelayed"]; ok { + // attr := models.Attribute{Name: "ApproximateNumberOfMessagesDelayed", Value: strconv.Itoa(len(queue.Messages))} + // queueAttributes = append(queueAttributes, attr) + //} + if _, ok := includedAttributes["ApproximateNumberOfMessagesNotVisible"]; ok { + attr := models.Attribute{Name: "ApproximateNumberOfMessagesNotVisible", Value: strconv.Itoa(numberOfHiddenMessagesInQueue(*queue))} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["CreatedTimestamp"]; ok { + attr := models.Attribute{Name: "CreatedTimestamp", Value: "0000000000"} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["LastModifiedTimestamp"]; ok { + attr := models.Attribute{Name: "LastModifiedTimestamp", Value: "0000000000"} + queueAttributes = append(queueAttributes, attr) + } + if _, ok := includedAttributes["QueueArn"]; ok { + attr := models.Attribute{Name: "QueueArn", Value: queue.Arn} + queueAttributes = append(queueAttributes, attr) + } + // TODO - implement + //if _, ok := includedAttributes["Policy"]; ok { + // attr := models.Attribute{Name: "Policy", Value: ""} + // queueAttributes = append(queueAttributes, attr) + //} + //if _, ok := includedAttributes["RedriveAllowPolicy"]; ok { + // attr := models.Attribute{Name: "RedriveAllowPolicy", Value: ""} + // queueAttributes = append(queueAttributes, attr) + //} + if _, ok := includedAttributes["RedrivePolicy"]; ok && queue.DeadLetterQueue != nil { + attr := models.Attribute{Name: "RedrivePolicy", Value: fmt.Sprintf(`{"maxReceiveCount":"%d", "deadLetterTargetArn":"%s"}`, queue.MaxReceiveCount, queue.DeadLetterQueue.Arn)} + queueAttributes = append(queueAttributes, attr) + } + app.SyncQueues.RUnlock() + + respStruct := models.GetQueueAttributesResponse{ + models.BASE_XMLNS, + models.GetQueueAttributesResult{Attrs: queueAttributes}, + models.BASE_RESPONSE_METADATA, + } + return http.StatusOK, respStruct +} diff --git a/app/gosqs/get_queue_attributes_test.go b/app/gosqs/get_queue_attributes_test.go new file mode 100644 index 00000000..ec713551 --- /dev/null +++ b/app/gosqs/get_queue_attributes_test.go @@ -0,0 +1,178 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/mitchellh/copystructure" + + "github.com/Admiral-Piett/goaws/app/conf" + + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestGetQueueAttributesV1_success_all(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.GetQueueAttributesRequest) + *v = fixtures.GetQueueAttributesRequest + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := GetQueueAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.GetQueueAttributesResponse, response) +} + +func TestGetQueueAttributesV1_success_no_request_attrs_returns_all(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.GetQueueAttributesRequest) + *v = models.GetQueueAttributesRequest{ + QueueUrl: "unit-queue1", + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := GetQueueAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, fixtures.GetQueueAttributesResponse, response) +} + +func TestGetQueueAttributesV1_success_all_with_redrive_queue(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.GetQueueAttributesRequest) + *v = models.GetQueueAttributesRequest{ + QueueUrl: "unit-queue2", + AttributeNames: []string{"All"}, + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := GetQueueAttributesV1(r) + + dupe, _ := copystructure.Copy(fixtures.GetQueueAttributesResponse) + expectedResponse, _ := dupe.(models.GetQueueAttributesResponse) + expectedResponse.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", fixtures.BASE_ARN, "unit-queue2") + expectedResponse.Result.Attrs = append(expectedResponse.Result.Attrs, + models.Attribute{ + Name: "RedrivePolicy", + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, fixtures.BASE_ARN, "other-queue1"), + }, + ) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) +} + +func TestGetQueueAttributesV1_success_specific_fields(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.GetQueueAttributesRequest) + *v = models.GetQueueAttributesRequest{ + QueueUrl: fmt.Sprintf("%s/unit-queue1", fixtures.BASE_URL), + AttributeNames: []string{"DelaySeconds"}, + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := GetQueueAttributesV1(r) + + expectedResponse := models.GetQueueAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.GetQueueAttributesResult{Attrs: []models.Attribute{ + models.Attribute{ + Name: "DelaySeconds", + Value: "0", + }, + }}, + Metadata: models.BASE_RESPONSE_METADATA, + } + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) +} + +func TestGetQueueAttributesV1_request_transformer_error(t *testing.T) { + defer func() { + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := GetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestGetQueueAttributesV1_missing_queue_url_in_request_returns_error(t *testing.T) { + defer func() { + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.GetQueueAttributesRequest) + *v = models.GetQueueAttributesRequest{ + QueueUrl: "", + AttributeNames: []string{}, + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := GetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestGetQueueAttributesV1_missing_queue_returns_error(t *testing.T) { + defer func() { + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.GetQueueAttributesRequest) + *v = fixtures.GetQueueAttributesRequest + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := GetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 4dc0f9c6..afbfe4c8 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -383,7 +383,7 @@ func ReceiveMessage(w http.ResponseWriter, req *http.Request) { } } - log.Println("Getting Message from Queue:", queueName) + log.Debugf("Getting Message from Queue:%s", queueName) app.SyncQueues.Lock() // Lock the Queues if len(app.SyncQueues.Queues[queueName].Messages) > 0 { @@ -755,104 +755,6 @@ func GetQueueUrl(w http.ResponseWriter, req *http.Request) { } } -func GetQueueAttributes(w http.ResponseWriter, req *http.Request) { - // Sent response type - w.Header().Set("Content-Type", "application/xml") - // Retrieve FormValues required - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - - attribute_names := map[string]bool{} - - for field, value := range req.Form { - if strings.HasPrefix(field, "AttributeName.") { - attribute_names[value[0]] = true - } - } - - include_attr := func(a string) bool { - if len(attribute_names) == 0 { - return true - } - if _, ok := attribute_names[a]; ok { - return true - } - if _, ok := attribute_names["All"]; ok { - return true - } - return false - } - - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - log.Println("Get Queue Attributes:", queueName) - app.SyncQueues.RLock() - if queue, ok := app.SyncQueues.Queues[queueName]; ok { - // Create, encode/xml and send response - attribs := make([]app.Attribute, 0, 0) - if include_attr("VisibilityTimeout") { - attr := app.Attribute{Name: "VisibilityTimeout", Value: strconv.Itoa(queue.VisibilityTimeout)} - attribs = append(attribs, attr) - } - if include_attr("DelaySeconds") { - attr := app.Attribute{Name: "DelaySeconds", Value: strconv.Itoa(queue.DelaySeconds)} - attribs = append(attribs, attr) - } - if include_attr("ReceiveMessageWaitTimeSeconds") { - attr := app.Attribute{Name: "ReceiveMessageWaitTimeSeconds", Value: strconv.Itoa(queue.ReceiveMessageWaitTimeSeconds)} - attribs = append(attribs, attr) - } - if include_attr("ApproximateNumberOfMessages") { - attr := app.Attribute{Name: "ApproximateNumberOfMessages", Value: strconv.Itoa(len(queue.Messages))} - attribs = append(attribs, attr) - } - if include_attr("ApproximateNumberOfMessagesNotVisible") { - attr := app.Attribute{Name: "ApproximateNumberOfMessagesNotVisible", Value: strconv.Itoa(numberOfHiddenMessagesInQueue(*queue))} - attribs = append(attribs, attr) - } - if include_attr("CreatedTimestamp") { - attr := app.Attribute{Name: "CreatedTimestamp", Value: "0000000000"} - attribs = append(attribs, attr) - } - if include_attr("LastModifiedTimestamp") { - attr := app.Attribute{Name: "LastModifiedTimestamp", Value: "0000000000"} - attribs = append(attribs, attr) - } - if include_attr("QueueArn") { - attr := app.Attribute{Name: "QueueArn", Value: queue.Arn} - attribs = append(attribs, attr) - } - - // TODO - why do we just return the name and NOT the actual ARN here? - deadLetterTargetArn := "" - if queue.DeadLetterQueue != nil { - deadLetterTargetArn = queue.DeadLetterQueue.Name - } - if include_attr("RedrivePolicy") { - attr := app.Attribute{Name: "RedrivePolicy", Value: fmt.Sprintf(`{"maxReceiveCount": "%d", "deadLetterTargetArn":"%s"}`, queue.MaxReceiveCount, deadLetterTargetArn)} - attribs = append(attribs, attr) - } - - result := app.GetQueueAttributesResult{Attrs: attribs} - respStruct := app.GetQueueAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", result, app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } - } else { - log.Println("Get Queue URL:", queueName, ", queue does not exist!!!") - createErrorResponse(w, req, "QueueNotFound") - } - app.SyncQueues.RUnlock() -} - func SetQueueAttributes(w http.ResponseWriter, req *http.Request) { // Sent response type w.Header().Set("Content-Type", "application/xml") diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 3d373b00..84e0314c 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -1750,145 +1750,6 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { } } -func TestGetQueueAttributes_GetAllAttributes(t *testing.T) { - done := make(chan struct{}, 0) - go PeriodicTasks(1*time.Second, done) - - // create a queue - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "get-queue-attributes") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - status, _ := CreateQueueV1(req) - - assert.Equal(t, status, http.StatusOK) - - // get queue attributes - req, err = http.NewRequest("GET", "/queue/get-queue-attributes?Action=GetQueueAttributes&AttributeName.1=All", nil) - if err != nil { - t.Fatal(err) - } - - rr = httptest.NewRecorder() - http.HandlerFunc(GetQueueAttributes).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - resp := app.GetQueueAttributesResponse{} - err = xml.Unmarshal(rr.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("unexpected unmarshal error: %s", err) - } - - hasAttribute := func(attrs []app.Attribute, name string) bool { - for _, attr := range attrs { - if attr.Name == name { - return true - } - } - return false - } - - ok := hasAttribute(resp.Result.Attrs, "VisibilityTimeout") && - hasAttribute(resp.Result.Attrs, "DelaySeconds") && - hasAttribute(resp.Result.Attrs, "ReceiveMessageWaitTimeSeconds") && - hasAttribute(resp.Result.Attrs, "ApproximateNumberOfMessages") && - hasAttribute(resp.Result.Attrs, "ApproximateNumberOfMessagesNotVisible") && - hasAttribute(resp.Result.Attrs, "CreatedTimestamp") && - hasAttribute(resp.Result.Attrs, "LastModifiedTimestamp") && - hasAttribute(resp.Result.Attrs, "QueueArn") && - hasAttribute(resp.Result.Attrs, "RedrivePolicy") - - if !ok { - t.Fatal("handler should return all attributes") - } - - done <- struct{}{} -} - -func TestGetQueueAttributes_GetSelectedAttributes(t *testing.T) { - done := make(chan struct{}, 0) - go PeriodicTasks(1*time.Second, done) - - // create a queue - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "get-queue-attributes") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - status, _ := CreateQueueV1(req) - - assert.Equal(t, status, http.StatusOK) - - // get queue attributes - req, err = http.NewRequest("GET", "/queue/get-queue-attributes?Action=GetQueueAttributes&AttributeName.1=ApproximateNumberOfMessages&AttributeName.2=ApproximateNumberOfMessagesNotVisible&AttributeName.2=ApproximateNumberOfMessagesNotVisible", nil) - if err != nil { - t.Fatal(err) - } - - rr = httptest.NewRecorder() - http.HandlerFunc(GetQueueAttributes).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - resp := app.GetQueueAttributesResponse{} - err = xml.Unmarshal(rr.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("unexpected unmarshal error: %s", err) - } - - hasAttribute := func(attrs []app.Attribute, name string) bool { - for _, attr := range attrs { - if attr.Name == name { - return true - } - } - return false - } - - ok := hasAttribute(resp.Result.Attrs, "ApproximateNumberOfMessages") && - hasAttribute(resp.Result.Attrs, "ApproximateNumberOfMessagesNotVisible") - - if !ok { - t.Fatal("handler should return requested attributes") - } - - ok = !(hasAttribute(resp.Result.Attrs, "VisibilityTimeout") || - hasAttribute(resp.Result.Attrs, "DelaySeconds") || - hasAttribute(resp.Result.Attrs, "ReceiveMessageWaitTimeSeconds") || - hasAttribute(resp.Result.Attrs, "CreatedTimestamp") || - hasAttribute(resp.Result.Attrs, "LastModifiedTimestamp") || - hasAttribute(resp.Result.Attrs, "QueueArn") || - hasAttribute(resp.Result.Attrs, "RedrivePolicy")) - - if !ok { - t.Fatal("handler should return only requested attributes") - } - - done <- struct{}{} -} - func TestCreateErrorResponseV1(t *testing.T) { expectedResponse := models.ErrorResponse{ Result: models.ErrorResult{ diff --git a/app/gosqs/queue_attributes_test.go b/app/gosqs/queue_attributes_test.go index 7401f6b0..da471c78 100644 --- a/app/gosqs/queue_attributes_test.go +++ b/app/gosqs/queue_attributes_test.go @@ -29,7 +29,7 @@ func TestApplyQueueAttributes(t *testing.T) { u.Add("Attribute.2.Value", "60") u.Add("Attribute.3.Name", "Policy") u.Add("Attribute.4.Name", "RedrivePolicy") - u.Add("Attribute.4.Value", `{"maxReceiveCount": "4", "deadLetterTargetArn":"arn:aws:sqs::000000000000:failed-messages"}`) + u.Add("Attribute.4.Value", `{"maxReceiveCount":"4", "deadLetterTargetArn":"arn:aws:sqs::000000000000:failed-messages"}`) u.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") u.Add("Attribute.5.Value", "20") if err := validateAndSetQueueAttributesFromForm(q, u); err != nil { @@ -50,7 +50,7 @@ func TestApplyQueueAttributes(t *testing.T) { q := &app.Queue{VisibilityTimeout: 30} u := url.Values{} u.Add("Attribute.1.Name", "RedrivePolicy") - u.Add("Attribute.1.Value", `{"maxReceiveCount": "4"}`) + u.Add("Attribute.1.Value", `{"maxReceiveCount":"4"}`) err := validateAndSetQueueAttributesFromForm(q, u) if err != ErrInvalidParameterValue { t.Fatalf("expected %s, got %s", ErrInvalidParameterValue, err) diff --git a/app/models/models.go b/app/models/models.go index cd6dcd2d..6e74af48 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -13,6 +13,23 @@ import ( var BASE_XMLNS = "http://queue.amazonaws.com/doc/2012-11-05/" var BASE_RESPONSE_METADATA = app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"} +var AVAILABLE_QUEUE_ATTRIBUTES = map[string]bool{ + "DelaySeconds": true, + "MaximumMessageSize": true, + "MessageRetentionPeriod": true, + "Policy": true, + "ReceiveMessageWaitTimeSeconds": true, + "VisibilityTimeout": true, + "RedrivePolicy": true, + "RedriveAllowPolicy": true, + "ApproximateNumberOfMessages": true, + "ApproximateNumberOfMessagesDelayed": true, + "ApproximateNumberOfMessagesNotVisible": true, + "CreatedTimestamp": true, + "LastModifiedTimestamp": true, + "QueueArn": true, +} + func NewCreateQueueRequest() *CreateQueueRequest { return &CreateQueueRequest{ Attributes: Attributes{ @@ -153,6 +170,29 @@ func (r *ListQueueRequest) SetAttributesFromForm(values url.Values) { r.QueueNamePrefix = values.Get("QueueNamePrefix") } +// TODO - test models and responses +func NewGetQueueAttributesRequest() *GetQueueAttributesRequest { + return &GetQueueAttributesRequest{} +} + +type GetQueueAttributesRequest struct { + QueueUrl string `json:"QueueUrl"` + AttributeNames []string `json:"AttributeNames"` +} + +func (r *GetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { + r.QueueUrl = values.Get("QueueUrl") + // TODO - test me + for i := 1; true; i++ { + attrKey := fmt.Sprintf("AttributeName.%d", i) + attrValue := values.Get(attrKey) + if attrValue == "" { + break + } + r.AttributeNames = append(r.AttributeNames, attrValue) + } +} + // TODO - copy Attributes for SNS // TODO - there are FIFO attributes and things too diff --git a/app/models/models_test.go b/app/models/models_test.go index 4808a20a..a6ac828c 100644 --- a/app/models/models_test.go +++ b/app/models/models_test.go @@ -241,7 +241,7 @@ func TestNewListQueuesRequest_SetAttributesFromForm(t *testing.T) { assert.Equal(t, "queue-name-prefix", lqr.QueueNamePrefix) } -func TestNewListQueuesRequest_SetAttributesFromForm_invalid_max_results(t *testing.T) { +func TestListQueuesRequest_SetAttributesFromForm_invalid_max_results(t *testing.T) { form := url.Values{} form.Add("MaxResults", "1.0") form.Add("NextToken", "next-token") @@ -254,3 +254,32 @@ func TestNewListQueuesRequest_SetAttributesFromForm_invalid_max_results(t *testi assert.Equal(t, "next-token", lqr.NextToken) assert.Equal(t, "queue-name-prefix", lqr.QueueNamePrefix) } + +func TestGetQueueAttributesRequest_SetAttributesFromForm(t *testing.T) { + form := url.Values{} + form.Add("QueueUrl", "queue-url") + form.Add("AttributeName.1", "attribute-1") + form.Add("AttributeName.2", "attribute-2") + + lqr := &GetQueueAttributesRequest{} + lqr.SetAttributesFromForm(form) + + assert.Equal(t, "queue-url", lqr.QueueUrl) + assert.Equal(t, 2, len(lqr.AttributeNames)) + assert.Contains(t, lqr.AttributeNames, "attribute-1") + assert.Contains(t, lqr.AttributeNames, "attribute-2") +} + +func TestGetQueueAttributesRequest_SetAttributesFromForm_skips_invalid_key_sequence(t *testing.T) { + form := url.Values{} + form.Add("QueueUrl", "queue-url") + form.Add("AttributeName.1", "attribute-1") + form.Add("AttributeName.3", "attribute-3") + + lqr := &GetQueueAttributesRequest{} + lqr.SetAttributesFromForm(form) + + assert.Equal(t, "queue-url", lqr.QueueUrl) + assert.Equal(t, 1, len(lqr.AttributeNames)) + assert.Contains(t, lqr.AttributeNames, "attribute-1") +} diff --git a/app/models/responses.go b/app/models/responses.go index e977da4f..836f1114 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -63,3 +63,33 @@ func (r ListQueuesResponse) GetResult() interface{} { func (r ListQueuesResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Get Queue Attributes ***/ +type Attribute struct { + Name string `xml:"Name,omitempty"` + Value string `xml:"Value,omitempty"` +} + +type GetQueueAttributesResult struct { + /* VisibilityTimeout, DelaySeconds, ReceiveMessageWaitTimeSeconds, ApproximateNumberOfMessages + ApproximateNumberOfMessagesNotVisible, CreatedTimestamp, LastModifiedTimestamp, QueueArn */ + Attrs []Attribute `xml:"Attribute,omitempty"` +} + +type GetQueueAttributesResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Result GetQueueAttributesResult `xml:"GetQueueAttributesResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r GetQueueAttributesResponse) GetResult() interface{} { + result := map[string]string{} + for _, attr := range r.Result.Attrs { + result[attr.Name] = attr.Value + } + return map[string]map[string]string{"Attributes": result} +} + +func (r GetQueueAttributesResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/responses_test.go b/app/models/responses_test.go new file mode 100644 index 00000000..d9da276f --- /dev/null +++ b/app/models/responses_test.go @@ -0,0 +1,28 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// NOTE: For now, we're only going to test those methods that do something other than just return a field + +func TestGetQueueAttributesResponse_GetResult(t *testing.T) { + gqa := GetQueueAttributesResponse{ + Result: GetQueueAttributesResult{Attrs: []Attribute{ + {Name: "attribute-name1", Value: "attribute-value1"}, + {Name: "attribute-name2", Value: "attribute-value2"}, + }}, + } + + expectedAttributes := map[string]map[string]string{ + "Attributes": { + "attribute-name1": "attribute-value1", + "attribute-name2": "attribute-value2", + }, + } + result := gqa.GetResult() + + assert.Equal(t, expectedAttributes, result) +} diff --git a/app/router/router.go b/app/router/router.go index f3ef1fb3..a92c46b1 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -60,13 +60,13 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // V1 - includes JSON Support (and of course the old XML). var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "ListQueues": sqs.ListQueuesV1, + "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, + "GetQueueAttributes": sqs.GetQueueAttributesV1, } var routingTable = map[string]http.HandlerFunc{ // SQS - "GetQueueAttributes": sqs.GetQueueAttributes, "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessage": sqs.SendMessage, "SendMessageBatch": sqs.SendMessageBatch, diff --git a/app/router/router_test.go b/app/router/router_test.go index 8f3d975b..cb341752 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -4,12 +4,15 @@ import ( "bytes" "encoding/json" "encoding/xml" + "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/mocks" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -81,7 +84,6 @@ func TestIndexServerhandler_POST_GoodRequest(t *testing.T) { } func TestIndexServerhandler_POST_GoodRequest_With_URL(t *testing.T) { - req, err := http.NewRequest("POST", "/100010001000/local-queue1", nil) if err != nil { t.Fatal(err) @@ -96,6 +98,7 @@ func TestIndexServerhandler_POST_GoodRequest_With_URL(t *testing.T) { form = url.Values{} form.Add("Action", "GetQueueAttributes") + form.Add("QueueUrl", fmt.Sprintf("%s/local-queue1", af.BASE_URL)) req.PostForm = form // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. @@ -105,11 +108,7 @@ func TestIndexServerhandler_POST_GoodRequest_With_URL(t *testing.T) { // directly and pass in our Request and ResponseRecorder. New().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) - } + assert.Equal(t, http.StatusOK, rr.Code) } func TestIndexServerhandler_POST_GoodRequest_With_URL_And_Aws_Json_Protocol(t *testing.T) { @@ -261,12 +260,12 @@ func TestActionHandler_v1_xml(t *testing.T) { func TestActionHandler_v0_xml(t *testing.T) { defer func() { routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "ListQueues": sqs.ListQueuesV1, + "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, + "GetQueueAttributes": sqs.GetQueueAttributesV1, } routingTable = map[string]http.HandlerFunc{ // SQS - "GetQueueAttributes": sqs.GetQueueAttributes, "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessage": sqs.SendMessage, "SendMessageBatch": sqs.SendMessageBatch, diff --git a/app/sqs_messages.go b/app/sqs_messages.go index f8693277..314c89fa 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -129,24 +129,6 @@ type GetQueueUrlResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` } -/*** Get Queue Attributes ***/ -type Attribute struct { - Name string `xml:"Name,omitempty"` - Value string `xml:"Value,omitempty"` -} - -type GetQueueAttributesResult struct { - /* VisibilityTimeout, DelaySeconds, ReceiveMessageWaitTimeSeconds, ApproximateNumberOfMessages - ApproximateNumberOfMessagesNotVisible, CreatedTimestamp, LastModifiedTimestamp, QueueArn */ - Attrs []Attribute `xml:"Attribute,omitempty"` -} - -type GetQueueAttributesResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Result GetQueueAttributesResult `xml:"GetQueueAttributesResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} - type SetQueueAttributesResponse struct { Xmlns string `xml:"xmlns,attr,omitempty"` Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` diff --git a/smoke_tests/fixtures/responses.go b/smoke_tests/fixtures/responses.go index 62ed5a66..0b4fbb5a 100644 --- a/smoke_tests/fixtures/responses.go +++ b/smoke_tests/fixtures/responses.go @@ -3,26 +3,36 @@ package fixtures import ( "fmt" + "github.com/Admiral-Piett/goaws/app/models" + af "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app" ) -var BASE_GET_QUEUE_ATTRIBUTES_RESPONSE = app.GetQueueAttributesResponse{ +var BASE_GET_QUEUE_ATTRIBUTES_RESPONSE = models.GetQueueAttributesResponse{ Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: app.GetQueueAttributesResult{Attrs: []app.Attribute{ + Result: models.GetQueueAttributesResult{Attrs: []models.Attribute{ { - Name: "VisibilityTimeout", + Name: "DelaySeconds", Value: "0", }, { - Name: "DelaySeconds", + Name: "MaximumMessageSize", + Value: "0", + }, + { + Name: "MessageRetentionPeriod", Value: "0", }, { Name: "ReceiveMessageWaitTimeSeconds", Value: "0", }, + { + Name: "VisibilityTimeout", + Value: "0", + }, { Name: "ApproximateNumberOfMessages", Value: "0", @@ -43,10 +53,6 @@ var BASE_GET_QUEUE_ATTRIBUTES_RESPONSE = app.GetQueueAttributesResponse{ Name: "QueueArn", Value: fmt.Sprintf("%s:new-queue-1", af.BASE_ARN), }, - { - Name: "RedrivePolicy", - Value: "{\"maxReceiveCount\": \"0\", \"deadLetterTargetArn\":\"\"}", - }, }}, Metadata: app.ResponseMetadata{RequestId: REQUEST_ID}, } diff --git a/smoke_tests/sqs_create_queue_test.go b/smoke_tests/sqs_create_queue_test.go index 6641f081..1789badc 100644 --- a/smoke_tests/sqs_create_queue_test.go +++ b/smoke_tests/sqs_create_queue_test.go @@ -69,7 +69,7 @@ func Test_CreateQueueV1_json_no_attributes(t *testing.T) { Status(http.StatusOK). Body().Raw() - r3 := app.GetQueueAttributesResponse{} + r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE, r3) } @@ -116,14 +116,14 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { Status(http.StatusOK). Body().Raw() - exp2 := models.ListQueuesResponse{ - Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: models.ListQueuesResult{QueueUrls: []string{fmt.Sprintf("%s/%s", af.BASE_URL, redriveQueue), fmt.Sprintf("%s/new-queue-1", af.BASE_URL)}}, - Metadata: app.ResponseMetadata{RequestId: sf.REQUEST_ID}, - } r2 := models.ListQueuesResponse{} xml.Unmarshal([]byte(r), &r2) - assert.Equal(t, exp2, r2) + + assert.Equal(t, models.BASE_XMLNS, r2.Xmlns) + assert.Equal(t, models.BASE_RESPONSE_METADATA, r2.Metadata) + assert.Equal(t, 2, len(r2.Result.QueueUrls)) + assert.Contains(t, r2.Result.QueueUrls, fmt.Sprintf("%s/%s", af.BASE_URL, redriveQueue)) + assert.Contains(t, r2.Result.QueueUrls, fmt.Sprintf("%s/new-queue-1", af.BASE_URL)) r = e.POST("/"). WithForm(sf.GetQueueAttributesRequestBodyXML). @@ -132,12 +132,18 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { Body().Raw() dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) - exp3, _ := dupe.(app.GetQueueAttributesResponse) - exp3.Result.Attrs[0].Value = "5" - exp3.Result.Attrs[1].Value = "1" - exp3.Result.Attrs[2].Value = "4" - exp3.Result.Attrs[8].Value = fmt.Sprintf(`{"maxReceiveCount": "100", "deadLetterTargetArn":"%s"}`, redriveQueue) - r3 := app.GetQueueAttributesResponse{} + exp3, _ := dupe.(models.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "1" + exp3.Result.Attrs[1].Value = "2" + exp3.Result.Attrs[2].Value = "3" + exp3.Result.Attrs[3].Value = "4" + exp3.Result.Attrs[4].Value = "5" + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + exp3.Result.Attrs = append(exp3.Result.Attrs, models.Attribute{ + Name: "RedrivePolicy", + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + }) + r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, exp3, r3) } @@ -189,11 +195,15 @@ func Test_CreateQueueV1_json_with_attributes_as_ints(t *testing.T) { Body().Raw() dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) - exp3, _ := dupe.(app.GetQueueAttributesResponse) - exp3.Result.Attrs[0].Value = "5" - exp3.Result.Attrs[1].Value = "1" - exp3.Result.Attrs[2].Value = "4" - r3 := app.GetQueueAttributesResponse{} + exp3, _ := dupe.(models.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "1" + exp3.Result.Attrs[1].Value = "2" + exp3.Result.Attrs[2].Value = "3" + exp3.Result.Attrs[3].Value = "4" + exp3.Result.Attrs[4].Value = "5" + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + + r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, exp3, r3) } @@ -292,12 +302,18 @@ func Test_CreateQueueV1_json_with_attributes_ints_as_strings(t *testing.T) { Body().Raw() dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) - exp3, _ := dupe.(app.GetQueueAttributesResponse) - exp3.Result.Attrs[0].Value = "30" - exp3.Result.Attrs[1].Value = "1" - exp3.Result.Attrs[7].Value = fmt.Sprintf("%s:new-string-queue", af.BASE_ARN) - exp3.Result.Attrs[8].Value = "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"new-queue-1\"}" - r3 := app.GetQueueAttributesResponse{} + exp3, _ := dupe.(models.GetQueueAttributesResponse) + exp3.Result.Attrs[0].Value = "1" + exp3.Result.Attrs[1].Value = "2" + exp3.Result.Attrs[2].Value = "3" + exp3.Result.Attrs[3].Value = "0" + exp3.Result.Attrs[4].Value = "30" + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:new-string-queue", af.BASE_ARN) + exp3.Result.Attrs = append(exp3.Result.Attrs, models.Attribute{ + Name: "RedrivePolicy", + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, af.QueueName), + }) + r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, exp3, r3) } @@ -348,7 +364,7 @@ func Test_CreateQueueV1_xml_no_attributes(t *testing.T) { Status(http.StatusOK). Body().Raw() - r3 := app.GetQueueAttributesResponse{} + r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE, r3) } @@ -383,17 +399,17 @@ func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { r := e.POST("/"). WithForm(request). WithFormField("Attribute.1.Name", "VisibilityTimeout"). - WithFormField("Attribute.1.Value", "1"). + WithFormField("Attribute.1.Value", "5"). WithFormField("Attribute.2.Name", "MaximumMessageSize"). WithFormField("Attribute.2.Value", "2"). WithFormField("Attribute.3.Name", "DelaySeconds"). - WithFormField("Attribute.3.Value", "3"). + WithFormField("Attribute.3.Value", "1"). WithFormField("Attribute.4.Name", "MessageRetentionPeriod"). - WithFormField("Attribute.4.Value", "4"). + WithFormField("Attribute.4.Value", "3"). WithFormField("Attribute.5.Name", "Policy"). WithFormField("Attribute.5.Value", "{\"this-is\": \"the-policy\"}"). WithFormField("Attribute.6.Name", "ReceiveMessageWaitTimeSeconds"). - WithFormField("Attribute.6.Value", "5"). + WithFormField("Attribute.6.Value", "4"). WithFormField("Attribute.7.Name", "RedrivePolicy"). WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:new-queue-1\"}", af.BASE_ARN)). WithFormField("Attribute.8.Name", "RedriveAllowPolicy"). @@ -428,13 +444,18 @@ func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { Body().Raw() dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) - exp3, _ := dupe.(app.GetQueueAttributesResponse) + exp3, _ := dupe.(models.GetQueueAttributesResponse) exp3.Result.Attrs[0].Value = "1" - exp3.Result.Attrs[1].Value = "3" - exp3.Result.Attrs[2].Value = "5" - exp3.Result.Attrs[7].Value = fmt.Sprintf("%s:new-queue-2", af.BASE_ARN) - exp3.Result.Attrs[8].Value = "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"new-queue-1\"}" - r3 := app.GetQueueAttributesResponse{} + exp3.Result.Attrs[1].Value = "2" + exp3.Result.Attrs[2].Value = "3" + exp3.Result.Attrs[3].Value = "4" + exp3.Result.Attrs[4].Value = "5" + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:new-queue-2", af.BASE_ARN) + exp3.Result.Attrs = append(exp3.Result.Attrs, models.Attribute{ + Name: "RedrivePolicy", + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, af.QueueName), + }) + r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) assert.Equal(t, exp3, r3) } diff --git a/smoke_tests/sqs_get_queue_attributes_test.go b/smoke_tests/sqs_get_queue_attributes_test.go new file mode 100644 index 00000000..ad7298d3 --- /dev/null +++ b/smoke_tests/sqs_get_queue_attributes_test.go @@ -0,0 +1,335 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app/models" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + "github.com/gavv/httpexpect/v2" + + "github.com/mitchellh/copystructure" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/aws/aws-sdk-go-v2/service/sqs/types" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/stretchr/testify/assert" +) + +func Test_GetQueueAttributes_json_all(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + attributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", + } + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: attributes, + }) + + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &queueUrl, + AttributeNames: []types.QueueAttributeName{"All"}, + }) + + dupe, _ := copystructure.Copy(attributes) + expectedAttributes, _ := dupe.(map[string]string) + expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue) + expectedAttributes["ApproximateNumberOfMessages"] = "0" + expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" + expectedAttributes["CreatedTimestamp"] = "0000000000" + expectedAttributes["LastModifiedTimestamp"] = "0000000000" + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} + +func Test_GetQueueAttributes_json_specific_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + }, + }) + + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &queueUrl, + AttributeNames: []types.QueueAttributeName{"DelaySeconds"}, + }) + + expectedAttributes := map[string]string{ + "DelaySeconds": "1", + } + + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} + +func Test_GetQueueAttributes_json_missing_attribute_name_returns_all(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + attributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", + } + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: attributes, + }) + + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &queueUrl, + }) + + dupe, _ := copystructure.Copy(attributes) + expectedAttributes, _ := dupe.(map[string]string) + expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue) + expectedAttributes["ApproximateNumberOfMessages"] = "0" + expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" + expectedAttributes["CreatedTimestamp"] = "0000000000" + expectedAttributes["LastModifiedTimestamp"] = "0000000000" + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} + +func Test_GetQueueAttributes_xml_all(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + attributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + } + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: attributes, + }) + + r := e.POST("/"). + WithForm(sf.GetQueueAttributesRequestBodyXML). + WithFormField("AttributeName.1", "All"). + Expect(). + Status(http.StatusOK). + Body().Raw() + + dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) + expectedResponse, _ := dupe.(models.GetQueueAttributesResponse) + expectedResponse.Result.Attrs[0].Value = "1" + expectedResponse.Result.Attrs[1].Value = "2" + expectedResponse.Result.Attrs[2].Value = "3" + expectedResponse.Result.Attrs[3].Value = "4" + expectedResponse.Result.Attrs[4].Value = "5" + expectedResponse.Result.Attrs = append(expectedResponse.Result.Attrs, models.Attribute{ + Name: "RedrivePolicy", + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + }) + + r1 := models.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r1) + assert.Equal(t, expectedResponse, r1) +} + +func Test_GetQueueAttributes_xml_select_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + attributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", + } + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: attributes, + }) + + body := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "GetQueueAttributes", + Version: "2012-11-05", + QueueUrl: fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName), + } + + r := e.POST("/"). + WithForm(body). + WithFormField("AttributeName.1", "DelaySeconds"). + Expect(). + Status(http.StatusOK). + Body().Raw() + + expectedResponse := models.GetQueueAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.GetQueueAttributesResult{ + Attrs: []models.Attribute{ + { + Name: "DelaySeconds", + Value: "1", + }, + }, + }, + Metadata: models.BASE_RESPONSE_METADATA, + } + + r1 := models.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r1) + assert.Equal(t, expectedResponse, r1) +} + +func Test_GetQueueAttributes_xml_missing_attribute_name_returns_all(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + attributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + } + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: attributes, + }) + + body := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "GetQueueAttributes", + Version: "2012-11-05", + QueueUrl: fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName), + } + + r := e.POST("/"). + WithForm(body). + Expect(). + Status(http.StatusOK). + Body().Raw() + + dupe, _ := copystructure.Copy(sf.BASE_GET_QUEUE_ATTRIBUTES_RESPONSE) + expectedResponse, _ := dupe.(models.GetQueueAttributesResponse) + expectedResponse.Result.Attrs[0].Value = "1" + expectedResponse.Result.Attrs[1].Value = "2" + expectedResponse.Result.Attrs[2].Value = "3" + expectedResponse.Result.Attrs[3].Value = "4" + expectedResponse.Result.Attrs[4].Value = "5" + expectedResponse.Result.Attrs = append(expectedResponse.Result.Attrs, models.Attribute{ + Name: "RedrivePolicy", + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + }) + + r1 := models.GetQueueAttributesResponse{} + xml.Unmarshal([]byte(r), &r1) + assert.Equal(t, expectedResponse, r1) +} diff --git a/smoke_tests/utils.go b/smoke_tests/utils.go index c755f546..22447c47 100644 --- a/smoke_tests/utils.go +++ b/smoke_tests/utils.go @@ -24,7 +24,7 @@ func generateServer() *httptest.Server { // USAGE: // // //sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) -// sdkConfig := utils.GenerateLocalProxyConfig(9090) +// sdkConfig := GenerateLocalProxyConfig(9090) func GenerateLocalProxyConfig(proxyPort int) aws.Config { tr := &http.Transport{ TLSClientConfig: &tls.Config{ From edaa9b756d7dfca864b3c8c490f581da8e49ae96 Mon Sep 17 00:00:00 2001 From: kojisaikiAtSony Date: Thu, 14 Mar 2024 16:56:22 +0900 Subject: [PATCH 21/41] Migrate SendMessage (+15 squashed commits) Squashed commits: [7342cdd] Update [f1de347] Add tests [28011fb] fix test [a2d30b4] Add SetAttributesFromForm test [8464892] Add xml smoke test [fe667e4] Update UT [14c8798] Update by reviews [29dc396] Add a test about message size exceeding [9083ad0] Add a test [4efb39a] Fix MessageAttributes propagation [85b0f0b] separate unit test [29b45a3] move model into same model files + update a test [06db0ee] add 1 smoke test [031e67e] test commit [f3062f3] Migrate SQS SendMessage w/o smoke test test commit add 1 smoke test move model into same model files + update a test separate unit test Fix MessageAttributes propagation Add a test Add a test about message size exceeding Update by reviews Update UT Add xml smoke test Add SetAttributesFromForm test fix test Add tests Update add TODO comment --- app/gosqs/gosqs.go | 88 +------ app/gosqs/gosqs_test.go | 188 +++----------- app/gosqs/message_attributes.go | 38 --- app/gosqs/send_message.go | 133 ++++++++++ app/gosqs/send_message_test.go | 219 ++++++++++++++++ app/models/models.go | 63 +++++ app/models/models_test.go | 36 +++ app/models/responses.go | 22 ++ app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sqs_messages.go | 18 +- smoke_tests/sqs_send_message_test.go | 373 +++++++++++++++++++++++++++ 12 files changed, 890 insertions(+), 292 deletions(-) create mode 100644 app/gosqs/send_message.go create mode 100644 app/gosqs/send_message_test.go create mode 100644 smoke_tests/sqs_send_message_test.go diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index afbfe4c8..da3c9e56 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -42,7 +42,7 @@ func init() { app.SqsErrors["InvalidVisibilityTimeout"] = err8 err9 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "MessageNotInFlight", Code: "AWS.SimpleQueueService.MessageNotInFlight", Message: "The message referred to isn't in flight."} app.SqsErrors["MessageNotInFlight"] = err9 - err10 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "MessageTooBig", Code: "InvalidMessageContents", Message: "The message size exceeds the limit."} + err10 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "MessageTooBig", Code: "InvalidParameterValue", Message: "The message size exceeds the limit."} app.SqsErrors["MessageTooBig"] = err10 app.SqsErrors[ErrInvalidParameterValue.Type] = *ErrInvalidParameterValue app.SqsErrors[ErrInvalidAttributeValue.Type] = *ErrInvalidAttributeValue @@ -95,92 +95,6 @@ func PeriodicTasks(d time.Duration, quit <-chan struct{}) { } } -func SendMessage(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - req.ParseForm() - messageBody := req.FormValue("MessageBody") - messageGroupID := req.FormValue("MessageGroupId") - messageDeduplicationID := req.FormValue("MessageDeduplicationId") - messageAttributes := extractMessageAttributes(req, "") - - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - if _, ok := app.SyncQueues.Queues[queueName]; !ok { - // Queue does not exist - createErrorResponse(w, req, "QueueNotFound") - return - } - - if app.SyncQueues.Queues[queueName].MaximumMessageSize > 0 && - len(messageBody) > app.SyncQueues.Queues[queueName].MaximumMessageSize { - // Message size is too big - createErrorResponse(w, req, "MessageTooBig") - return - } - - delaySecs := app.SyncQueues.Queues[queueName].DelaySeconds - if mv := req.FormValue("DelaySeconds"); mv != "" { - delaySecs, _ = strconv.Atoi(mv) - } - - log.Println("Putting Message in Queue:", queueName) - msg := app.Message{MessageBody: []byte(messageBody)} - if len(messageAttributes) > 0 { - msg.MessageAttributes = messageAttributes - msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) - } - msg.MD5OfMessageBody = common.GetMD5Hash(messageBody) - msg.Uuid, _ = common.NewUUID() - msg.GroupID = messageGroupID - msg.DeduplicationID = messageDeduplicationID - msg.SentTime = time.Now() - msg.DelaySecs = delaySecs - - app.SyncQueues.Lock() - fifoSeqNumber := "" - if app.SyncQueues.Queues[queueName].IsFIFO { - fifoSeqNumber = app.SyncQueues.Queues[queueName].NextSequenceNumber(messageGroupID) - } - - if !app.SyncQueues.Queues[queueName].IsDuplicate(messageDeduplicationID) { - app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) - } else { - log.Debugf("Message with deduplicationId [%s] in queue [%s] is duplicate ", messageDeduplicationID, queueName) - } - - app.SyncQueues.Queues[queueName].InitDuplicatation(messageDeduplicationID) - app.SyncQueues.Unlock() - log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) - - respStruct := app.SendMessageResponse{ - Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", - Result: app.SendMessageResult{ - MD5OfMessageAttributes: msg.MD5OfMessageAttributes, - MD5OfMessageBody: msg.MD5OfMessageBody, - MessageId: msg.Uuid, - SequenceNumber: fifoSeqNumber, - }, - Metadata: app.ResponseMetadata{ - RequestId: "00000000-0000-0000-0000-000000000000", - }, - } - - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } -} - type SendEntry struct { Id string MessageBody string diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 84e0314c..38a695d1 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -24,119 +24,6 @@ func TestMain(m *testing.M) { m.Run() } -func TestSendMessage_MaximumMessageSize_Success(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["test_max_message_size"] = - &app.Queue{Name: "test_max_message_size", MaximumMessageSize: 100} - - form := url.Values{} - form.Add("Action", "SendMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/test_max_message_size") - form.Add("MessageBody", "test%20message%20body%201") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessage) - - // 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 := "MD5OfMessageBody" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendMessage_MaximumMessageSize_MessageTooBig(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["test_max_message_size"] = - &app.Queue{Name: "test_max_message_size", MaximumMessageSize: 10} - - form := url.Values{} - form.Add("Action", "SendMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/test_max_message_size") - form.Add("MessageBody", "test%20message%20body%201") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessage) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "MessageTooBig" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendQueue_POST_NonExistant(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) - } - - form := url.Values{} - form.Add("Action", "SendMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/NON-EXISTANT") - form.Add("MessageBody", "Test123") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessage) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "NonExistentQueue" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - func TestSendMessageBatch_POST_QueueNotFound(t *testing.T) { req, err := http.NewRequest("POST", "/", nil) if err != nil { @@ -503,7 +390,7 @@ func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", @@ -619,9 +506,9 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -772,9 +659,9 @@ func TestDeadLetterQueue(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -909,9 +796,9 @@ func TestReceiveMessageWaitTimeEnforced(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1014,9 +901,9 @@ func TestReceiveMessage_CanceledByClient(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1173,9 +1060,11 @@ func TestReceiveMessageDelaySeconds(t *testing.T) { form.Add("MessageBody", "1") form.Add("Version", "2012-11-05") req.PostForm = form + rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) - if status := rr.Code; status != http.StatusOK { + status, _ = SendMessageV1(req) + + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1302,9 +1191,9 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1324,9 +1213,9 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1474,7 +1363,6 @@ func TestSendMessage_POST_DuplicatationNotAppliedToStandardQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) assert.Equal(t, status, http.StatusOK) @@ -1492,11 +1380,10 @@ func TestSendMessage_POST_DuplicatationNotAppliedToStandardQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1512,11 +1399,10 @@ func TestSendMessage_POST_DuplicatationNotAppliedToStandardQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1541,7 +1427,6 @@ func TestSendMessage_POST_DuplicatationDisabledOnFifoQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) assert.Equal(t, status, http.StatusOK) @@ -1559,11 +1444,10 @@ func TestSendMessage_POST_DuplicatationDisabledOnFifoQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1579,11 +1463,10 @@ func TestSendMessage_POST_DuplicatationDisabledOnFifoQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1608,7 +1491,6 @@ func TestSendMessage_POST_DuplicatationEnabledOnFifoQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) assert.Equal(t, status, http.StatusOK) @@ -1628,11 +1510,10 @@ func TestSendMessage_POST_DuplicatationEnabledOnFifoQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1648,11 +1529,10 @@ func TestSendMessage_POST_DuplicatationEnabledOnFifoQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) + status, _ = SendMessageV1(req) // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1676,7 +1556,6 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) assert.Equal(t, status, http.StatusOK) @@ -1693,9 +1572,9 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { form.Add("DelaySeconds", "2") form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(SendMessage).ServeHTTP(rr, req) - if status := rr.Code; status != http.StatusOK { + + status, _ = SendMessageV1(req) + if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) } @@ -1710,7 +1589,8 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { form.Add("QueueUrl", "http://localhost:4100/queue/sendmessage-delay") form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() + + rr := httptest.NewRecorder() http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", diff --git a/app/gosqs/message_attributes.go b/app/gosqs/message_attributes.go index fd78a1e4..1badeb7e 100644 --- a/app/gosqs/message_attributes.go +++ b/app/gosqs/message_attributes.go @@ -1,47 +1,9 @@ package gosqs import ( - "fmt" - "net/http" - "github.com/Admiral-Piett/goaws/app" - log "github.com/sirupsen/logrus" ) -func extractMessageAttributes(req *http.Request, prefix string) map[string]app.MessageAttributeValue { - attributes := make(map[string]app.MessageAttributeValue) - if prefix != "" { - prefix += "." - } - - for i := 1; true; i++ { - name := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Name", prefix, i)) - if name == "" { - break - } - - dataType := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Value.DataType", prefix, i)) - if dataType == "" { - log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - continue - } - - // StringListValue and BinaryListValue is currently not implemented - for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { - value := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Value.%s", prefix, i, valueKey)) - if value != "" { - attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} - } - } - - if _, ok := attributes[name]; !ok { - log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - } - } - - return attributes -} - func getMessageAttributeResult(a *app.MessageAttributeValue) *app.ResultMessageAttribute { v := &app.ResultMessageAttributeValue{ DataType: a.DataType, diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go new file mode 100644 index 00000000..701eb636 --- /dev/null +++ b/app/gosqs/send_message.go @@ -0,0 +1,133 @@ +package gosqs + +import ( + "net/http" + "strings" + "time" + + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + + "github.com/Admiral-Piett/goaws/app/utils" + + log "github.com/sirupsen/logrus" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + "github.com/gorilla/mux" +) + +func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewSendMessageRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - CreateQueueV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + messageBody := requestBody.MessageBody + messageGroupID := requestBody.MessageGroupId + messageDeduplicationID := requestBody.MessageDeduplicationId + messageAttributes := requestBody.MessageAttributes + + queueUrl := getQueueFromPath(requestBody.QueueUrl, req.URL.String()) + + queueName := "" + if queueUrl == "" { + // TODO: Remove this query param logic if it's not still valid or something + vars := mux.Vars(req) + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(queueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } + + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + // Queue does not exist + return createErrorResponseV1("QueueNotFound") + } + + if app.SyncQueues.Queues[queueName].MaximumMessageSize > 0 && + len(messageBody) > app.SyncQueues.Queues[queueName].MaximumMessageSize { + // Message size is too big + return createErrorResponseV1("MessageTooBig") + } + + delaySecs := app.SyncQueues.Queues[queueName].DelaySeconds + if requestBody.DelaySeconds != 0 { + delaySecs = requestBody.DelaySeconds + } + + log.Debugf("Putting Message in Queue: [%s]", queueName) + msg := app.Message{MessageBody: []byte(messageBody)} + if len(messageAttributes) > 0 { + oldStyleMessageAttributes := convertToOldMessageAttributeValueStructure(messageAttributes) + msg.MessageAttributes = oldStyleMessageAttributes + msg.MD5OfMessageAttributes = common.HashAttributes(oldStyleMessageAttributes) + } + msg.MD5OfMessageBody = common.GetMD5Hash(messageBody) + msg.Uuid, _ = common.NewUUID() + msg.GroupID = messageGroupID + msg.DeduplicationID = messageDeduplicationID + msg.SentTime = time.Now() + msg.DelaySecs = delaySecs + + app.SyncQueues.Lock() + fifoSeqNumber := "" + if app.SyncQueues.Queues[queueName].IsFIFO { + fifoSeqNumber = app.SyncQueues.Queues[queueName].NextSequenceNumber(messageGroupID) + } + + if !app.SyncQueues.Queues[queueName].IsDuplicate(messageDeduplicationID) { + app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) + } else { + log.Debugf("Message with deduplicationId [%s] in queue [%s] is duplicate ", messageDeduplicationID, queueName) + } + + app.SyncQueues.Queues[queueName].InitDuplicatation(messageDeduplicationID) + app.SyncQueues.Unlock() + log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) + + respStruct := models.SendMessageResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Result: models.SendMessageResult{ + MD5OfMessageAttributes: msg.MD5OfMessageAttributes, + MD5OfMessageBody: msg.MD5OfMessageBody, + MessageId: msg.Uuid, + SequenceNumber: fifoSeqNumber, + }, + Metadata: app.ResponseMetadata{ + RequestId: "00000000-0000-0000-0000-000000000000", + }, + } + + return http.StatusOK, respStruct +} + +// TODO: +// Refactor internal model for MessageAttribute between SendMessage and ReceiveMessage +// from app.MessageAttributeValue(old) to models.MessageAttributeValue(new) and remove this temporary function. +func convertToOldMessageAttributeValueStructure(newValues map[string]models.MessageAttributeValue) map[string]app.MessageAttributeValue { + attributes := make(map[string]app.MessageAttributeValue) + + for name, entry := range newValues { + // StringListValue and BinaryListValue is currently not implemented + // Please refer app/gosqs/message_attributes.go + value := "" + valueKey := "" + if entry.StringValue != "" { + value = entry.StringValue + valueKey = "StringValue" + } else if entry.BinaryValue != "" { + value = entry.BinaryValue + valueKey = "BinaryValue" + } + attributes[name] = app.MessageAttributeValue{ + Name: name, + DataType: entry.DataType, + Value: value, + ValueKey: valueKey, + } + } + + return attributes +} diff --git a/app/gosqs/send_message_test.go b/app/gosqs/send_message_test.go new file mode 100644 index 00000000..a0fc1431 --- /dev/null +++ b/app/gosqs/send_message_test.go @@ -0,0 +1,219 @@ +package gosqs + +import ( + "net/http" + "testing" + "time" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestSendMessageV1_Success(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageRequest{ + QueueUrl: "http://localhost:4200/new-queue-1", + MessageBody: "Test Message", + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.SendMessageRequest) + *v = sendMessageRequest_success + return true + } + + q := &app.Queue{ + Name: "new-queue-1", + MaximumMessageSize: 1024, + } + app.SyncQueues.Queues["new-queue-1"] = q + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageV1(r) + + // Check the queue + assert.Equal(t, 1, len(q.Messages)) + msg := q.Messages[0] + assert.Equal(t, "Test Message", string(msg.MessageBody)) + + // Check the response + assert.Equal(t, http.StatusOK, status) + sendMessageResponse, ok := response.(models.SendMessageResponse) + assert.True(t, ok) + assert.NotEmpty(t, sendMessageResponse.Result.MD5OfMessageBody) + // No FIFO Sequence + assert.Empty(t, sendMessageResponse.Result.SequenceNumber) +} + +func TestSendMessageV1_Success_FIFOQueue(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageRequest{ + QueueUrl: "http://localhost:4200/new-queue-1", + MessageBody: "Test Message", + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.SendMessageRequest) + *v = sendMessageRequest_success + return true + } + + q := &app.Queue{ + Name: "new-queue-1", + MaximumMessageSize: 1024, + IsFIFO: true, + } + app.SyncQueues.Queues["new-queue-1"] = q + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageV1(r) + + // Check the queue + assert.Equal(t, 1, len(q.Messages)) + msg := q.Messages[0] + assert.Equal(t, "Test Message", string(msg.MessageBody)) + + // Check the response + assert.Equal(t, http.StatusOK, status) + sendMessageResponse, ok := response.(models.SendMessageResponse) + assert.True(t, ok) + assert.NotEmpty(t, sendMessageResponse.Result.MD5OfMessageBody) + // Should have FIFO Sequence + assert.NotEmpty(t, sendMessageResponse.Result.SequenceNumber) +} + +func TestSendMessageV1_Success_Deduplication(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageRequest{ + QueueUrl: "http://localhost:4200/new-queue-1", + MessageBody: "Test Message", + MessageDeduplicationId: "1", + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.SendMessageRequest) + *v = sendMessageRequest_success + return true + } + + q := &app.Queue{ + Name: "new-queue-1", + MaximumMessageSize: 1024, + IsFIFO: true, + EnableDuplicates: true, + Duplicates: make(map[string]time.Time), + } + app.SyncQueues.Queues["new-queue-1"] = q + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + status, _ := SendMessageV1(r) + + // Check the queue + assert.Equal(t, 1, len(q.Messages)) + // Check the response + assert.Equal(t, http.StatusOK, status) + + // Send the same message (have DeduplicationId) + status, _ = SendMessageV1(r) + // Response is "success" + assert.Equal(t, http.StatusOK, status) + // Only 1 message should be in the queue + assert.Equal(t, 1, len(q.Messages)) +} + +func TestSendMessageV1_request_transformer_error(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SendMessageV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestSendMessageV1_MaximumMessageSize_MessageTooBig(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageRequest{ + QueueUrl: "http://localhost:4200/new-queue-1", + MessageBody: "Test Message", + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.SendMessageRequest) + *v = sendMessageRequest_success + return true + } + + q := &app.Queue{ + Name: "new-queue-1", + MaximumMessageSize: 1, + } + app.SyncQueues.Queues["new-queue-1"] = q + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageV1(r) + + // Check the response + assert.Equal(t, http.StatusBadRequest, status) + errorResponse, ok := response.(models.ErrorResponse) + assert.True(t, ok) + assert.Equal(t, "MessageTooBig", errorResponse.Result.Type) +} + +func TestSendMessageV1_POST_QueueNonExistant(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageRequest{ + QueueUrl: "http://localhost:4200/new-queue-1", + MessageBody: "Test Message", + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + v := resultingStruct.(*models.SendMessageRequest) + *v = sendMessageRequest_success + return true + } + + // No test queue is added to app.SyncQueues + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageV1(r) + + // Check the status code is what we expect. + assert.Equal(t, http.StatusBadRequest, status) + + // Check the response body is what we expect. + errorResponse, ok := response.(models.ErrorResponse) + assert.True(t, ok) + assert.Equal(t, "Not Found", errorResponse.Result.Type) +} diff --git a/app/models/models.go b/app/models/models.go index 6e74af48..cebdf655 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -193,6 +193,69 @@ func (r *GetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { } } +/*** Send Message Request */ + +func NewSendMessageRequest() *SendMessageRequest { + return &SendMessageRequest{ + MessageAttributes: make(map[string]MessageAttributeValue), + MessageSystemAttributes: make(map[string]MessageAttributeValue), + } +} + +type SendMessageRequest struct { + DelaySeconds int `json:"DelaySeconds" schema:"DelaySeconds"` + // MessageAttributes is custom attributes that users can add on the message as they like. + // Please see: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageAttributes + MessageAttributes map[string]MessageAttributeValue `json:"MessageAttributes" schema:"MessageAttributes"` + MessageBody string `json:"MessageBody" schema:"MessageBody"` + MessageDeduplicationId string `json:"MessageDeduplicationId" schema:"MessageDeduplicationId"` + MessageGroupId string `json:"MessageGroupId" schema:"MessageGroupId"` + // MessageSystemAttributes is custom attributes for AWS services. + // Please see: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageSystemAttributes + // On AWS, the only supported attribute is "AWSTraceHeader" that is for AWS X-Ray. + // Goaws does not contains X-Ray emulation, so currently MessageSystemAttributes is unsupported. + // TODO: Replace with a struct with known attributes "AWSTraceHeader". + MessageSystemAttributes map[string]MessageAttributeValue `json:"MessageSystemAttributes" schema:"MessageSystemAttributes"` + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} +type MessageAttributeValue struct { + BinaryListValues []string `json:"BinaryListValues"` // currently unsupported by AWS + BinaryValue string `json:"BinaryValue"` + DataType string `json:"DataType"` + StringListValues []string `json:"StringListValues"` // currently unsupported by AWS + StringValue string `json:"StringValue"` +} + +func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { + for i := 1; true; i++ { + nameKey := fmt.Sprintf("MessageAttribute.%d.Name", i) + name := values.Get(nameKey) + if name == "" { + break + } + + dataTypeKey := fmt.Sprintf("MessageAttribute.%d.Value.DataType", i) + dataType := values.Get(dataTypeKey) + if dataType == "" { + log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + continue + } + + stringValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.StringValue", i)) + binaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i)) + + r.MessageAttributes[name] = MessageAttributeValue{ + DataType: dataType, + StringValue: stringValue, + BinaryValue: binaryValue, + } + + if _, ok := r.MessageAttributes[name]; !ok { + log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + } + } +} + // TODO - copy Attributes for SNS // TODO - there are FIFO attributes and things too diff --git a/app/models/models_test.go b/app/models/models_test.go index a6ac828c..9df80860 100644 --- a/app/models/models_test.go +++ b/app/models/models_test.go @@ -283,3 +283,39 @@ func TestGetQueueAttributesRequest_SetAttributesFromForm_skips_invalid_key_seque assert.Equal(t, 1, len(lqr.AttributeNames)) assert.Contains(t, lqr.AttributeNames, "attribute-1") } + +func TestSendMessageRequest_SetAttributesFromForm_success(t *testing.T) { + form := url.Values{} + form.Add("MessageAttribute.1.Name", "Attr1") + form.Add("MessageAttribute.1.Value.DataType", "String") + form.Add("MessageAttribute.1.Value.StringValue", "Value1") + form.Add("MessageAttribute.2.Name", "Attr2") + form.Add("MessageAttribute.2.Value.DataType", "Binary") + form.Add("MessageAttribute.2.Value.BinaryValue", "VmFsdWUy") + form.Add("MessageAttribute.3.Name", "") + form.Add("MessageAttribute.3.Value.DataType", "String") + form.Add("MessageAttribute.3.Value.StringValue", "Value") + form.Add("MessageAttribute.4.Name", "Attr4") + form.Add("MessageAttribute.4.Value.DataType", "") + form.Add("MessageAttribute.4.Value.StringValue", "Value4") + + r := &SendMessageRequest{ + MessageAttributes: make(map[string]MessageAttributeValue), + MessageSystemAttributes: make(map[string]MessageAttributeValue), + } + r.SetAttributesFromForm(form) + + assert.Equal(t, 2, len(r.MessageAttributes)) + + assert.NotNil(t, r.MessageAttributes["Attr1"]) + attr1 := r.MessageAttributes["Attr1"] + assert.Equal(t, "String", attr1.DataType) + assert.Equal(t, "Value1", attr1.StringValue) + assert.Equal(t, "", attr1.BinaryValue) + + assert.NotNil(t, r.MessageAttributes["Attr2"]) + attr2 := r.MessageAttributes["Attr2"] + assert.Equal(t, "Binary", attr2.DataType) + assert.Equal(t, "", attr2.StringValue) + assert.Equal(t, "VmFsdWUy", attr2.BinaryValue) +} diff --git a/app/models/responses.go b/app/models/responses.go index 836f1114..dac764a8 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -93,3 +93,25 @@ func (r GetQueueAttributesResponse) GetResult() interface{} { func (r GetQueueAttributesResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Send Message Response */ +type SendMessageResult struct { + MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes"` + MD5OfMessageBody string `xml:"MD5OfMessageBody"` + MessageId string `xml:"MessageId"` + SequenceNumber string `xml:"SequenceNumber"` +} + +type SendMessageResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result SendMessageResult `xml:"SendMessageResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r SendMessageResponse) GetResult() interface{} { + return r.Result +} + +func (r SendMessageResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index a92c46b1..e3168efa 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -63,12 +63,12 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "CreateQueue": sqs.CreateQueueV1, "ListQueues": sqs.ListQueuesV1, "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SendMessage": sqs.SendMessageV1, } var routingTable = map[string]http.HandlerFunc{ // SQS "SetQueueAttributes": sqs.SetQueueAttributes, - "SendMessage": sqs.SendMessage, "SendMessageBatch": sqs.SendMessageBatch, "ReceiveMessage": sqs.ReceiveMessage, "DeleteMessage": sqs.DeleteMessage, diff --git a/app/router/router_test.go b/app/router/router_test.go index cb341752..3d4216a3 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -263,11 +263,11 @@ func TestActionHandler_v0_xml(t *testing.T) { "CreateQueue": sqs.CreateQueueV1, "ListQueues": sqs.ListQueuesV1, "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SendMessage": sqs.SendMessageV1, } routingTable = map[string]http.HandlerFunc{ // SQS "SetQueueAttributes": sqs.SetQueueAttributes, - "SendMessage": sqs.SendMessage, "SendMessageBatch": sqs.SendMessageBatch, "ReceiveMessage": sqs.ReceiveMessage, "DeleteMessage": sqs.DeleteMessage, diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 314c89fa..5885a54c 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -1,18 +1,14 @@ package app -/*** Send Message Response */ - -type SendMessageResult struct { - MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes"` - MD5OfMessageBody string `xml:"MD5OfMessageBody"` - MessageId string `xml:"MessageId"` - SequenceNumber string `xml:"SequenceNumber"` +/*** List Queues Response */ +type ListQueuesResult struct { + QueueUrl []string `xml:"QueueUrl"` } -type SendMessageResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result SendMessageResult `xml:"SendMessageResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` +type ListQueuesResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result ListQueuesResult `xml:"ListQueuesResult"` + Metadata ResponseMetadata `xml:"ResponseMetadata"` } /*** Receive Message Response */ diff --git a/smoke_tests/sqs_send_message_test.go b/smoke_tests/sqs_send_message_test.go new file mode 100644 index 00000000..23e713b3 --- /dev/null +++ b/smoke_tests/sqs_send_message_test.go @@ -0,0 +1,373 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "net/http" + "testing" + "time" + + "github.com/Admiral-Piett/goaws/app" + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_SendMessageV1_json_no_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + targetQueueUrl := sdkResponse.QueueUrl + + // Assert no messages in the queue before sending message + getQueueAttributeOutput, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: targetQueueUrl, + }) + assert.Equal(t, "0", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + // Target test: send a message + targetMessageBody := "Test_SendMessageV1_json_no_attributes" + sendMessageOutput, _ := sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + QueueUrl: targetQueueUrl, + MessageBody: &targetMessageBody, + }) + assert.NotNil(t, sendMessageOutput.MessageId) + + // Assert 1 message in the queue + getQueueAttributeOutput, _ = sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: targetQueueUrl, + }) + assert.Equal(t, "1", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + // Receive message and check attribute + receiveMessageBodyXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "ReceiveMessage", + Version: "2012-11-05", + QueueUrl: *targetQueueUrl, + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(receiveMessageBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r3 := app.ReceiveMessageResponse{} + xml.Unmarshal([]byte(r), &r3) + message := r3.Result.Message[0] + assert.Equal(t, targetMessageBody, string(message.Body)) + assert.Equal(t, 0, len(message.MessageAttributes)) +} + +func Test_SendMessageV1_json_with_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + targetQueueUrl := sdkResponse.QueueUrl + + // Target test: send a message + targetMessageBody := "Test_SendMessageV1_json_with_attributes" + attr1_dataType := "String" + attr1_value := "attr1_value" + attr2_dataType := "Number" + attr2_value := "2" + attr3_dataType := "Binary" + attr3_value := []byte("attr3_value") + sendMessageOutput, _ := sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + QueueUrl: targetQueueUrl, + MessageBody: &targetMessageBody, + DelaySeconds: 1, + MessageAttributes: map[string]sqstypes.MessageAttributeValue{ + "attr1": { + DataType: &attr1_dataType, + StringValue: &attr1_value, + }, + "attr2": { + DataType: &attr2_dataType, + StringValue: &attr2_value, + }, + "attr3": { + DataType: &attr3_dataType, + BinaryValue: attr3_value, + }, + }, + }) + assert.NotNil(t, sendMessageOutput.MessageId) + + // Wait for DelaySecond + time.Sleep(1 * time.Second) + + // Assert 1 message in the queue + getQueueAttributeOutput, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: targetQueueUrl, + }) + assert.Equal(t, "1", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + // Receive message and check attribute + receiveMessageBodyXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "ReceiveMessage", + Version: "2012-11-05", + QueueUrl: *targetQueueUrl, + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(receiveMessageBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r3 := app.ReceiveMessageResponse{} + xml.Unmarshal([]byte(r), &r3) + message := r3.Result.Message[0] + assert.Equal(t, targetMessageBody, string(message.Body)) + assert.Equal(t, 3, len(message.MessageAttributes)) + var attr1, attr2, attr3 app.ResultMessageAttribute + for _, attr := range message.MessageAttributes { + if attr.Name == "attr1" { + attr1 = *attr + } else if attr.Name == "attr2" { + attr2 = *attr + } else if attr.Name == "attr3" { + attr3 = *attr + } + } + assert.Equal(t, "attr1", attr1.Name) + assert.Equal(t, "String", attr1.Value.DataType) + assert.Equal(t, "attr1_value", attr1.Value.StringValue) + assert.Equal(t, "attr2", attr2.Name) + assert.Equal(t, "Number", attr2.Value.DataType) + assert.Equal(t, "2", attr2.Value.StringValue) + assert.Equal(t, "attr3", attr3.Name) + assert.Equal(t, "Binary", attr3.Value.DataType) + assert.Equal(t, "YXR0cjNfdmFsdWU=", attr3.Value.BinaryValue) // base64 encoded "attr3_value" +} + +func Test_SendMessageV1_json_MaximumMessageSize_TooBig(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: map[string]string{ + "MaximumMessageSize": "1", + }, + }) + targetQueueUrl := sdkResponse.QueueUrl + + // Target test: send a message that is bigger than MaximumMessageSize + targetMessageBody := "Test_SendMessageV1_json_no_attributes" + sendMessageOutput, err := sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + QueueUrl: targetQueueUrl, + MessageBody: &targetMessageBody, + }) + assert.Contains(t, err.Error(), "400") + assert.Contains(t, err.Error(), "InvalidParameterValue") + assert.Nil(t, sendMessageOutput) +} + +func Test_SendMessageV1_json_QueueNotExistant(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // Target test: send a message to a queue that is not exist + queueUrl := "http://region.host:port/accountID/not-existant-queue" + targetMessageBody := "Test_SendMessageV1_json_no_attributes" + sendMessageOutput, err := sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + QueueUrl: &queueUrl, + MessageBody: &targetMessageBody, + }) + assert.Contains(t, err.Error(), "400") + assert.Contains(t, err.Error(), "AWS.SimpleQueueService.NonExistentQueue") + assert.Nil(t, sendMessageOutput) +} + +func Test_SendMessageV1_xml_no_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + targetQueueUrl := sdkResponse.QueueUrl + + // Assert no messages in the queue before sending message + getQueueAttributeOutput, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: targetQueueUrl, + }) + assert.Equal(t, "0", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + // Target test: send a message + sendMessageXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + MessageBody string `xml:"MessageBody"` + }{ + Action: "SendMessage", + Version: "2012-11-05", + QueueUrl: *targetQueueUrl, + MessageBody: "Test Message", + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(sendMessageXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r3 := models.SendMessageResult{} + xml.Unmarshal([]byte(r), &r3) + assert.NotNil(t, r3.MessageId) + + // Assert 1 message in the queue + getQueueAttributeOutput, _ = sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: targetQueueUrl, + }) + assert.Equal(t, "1", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) +} + +func Test_SendMessageV1_xml_with_attributes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + targetQueueUrl := sdkResponse.QueueUrl + + // Assert no messages in the queue before sending message + getQueueAttributeOutput, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: targetQueueUrl, + }) + assert.Equal(t, "0", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + // Target test: send a message + sendMessageXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + MessageBody string `xml:"MessageBody"` + DelaySeconds string `xml:"DelaySeconds"` + }{ + Action: "SendMessage", + Version: "2012-11-05", + QueueUrl: *targetQueueUrl, + MessageBody: "Test Message", + DelaySeconds: "1", + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(sendMessageXML). + WithFormField("MessageAttribute.1.Name", "attr1"). + WithFormField("MessageAttribute.1.Value.DataType", "String"). + WithFormField("MessageAttribute.1.Value.StringValue", "attr1_value"). + WithFormField("MessageAttribute.2.Name", "attr2"). + WithFormField("MessageAttribute.2.Value.DataType", "Number"). + WithFormField("MessageAttribute.2.Value.StringValue", "2"). + WithFormField("MessageAttribute.3.Name", "attr3"). + WithFormField("MessageAttribute.3.Value.DataType", "Binary"). + WithFormField("MessageAttribute.3.Value.BinaryValue", "YXR0cjNfdmFsdWU="). + Expect(). + Status(http.StatusOK). + Body().Raw() + r3 := models.SendMessageResult{} + xml.Unmarshal([]byte(r), &r3) + assert.NotNil(t, r3.MessageId) + + // Wait for DelaySecond + time.Sleep(1 * time.Second) + + // Receive message and check attribute + receiveMessageBodyXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "ReceiveMessage", + Version: "2012-11-05", + QueueUrl: *targetQueueUrl, + } + r = e.POST("/"). + WithForm(receiveMessageBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r4 := app.ReceiveMessageResponse{} + xml.Unmarshal([]byte(r), &r4) + message := r4.Result.Message[0] + assert.Equal(t, "Test Message", string(message.Body)) + assert.Equal(t, 3, len(message.MessageAttributes)) + var attr1, attr2, attr3 app.ResultMessageAttribute + for _, attr := range message.MessageAttributes { + if attr.Name == "attr1" { + attr1 = *attr + } else if attr.Name == "attr2" { + attr2 = *attr + } else if attr.Name == "attr3" { + attr3 = *attr + } + } + assert.Equal(t, "attr1", attr1.Name) + assert.Equal(t, "String", attr1.Value.DataType) + assert.Equal(t, "attr1_value", attr1.Value.StringValue) + assert.Equal(t, "attr2", attr2.Name) + assert.Equal(t, "Number", attr2.Value.DataType) + assert.Equal(t, "2", attr2.Value.StringValue) + assert.Equal(t, "attr3", attr3.Name) + assert.Equal(t, "Binary", attr3.Value.DataType) + assert.Equal(t, "YXR0cjNfdmFsdWU=", attr3.Value.BinaryValue) // base64 encoded "attr3_value" +} From 8f356e78ad2947db9f8838e953c0200fe764d7a9 Mon Sep 17 00:00:00 2001 From: Andrew Womeldorf Date: Thu, 23 May 2024 12:43:19 -0500 Subject: [PATCH 22/41] SQS JSON API - ReceiveMessage, ChangeMessageVisibility, DeleteMessage --- app/gosns/gosns.go | 4 +- app/gosqs/change_message_visibility.go | 82 +++ app/gosqs/change_message_visibility_test.go | 48 ++ app/gosqs/create_queue_test.go | 13 +- app/gosqs/delete_message.go | 63 ++ app/gosqs/delete_message_test.go | 38 + app/gosqs/gosqs.go | 286 -------- app/gosqs/gosqs_test.go | 653 ++---------------- app/gosqs/list_queues_test.go | 7 +- app/gosqs/message_attributes.go | 45 +- app/gosqs/receive_message.go | 173 +++++ app/gosqs/receive_message_test.go | 272 ++++++++ app/models/models.go | 64 +- app/models/responses.go | 110 ++- app/router/router.go | 27 +- app/router/router_test.go | 26 +- app/sqs_messages.go | 49 -- smoke_tests/README.md | 8 + smoke_tests/fixtures/requests.go | 46 ++ .../sqs_change_message_visibility_test.go | 96 +++ smoke_tests/sqs_delete_message_test.go | 94 +++ smoke_tests/sqs_receive_message_test.go | 94 +++ smoke_tests/sqs_send_message_test.go | 86 +-- 23 files changed, 1332 insertions(+), 1052 deletions(-) create mode 100644 app/gosqs/change_message_visibility.go create mode 100644 app/gosqs/change_message_visibility_test.go create mode 100644 app/gosqs/delete_message.go create mode 100644 app/gosqs/delete_message_test.go create mode 100644 app/gosqs/receive_message.go create mode 100644 app/gosqs/receive_message_test.go create mode 100644 smoke_tests/README.md create mode 100644 smoke_tests/sqs_change_message_visibility_test.go create mode 100644 smoke_tests/sqs_delete_message_test.go create mode 100644 smoke_tests/sqs_receive_message_test.go diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 52b8e8bc..33de7ffb 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -524,7 +524,7 @@ func Publish(w http.ResponseWriter, req *http.Request) { //Create the response msgId, _ := common.NewUUID() uuid, _ := common.NewUUID() - respStruct := app.PublishResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.PublishResult{MessageId: msgId}, app.ResponseMetadata{RequestId: uuid}} + respStruct := app.PublishResponse{Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", Result: app.PublishResult{MessageId: msgId}, Metadata: app.ResponseMetadata{RequestId: uuid}} SendResponseBack(w, req, respStruct, content) } @@ -701,7 +701,7 @@ func getMessageAttributesFromRequest(req *http.Request) map[string]app.MessageAt for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { value := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Value.%s", i, valueKey)) if value != "" { - attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} + attributes[name] = app.MessageAttributeValue{Name: name, DataType: dataType, Value: value, ValueKey: valueKey} } } diff --git a/app/gosqs/change_message_visibility.go b/app/gosqs/change_message_visibility.go new file mode 100644 index 00000000..adf29a0d --- /dev/null +++ b/app/gosqs/change_message_visibility.go @@ -0,0 +1,82 @@ +package gosqs + +import ( + "net/http" + "strings" + "time" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewChangeMessageVisibilityRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - ChangeMessageVisibilityV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + vars := mux.Vars(req) + + queueUrl := requestBody.QueueUrl + queueName := "" + if queueUrl == "" { + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(queueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } + + receiptHandle := requestBody.ReceiptHandle + + visibilityTimeout := requestBody.VisibilityTimeout + if visibilityTimeout > 43200 { + return createErrorResponseV1("ValidationError") + } + + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + return createErrorResponseV1("QueueNotFound") + } + + app.SyncQueues.Lock() + messageFound := false + for i := 0; i < len(app.SyncQueues.Queues[queueName].Messages); i++ { + queue := app.SyncQueues.Queues[queueName] + msgs := queue.Messages + if msgs[i].ReceiptHandle == receiptHandle { + timeout := app.SyncQueues.Queues[queueName].VisibilityTimeout + if visibilityTimeout == 0 { + msgs[i].ReceiptTime = time.Now().UTC() + msgs[i].ReceiptHandle = "" + msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second) + msgs[i].Retry++ + if queue.MaxReceiveCount > 0 && + queue.DeadLetterQueue != nil && + msgs[i].Retry > queue.MaxReceiveCount { + queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i]) + queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...) + i++ + } + } else { + msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second) + } + messageFound = true + break + } + } + app.SyncQueues.Unlock() + if !messageFound { + return createErrorResponseV1("MessageNotInFlight") + } + + respStruct := models.ChangeMessageVisibilityResult{ + "http://queue.amazonaws.com/doc/2012-11-05/", + app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} + + return http.StatusOK, &respStruct +} diff --git a/app/gosqs/change_message_visibility_test.go b/app/gosqs/change_message_visibility_test.go new file mode 100644 index 00000000..4ffe7ae7 --- /dev/null +++ b/app/gosqs/change_message_visibility_test.go @@ -0,0 +1,48 @@ +package gosqs + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestChangeMessageVisibility_POST_SUCCESS(t *testing.T) { + // create a queue + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + q := &app.Queue{ + Name: "testing", + Messages: []app.Message{{ + MessageBody: []byte("test1"), + ReceiptHandle: "123", + }}, + } + app.SyncQueues.Queues["testing"] = q + + // The default value for the VisibilityTimeout is the zero value of time.Time + assert.Zero(t, q.Messages[0].VisibilityTimeout) + + _, r := utils.GenerateRequestInfo("POST", "/", models.ChangeMessageVisibilityRequest{ + QueueUrl: "http://localhost:4100/queue/testing", + ReceiptHandle: "123", + VisibilityTimeout: 0, + }, true) + status, _ := ChangeMessageVisibilityV1(r) + assert.Equal(t, status, http.StatusOK) + + // Changing the message visibility increments the time.Time by N seconds + // from the current time. + // + // Given that the current time is relative between calling the endpoint and + // the time being set, we can't reliably assert an exact value. So assert + // that the time.Time value is no longer the default zero value. + assert.NotZero(t, q.Messages[0].VisibilityTimeout) +} diff --git a/app/gosqs/create_queue_test.go b/app/gosqs/create_queue_test.go index 427e6622..ecabef7a 100644 --- a/app/gosqs/create_queue_test.go +++ b/app/gosqs/create_queue_test.go @@ -6,18 +6,13 @@ import ( "testing" "time" - "github.com/mitchellh/copystructure" - - "github.com/Admiral-Piett/goaws/app/models" - + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/interfaces" - + "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/utils" + "github.com/mitchellh/copystructure" "github.com/stretchr/testify/assert" - - "github.com/Admiral-Piett/goaws/app/fixtures" - - "github.com/Admiral-Piett/goaws/app" ) func TestCreateQueueV1_success(t *testing.T) { diff --git a/app/gosqs/delete_message.go b/app/gosqs/delete_message.go new file mode 100644 index 00000000..aab988e0 --- /dev/null +++ b/app/gosqs/delete_message.go @@ -0,0 +1,63 @@ +package gosqs + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewDeleteMessageRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - DeleteMessageV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + // Retrieve FormValues required + receiptHandle := requestBody.ReceiptHandle + + // Retrieve FormValues required + queueUrl := requestBody.QueueUrl + queueName := "" + if queueUrl == "" { + vars := mux.Vars(req) + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(queueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } + + log.Info("Deleting Message, Queue:", queueName, ", ReceiptHandle:", receiptHandle) + + // Find queue/message with the receipt handle and delete + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + if _, ok := app.SyncQueues.Queues[queueName]; ok { + for i, msg := range app.SyncQueues.Queues[queueName].Messages { + if msg.ReceiptHandle == receiptHandle { + // Unlock messages for the group + log.Debugf("FIFO Queue %s unlocking group %s:", queueName, msg.GroupID) + app.SyncQueues.Queues[queueName].UnlockGroup(msg.GroupID) + //Delete message from Q + app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages[:i], app.SyncQueues.Queues[queueName].Messages[i+1:]...) + delete(app.SyncQueues.Queues[queueName].Duplicates, msg.DeduplicationID) + + // Create, encode/xml and send response + respStruct := models.DeleteMessageResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} + return 200, &respStruct + } + } + log.Warning("Receipt Handle not found") + } else { + log.Warning("Queue not found") + } + + return createErrorResponseV1("MessageDoesNotExist") +} diff --git a/app/gosqs/delete_message_test.go b/app/gosqs/delete_message_test.go new file mode 100644 index 00000000..c6d98089 --- /dev/null +++ b/app/gosqs/delete_message_test.go @@ -0,0 +1,38 @@ +package gosqs + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestDeleteMessage(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + q := &app.Queue{ + Name: "testing", + Messages: []app.Message{{ + MessageBody: []byte("test1"), + ReceiptHandle: "123", + }}, + } + + app.SyncQueues.Queues["testing"] = q + + _, r := utils.GenerateRequestInfo("POST", "/", models.DeleteMessageRequest{ + QueueUrl: "http://localhost:4100/queue/testing", + ReceiptHandle: "123", + }, true) + status, _ := DeleteMessageV1(r) + + assert.Equal(t, status, http.StatusOK) + assert.Empty(t, q.Messages) +} diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index da3c9e56..33ca4bbf 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -2,7 +2,6 @@ package gosqs import ( "encoding/xml" - "fmt" "net/http" "net/url" "strconv" @@ -230,134 +229,6 @@ func SendMessageBatch(w http.ResponseWriter, req *http.Request) { } } -func ReceiveMessage(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - req.ParseForm() - - waitTimeSeconds := 0 - wts := req.FormValue("WaitTimeSeconds") - if wts != "" { - waitTimeSeconds, _ = strconv.Atoi(wts) - } - maxNumberOfMessages := 1 - mom := req.FormValue("MaxNumberOfMessages") - if mom != "" { - maxNumberOfMessages, _ = strconv.Atoi(mom) - } - - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - if _, ok := app.SyncQueues.Queues[queueName]; !ok { - createErrorResponse(w, req, "QueueNotFound") - return - } - - var messages []*app.ResultMessage - // respMsg := ResultMessage{} - respStruct := app.ReceiveMessageResponse{} - - if waitTimeSeconds == 0 { - app.SyncQueues.RLock() - waitTimeSeconds = app.SyncQueues.Queues[queueName].ReceiveMessageWaitTimeSeconds - app.SyncQueues.RUnlock() - } - - loops := waitTimeSeconds * 10 - for loops > 0 { - app.SyncQueues.RLock() - _, queueFound := app.SyncQueues.Queues[queueName] - if !queueFound { - app.SyncQueues.RUnlock() - createErrorResponse(w, req, "QueueNotFound") - return - } - messageFound := len(app.SyncQueues.Queues[queueName].Messages)-numberOfHiddenMessagesInQueue(*app.SyncQueues.Queues[queueName]) != 0 - app.SyncQueues.RUnlock() - if !messageFound { - continueTimer := time.NewTimer(100 * time.Millisecond) - select { - case <-req.Context().Done(): - continueTimer.Stop() - return // client gave up - case <-continueTimer.C: - continueTimer.Stop() - } - loops-- - } else { - break - } - - } - log.Debugf("Getting Message from Queue:%s", queueName) - - app.SyncQueues.Lock() // Lock the Queues - if len(app.SyncQueues.Queues[queueName].Messages) > 0 { - numMsg := 0 - messages = make([]*app.ResultMessage, 0) - for i := range app.SyncQueues.Queues[queueName].Messages { - if numMsg >= maxNumberOfMessages { - break - } - - if app.SyncQueues.Queues[queueName].Messages[i].ReceiptHandle != "" { - continue - } - - uuid, _ := common.NewUUID() - - msg := &app.SyncQueues.Queues[queueName].Messages[i] - if !msg.IsReadyForReceipt() { - continue - } - msg.ReceiptHandle = msg.Uuid + "#" + uuid - msg.ReceiptTime = time.Now().UTC() - msg.VisibilityTimeout = time.Now().Add(time.Duration(app.SyncQueues.Queues[queueName].VisibilityTimeout) * time.Second) - - if app.SyncQueues.Queues[queueName].IsFIFO { - // If we got messages here it means we have not processed it yet, so get next - if app.SyncQueues.Queues[queueName].IsLocked(msg.GroupID) { - continue - } - // Otherwise lock messages for group ID - app.SyncQueues.Queues[queueName].LockGroup(msg.GroupID) - } - - messages = append(messages, getMessageResult(msg)) - - numMsg++ - } - - // respMsg = ResultMessage{MessageId: messages.Uuid, ReceiptHandle: messages.ReceiptHandle, MD5OfBody: messages.MD5OfMessageBody, Body: messages.MessageBody, MD5OfMessageAttributes: messages.MD5OfMessageAttributes} - respStruct = app.ReceiveMessageResponse{ - "http://queue.amazonaws.com/doc/2012-11-05/", - app.ReceiveMessageResult{ - Message: messages, - }, - app.ResponseMetadata{ - RequestId: "00000000-0000-0000-0000-000000000000", - }, - } - } else { - log.Println("No messages in Queue:", queueName) - respStruct = app.ReceiveMessageResponse{Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", Result: app.ReceiveMessageResult{}, Metadata: app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - } - app.SyncQueues.Unlock() // Unlock the Queues - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } -} - func numberOfHiddenMessagesInQueue(queue app.Queue) int { num := 0 for _, m := range queue.Messages { @@ -368,79 +239,6 @@ func numberOfHiddenMessagesInQueue(queue app.Queue) int { return num } -func ChangeMessageVisibility(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - vars := mux.Vars(req) - - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - queueName := "" - if queueUrl == "" { - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - receiptHandle := req.FormValue("ReceiptHandle") - visibilityTimeout, err := strconv.Atoi(req.FormValue("VisibilityTimeout")) - if err != nil { - createErrorResponse(w, req, "ValidationError") - return - } - if visibilityTimeout > 43200 { - createErrorResponse(w, req, "ValidationError") - return - } - - if _, ok := app.SyncQueues.Queues[queueName]; !ok { - createErrorResponse(w, req, "QueueNotFound") - return - } - - app.SyncQueues.Lock() - messageFound := false - for i := 0; i < len(app.SyncQueues.Queues[queueName].Messages); i++ { - queue := app.SyncQueues.Queues[queueName] - msgs := queue.Messages - if msgs[i].ReceiptHandle == receiptHandle { - timeout := app.SyncQueues.Queues[queueName].VisibilityTimeout - if visibilityTimeout == 0 { - msgs[i].ReceiptTime = time.Now().UTC() - msgs[i].ReceiptHandle = "" - msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second) - msgs[i].Retry++ - if queue.MaxReceiveCount > 0 && - queue.DeadLetterQueue != nil && - msgs[i].Retry > queue.MaxReceiveCount { - queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i]) - queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...) - i++ - } - } else { - msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second) - } - messageFound = true - break - } - } - app.SyncQueues.Unlock() - if !messageFound { - createErrorResponse(w, req, "MessageNotInFlight") - return - } - - respStruct := app.ChangeMessageVisibilityResult{ - "http://queue.amazonaws.com/doc/2012-11-05/", - app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} - - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - createErrorResponse(w, req, "GeneralError") - return - } -} - type DeleteEntry struct { Id string ReceiptHandle string @@ -535,58 +333,6 @@ func DeleteMessageBatch(w http.ResponseWriter, req *http.Request) { } } -func DeleteMessage(w http.ResponseWriter, req *http.Request) { - // Sent response type - w.Header().Set("Content-Type", "application/xml") - - // Retrieve FormValues required - receiptHandle := req.FormValue("ReceiptHandle") - - // Retrieve FormValues required - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - log.Println("Deleting Message, Queue:", queueName, ", ReceiptHandle:", receiptHandle) - - // Find queue/message with the receipt handle and delete - app.SyncQueues.Lock() - if _, ok := app.SyncQueues.Queues[queueName]; ok { - for i, msg := range app.SyncQueues.Queues[queueName].Messages { - if msg.ReceiptHandle == receiptHandle { - // Unlock messages for the group - log.Printf("FIFO Queue %s unlocking group %s:", queueName, msg.GroupID) - app.SyncQueues.Queues[queueName].UnlockGroup(msg.GroupID) - //Delete message from Q - app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages[:i], app.SyncQueues.Queues[queueName].Messages[i+1:]...) - delete(app.SyncQueues.Queues[queueName].Duplicates, msg.DeduplicationID) - - app.SyncQueues.Unlock() - // Create, encode/xml and send response - respStruct := app.DeleteMessageResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } - return - } - } - log.Println("Receipt Handle not found") - } else { - log.Println("Queue not found") - } - app.SyncQueues.Unlock() - - createErrorResponse(w, req, "MessageDoesNotExist") -} - func DeleteQueue(w http.ResponseWriter, req *http.Request) { // Sent response type w.Header().Set("Content-Type", "application/xml") @@ -706,38 +452,6 @@ func SetQueueAttributes(w http.ResponseWriter, req *http.Request) { app.SyncQueues.Unlock() } -func getMessageResult(m *app.Message) *app.ResultMessage { - msgMttrs := []*app.ResultMessageAttribute{} - for _, attr := range m.MessageAttributes { - msgMttrs = append(msgMttrs, getMessageAttributeResult(&attr)) - } - - attrsMap := map[string]string{ - "ApproximateFirstReceiveTimestamp": fmt.Sprintf("%d", m.ReceiptTime.UnixNano()/int64(time.Millisecond)), - "SenderId": app.CurrentEnvironment.AccountID, - "ApproximateReceiveCount": fmt.Sprintf("%d", m.NumberOfReceives+1), - "SentTimestamp": fmt.Sprintf("%d", time.Now().UTC().UnixNano()/int64(time.Millisecond)), - } - - var attrs []*app.ResultAttribute - for k, v := range attrsMap { - attrs = append(attrs, &app.ResultAttribute{ - Name: k, - Value: v, - }) - } - - return &app.ResultMessage{ - MessageId: m.Uuid, - Body: m.MessageBody, - ReceiptHandle: m.ReceiptHandle, - MD5OfBody: common.GetMD5Hash(string(m.MessageBody)), - MD5OfMessageAttributes: m.MD5OfMessageAttributes, - MessageAttributes: msgMttrs, - Attributes: attrs, - } -} - func getQueueFromPath(formVal string, theUrl string) string { if formVal != "" { return formVal diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 38a695d1..3b205249 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -1,8 +1,6 @@ package gosqs import ( - "context" - "encoding/xml" "net/http" "net/http/httptest" "net/url" @@ -11,12 +9,10 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/stretchr/testify/assert" - - "github.com/Admiral-Piett/goaws/app" ) func TestMain(m *testing.M) { @@ -305,48 +301,6 @@ func TestSendMessageBatchToFIFOQueue_POST_Success(t *testing.T) { } } -func TestChangeMessageVisibility_POST_SUCCESS(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} - app.SyncQueues.Queues["testing"].Messages = []app.Message{{ - MessageBody: []byte("test1"), - ReceiptHandle: "123", - }} - - form := url.Values{} - form.Add("Action", "ChangeMessageVisibility") - form.Add("QueueUrl", "http://localhost:4100/queue/testing") - form.Add("VisibilityTimeout", "0") - form.Add("ReceiptHandle", "123") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(ChangeMessageVisibility) - - // 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 \n%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) - } -} - func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { done := make(chan struct{}, 0) go PeriodicTasks(1*time.Second, done) @@ -410,15 +364,8 @@ func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) // try to receive another message. req, err = http.NewRequest("POST", "/", nil) @@ -433,15 +380,8 @@ func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) time.Sleep(2 * time.Second) // message needs to be requeued @@ -457,15 +397,8 @@ func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { req.PostForm = form rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) done <- struct{}{} } @@ -487,9 +420,7 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) - assert.Equal(t, status, http.StatusOK) // send a message @@ -505,9 +436,7 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() status, _ = SendMessageV1(req) - if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) @@ -525,23 +454,10 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, resp := ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) - resp := app.ReceiveMessageResponse{} - err = xml.Unmarshal(rr.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("unexpected unmarshal error: %s", err) - } - receiptHandle := resp.Result.Message[0].ReceiptHandle + receiptHandle := resp.GetResult().(models.ReceiveMessageResult).Messages[0].ReceiptHandle // try to receive another message. req, err = http.NewRequest("POST", "/", nil) @@ -555,16 +471,8 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) // reset message visibility timeout to requeue it req, err = http.NewRequest("POST", "/", nil) @@ -580,14 +488,8 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ChangeMessageVisibility).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 \n%v want %v", - status, http.StatusOK) - } + status, _ = ChangeMessageVisibilityV1(req) + assert.Equal(t, status, http.StatusOK) // message needs to be requeued req, err = http.NewRequest("POST", "/", nil) @@ -601,16 +503,8 @@ func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) done <- struct{}{} } @@ -640,9 +534,7 @@ func TestDeadLetterQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) - assert.Equal(t, status, http.StatusOK) // send a message @@ -658,9 +550,7 @@ func TestDeadLetterQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() status, _ = SendMessageV1(req) - if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) @@ -678,16 +568,8 @@ func TestDeadLetterQueue(t *testing.T) { form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) time.Sleep(2 * time.Second) @@ -699,16 +581,8 @@ func TestDeadLetterQueue(t *testing.T) { req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) time.Sleep(2 * time.Second) // another receive attempt @@ -719,406 +593,13 @@ func TestDeadLetterQueue(t *testing.T) { req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) if len(deadLetterQueue.Messages) == 0 { t.Fatal("expected a message") } } -func TestReceiveMessageWaitTimeEnforced(t *testing.T) { - // create a queue - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "waiting-queue") - form.Add("Attribute.1.Name", "ReceiveMessageWaitTimeSeconds") - form.Add("Attribute.1.Value", "2") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - status, _ := CreateQueueV1(req) - - assert.Equal(t, status, http.StatusOK) - - // receive message ensure delay - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form = url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr = httptest.NewRecorder() - - start := time.Now() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - elapsed := time.Since(start) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } - if elapsed < 2*time.Second { - t.Fatal("handler didn't wait ReceiveMessageWaitTimeSeconds") - } - - // send a message - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form = url.Values{} - form.Add("Action", "SendMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") - form.Add("MessageBody", "1") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr = httptest.NewRecorder() - status, _ = SendMessageV1(req) - - if status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - // receive message - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form = url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr = httptest.NewRecorder() - - start = time.Now() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - elapsed = time.Since(start) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } - if elapsed > 1*time.Second { - t.Fatal("handler waited when message was available, expected not to wait") - } -} - -func TestReceiveMessage_CanceledByClient(t *testing.T) { - // create a queue - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "cancel-queue") - form.Add("Attribute.1.Name", "ReceiveMessageWaitTimeSeconds") - form.Add("Attribute.1.Value", "20") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - status, _ := CreateQueueV1(req) - - assert.Equal(t, status, http.StatusOK) - - var wg sync.WaitGroup - ctx, cancelReceive := context.WithCancel(context.Background()) - - wg.Add(1) - go func() { - defer wg.Done() - // receive message (that will be canceled) - req, err := http.NewRequest("POST", "/", nil) - req = req.WithContext(ctx) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/cancel-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - if ok := strings.Contains(rr.Body.String(), "12345"); ok { - t.Fatal("expecting this ReceiveMessage() to not pickup this message as it should canceled before the Send()") - } - }() - time.Sleep(100 * time.Millisecond) // let enought time for the Receive go to wait mode - cancelReceive() // cancel the first ReceiveMessage(), make sure it will not pickup the sent message below - time.Sleep(5 * time.Millisecond) - - // send a message - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form = url.Values{} - form.Add("Action", "SendMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/cancel-queue") - form.Add("MessageBody", "12345") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr = httptest.NewRecorder() - status, _ = SendMessageV1(req) - - if status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - // receive message - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form = url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/cancel-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr = httptest.NewRecorder() - - start := time.Now() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - elapsed := time.Since(start) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), "12345"); !ok { - t.Fatal("handler should return a message") - } - if elapsed > 1*time.Second { - t.Fatal("handler waited when message was available, expected not to wait") - } - - if timedout := waitTimeout(&wg, 2*time.Second); timedout { - t.Errorf("expected ReceiveMessage() in goroutine to exit quickly due to cancelReceive() called") - } -} - -func TestReceiveMessage_WithConcurrentDeleteQueue(t *testing.T) { - // create a queue - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "waiting-queue") - form.Add("Attribute.1.Name", "ReceiveMessageWaitTimeSeconds") - form.Add("Attribute.1.Value", "1") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - status, _ := CreateQueueV1(req) - - assert.Equal(t, status, http.StatusOK) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - var wg sync.WaitGroup - - wg.Add(1) - go func() { - defer wg.Done() - // receive message - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - // Check the status code is what we expect. - if status := rr.Code; status != http.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "QueueNotFound" - if !strings.Contains(rr.Body.String(), "Not Found") { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - time.Sleep(10 * time.Millisecond) // 10ms to let the ReceiveMessage() block - // delete queue message - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form := url.Values{} - form.Add("Action", "DeleteQueue") - form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - http.HandlerFunc(DeleteQueue).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - }() - - if timedout := waitTimeout(&wg, 2*time.Second); timedout { - t.Errorf("concurrent handlers timeout, expecting both to return within timeout") - } -} - -func TestReceiveMessageDelaySeconds(t *testing.T) { - // create a queue - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form := url.Values{} - form.Add("Action", "CreateQueue") - form.Add("QueueName", "delay-seconds-queue") - form.Add("Attribute.1.Name", "DelaySeconds") - form.Add("Attribute.1.Value", "2") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr := httptest.NewRecorder() - status, _ := CreateQueueV1(req) - - assert.Equal(t, status, http.StatusOK) - - // send a message - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form = url.Values{} - form.Add("Action", "SendMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/delay-seconds-queue") - form.Add("MessageBody", "1") - form.Add("Version", "2012-11-05") - req.PostForm = form - - rr = httptest.NewRecorder() - status, _ = SendMessageV1(req) - - if status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - - // receive message before delay is up - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form = url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/delay-seconds-queue") - form.Add("Version", "2012-11-05") - req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } - - // receive message with wait should return after delay - req, err = http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - form = url.Values{} - form.Add("Action", "ReceiveMessage") - form.Add("QueueUrl", "http://localhost:4100/queue/delay-seconds-queue") - form.Add("WaitTimeSeconds", "10") - form.Add("Version", "2012-11-05") - req.PostForm = form - rr = httptest.NewRecorder() - start := time.Now() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - elapsed := time.Since(start) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if elapsed < 1*time.Second { - t.Errorf("handler didn't wait at all") - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Errorf("handler should return a message") - } - if elapsed > 4*time.Second { - t.Errorf("handler didn't need to wait all WaitTimeSeconds=10, only DelaySeconds=2") - } -} - func TestSetQueueAttributes_POST_QueueNotFound(t *testing.T) { req, err := http.NewRequest("POST", "/", nil) if err != nil { @@ -1171,9 +652,7 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr := httptest.NewRecorder() status, _ := CreateQueueV1(req) - assert.Equal(t, status, http.StatusOK) // send a message @@ -1190,9 +669,7 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() status, _ = SendMessageV1(req) - if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) @@ -1212,9 +689,7 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() status, _ = SendMessageV1(req) - if status != http.StatusOK { t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) @@ -1232,24 +707,12 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Fatal("handler should return a message") - } + status, resp := ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) - resp := app.ReceiveMessageResponse{} - err = xml.Unmarshal(rr.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("unexpected unmarshal error: %s", err) - } - receiptHandleFirst := resp.Result.Message[0].ReceiptHandle - if string(resp.Result.Message[0].Body) != "1" { + result := resp.GetResult().(models.ReceiveMessageResult) + receiptHandleFirst := result.Messages[0].ReceiptHandle + if string(result.Messages[0].Body) != "1" { t.Fatalf("should have received body 1: %s", err) } @@ -1265,16 +728,8 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) if len(app.SyncQueues.Queues["requeue-reset.fifo"].FIFOMessages) != 1 { t.Fatal("there should be only 1 group locked") @@ -1297,14 +752,9 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(DeleteMessage).ServeHTTP(rr, req) + status, _ = DeleteMessageV1(req) + assert.Equal(t, status, http.StatusOK) - // Check the status code is what we expect. - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } if len(app.SyncQueues.Queues["requeue-reset.fifo"].Messages) != 1 { t.Fatal("there should be only 1 message in queue") } @@ -1322,23 +772,15 @@ func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) + status, resp := ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); !ok { + result := resp.GetResult().(models.ReceiveMessageResult) + if len(result.Messages) == 0 { continue } - resp = app.ReceiveMessageResponse{} - err = xml.Unmarshal(rr.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("unexpected unmarshal error: %s", err) - } - if string(resp.Result.Message[0].Body) != "2" { + if string(result.Messages[0].Body) != "2" { t.Fatalf("should have received body 2: %s", err) } break @@ -1589,16 +1031,8 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { form.Add("QueueUrl", "http://localhost:4100/queue/sendmessage-delay") form.Add("Version", "2012-11-05") req.PostForm = form - - rr := httptest.NewRecorder() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - if ok := strings.Contains(rr.Body.String(), ""); ok { - t.Fatal("handler should not return a message") - } + status, _ = ReceiveMessageV1(req) + assert.Equal(t, status, http.StatusOK) // receive message with wait should return after delay req, err = http.NewRequest("POST", "/", nil) @@ -1611,20 +1045,13 @@ func TestSendMessage_POST_DelaySeconds(t *testing.T) { form.Add("WaitTimeSeconds", "10") form.Add("Version", "2012-11-05") req.PostForm = form - rr = httptest.NewRecorder() start := time.Now() - http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) + status, _ = ReceiveMessageV1(req) elapsed := time.Since(start) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } + assert.Equal(t, status, http.StatusOK) if elapsed < 1*time.Second { t.Errorf("handler didn't wait at all") } - if ok := strings.Contains(rr.Body.String(), ""); !ok { - t.Errorf("handler should return a message") - } if elapsed > 4*time.Second { t.Errorf("handler didn't need to wait all WaitTimeSeconds=10, only DelaySeconds=2") } diff --git a/app/gosqs/list_queues_test.go b/app/gosqs/list_queues_test.go index 591e9fe0..f1102dbd 100644 --- a/app/gosqs/list_queues_test.go +++ b/app/gosqs/list_queues_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/Admiral-Piett/goaws/app/conf" - "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/interfaces" "github.com/Admiral-Piett/goaws/app/models" @@ -100,6 +99,12 @@ func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testi } func TestListQueuesV1_request_transformer_error(t *testing.T) { + //conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { return false } diff --git a/app/gosqs/message_attributes.go b/app/gosqs/message_attributes.go index 1badeb7e..e9147715 100644 --- a/app/gosqs/message_attributes.go +++ b/app/gosqs/message_attributes.go @@ -1,11 +1,50 @@ package gosqs import ( + "fmt" + "net/http" + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/models" + log "github.com/sirupsen/logrus" ) -func getMessageAttributeResult(a *app.MessageAttributeValue) *app.ResultMessageAttribute { - v := &app.ResultMessageAttributeValue{ +func extractMessageAttributes(req *http.Request, prefix string) map[string]app.MessageAttributeValue { + attributes := make(map[string]app.MessageAttributeValue) + if prefix != "" { + prefix += "." + } + + for i := 1; true; i++ { + name := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Name", prefix, i)) + if name == "" { + break + } + + dataType := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Value.DataType", prefix, i)) + if dataType == "" { + log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + continue + } + + // StringListValue and BinaryListValue is currently not implemented + for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { + value := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Value.%s", prefix, i, valueKey)) + if value != "" { + attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} + } + } + + if _, ok := attributes[name]; !ok { + log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + } + } + + return attributes +} + +func getMessageAttributeResult(a *app.MessageAttributeValue) *models.ResultMessageAttribute { + v := &models.ResultMessageAttributeValue{ DataType: a.DataType, } @@ -18,7 +57,7 @@ func getMessageAttributeResult(a *app.MessageAttributeValue) *app.ResultMessageA v.StringValue = a.Value } - return &app.ResultMessageAttribute{ + return &models.ResultMessageAttribute{ Name: a.Name, Value: v, } diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go new file mode 100644 index 00000000..0464df3c --- /dev/null +++ b/app/gosqs/receive_message.go @@ -0,0 +1,173 @@ +package gosqs + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewReceiveMessageRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req) + if !ok { + log.Error("Invalid Request - ReceiveMessageV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + maxNumberOfMessages := requestBody.MaxNumberOfMessages + if maxNumberOfMessages == 0 { + maxNumberOfMessages = 1 + } + + queueName := "" + if requestBody.QueueUrl == "" { + vars := mux.Vars(req) + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(requestBody.QueueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } + + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + return createErrorResponseV1("QueueNotFound") + } + + var messages []*models.ResultMessage + respStruct := models.ReceiveMessageResponse{} + + waitTimeSeconds := requestBody.WaitTimeSeconds + if waitTimeSeconds == 0 { + app.SyncQueues.RLock() + waitTimeSeconds = app.SyncQueues.Queues[queueName].ReceiveMessageWaitTimeSeconds + app.SyncQueues.RUnlock() + } + + loops := waitTimeSeconds * 10 + for loops > 0 { + app.SyncQueues.RLock() + _, queueFound := app.SyncQueues.Queues[queueName] + if !queueFound { + app.SyncQueues.RUnlock() + return createErrorResponseV1("QueueNotFound") + } + messageFound := len(app.SyncQueues.Queues[queueName].Messages)-numberOfHiddenMessagesInQueue(*app.SyncQueues.Queues[queueName]) != 0 + app.SyncQueues.RUnlock() + if !messageFound { + continueTimer := time.NewTimer(100 * time.Millisecond) + select { + case <-req.Context().Done(): + continueTimer.Stop() + return http.StatusOK, models.ReceiveMessageResponse{ + "http://queue.amazonaws.com/doc/2012-11-05/", + models.ReceiveMessageResult{}, + app.ResponseMetadata{ + RequestId: "00000000-0000-0000-0000-000000000000", + }, + } + case <-continueTimer.C: + continueTimer.Stop() + } + loops-- + } else { + break + } + + } + log.Debugf("Getting Message from Queue:%s", queueName) + + app.SyncQueues.Lock() // Lock the Queues + defer app.SyncQueues.Unlock() // Unlock the Queues + + if len(app.SyncQueues.Queues[queueName].Messages) > 0 { + numMsg := 0 + messages = make([]*models.ResultMessage, 0) + for i := range app.SyncQueues.Queues[queueName].Messages { + if numMsg >= maxNumberOfMessages { + break + } + + if app.SyncQueues.Queues[queueName].Messages[i].ReceiptHandle != "" { + continue + } + + uuid, _ := common.NewUUID() + + msg := &app.SyncQueues.Queues[queueName].Messages[i] + if !msg.IsReadyForReceipt() { + continue + } + msg.ReceiptHandle = msg.Uuid + "#" + uuid + msg.ReceiptTime = time.Now().UTC() + msg.VisibilityTimeout = time.Now().Add(time.Duration(app.SyncQueues.Queues[queueName].VisibilityTimeout) * time.Second) + + if app.SyncQueues.Queues[queueName].IsFIFO { + // If we got messages here it means we have not processed it yet, so get next + if app.SyncQueues.Queues[queueName].IsLocked(msg.GroupID) { + continue + } + // Otherwise lock messages for group ID + app.SyncQueues.Queues[queueName].LockGroup(msg.GroupID) + } + + messages = append(messages, getMessageResult(msg)) + + numMsg++ + } + + respStruct = models.ReceiveMessageResponse{ + "http://queue.amazonaws.com/doc/2012-11-05/", + models.ReceiveMessageResult{ + Messages: messages, + }, + app.ResponseMetadata{ + RequestId: "00000000-0000-0000-0000-000000000000", + }, + } + } else { + log.Warning("No messages in Queue:", queueName) + respStruct = models.ReceiveMessageResponse{Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", Result: models.ReceiveMessageResult{}, Metadata: app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} + } + + return http.StatusOK, respStruct +} + +func getMessageResult(m *app.Message) *models.ResultMessage { + msgMttrs := []*models.ResultMessageAttribute{} + for _, attr := range m.MessageAttributes { + msgMttrs = append(msgMttrs, getMessageAttributeResult(&attr)) + } + + attrsMap := map[string]string{ + "ApproximateFirstReceiveTimestamp": fmt.Sprintf("%d", m.ReceiptTime.UnixNano()/int64(time.Millisecond)), + "SenderId": app.CurrentEnvironment.AccountID, + "ApproximateReceiveCount": fmt.Sprintf("%d", m.NumberOfReceives+1), + "SentTimestamp": fmt.Sprintf("%d", time.Now().UTC().UnixNano()/int64(time.Millisecond)), + } + + var attrs []*models.ResultAttribute + for k, v := range attrsMap { + attrs = append(attrs, &models.ResultAttribute{ + Name: k, + Value: v, + }) + } + + return &models.ResultMessage{ + MessageId: m.Uuid, + Body: m.MessageBody, + ReceiptHandle: m.ReceiptHandle, + MD5OfBody: common.GetMD5Hash(string(m.MessageBody)), + MD5OfMessageAttributes: m.MD5OfMessageAttributes, + MessageAttributes: msgMttrs, + Attributes: attrs, + } +} diff --git a/app/gosqs/receive_message_test.go b/app/gosqs/receive_message_test.go new file mode 100644 index 00000000..12c6997c --- /dev/null +++ b/app/gosqs/receive_message_test.go @@ -0,0 +1,272 @@ +package gosqs + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + "time" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + q := &app.Queue{ + Name: "waiting-queue", + ReceiveMessageWaitTimeSeconds: 2, + } + app.SyncQueues.Queues["waiting-queue"] = q + + // receive message ensure delay + _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + QueueUrl: "http://localhost:4100/queue/waiting-queue", + }, true) + + start := time.Now() + status, _ := ReceiveMessageV1(r) + elapsed := time.Since(start) + + assert.Equal(t, http.StatusOK, status) + if elapsed < 2*time.Second { + t.Fatal("handler didn't wait ReceiveMessageWaitTimeSeconds") + } + + // mock sending a message + q.Messages = append(q.Messages, app.Message{MessageBody: []byte("1")}) + + // receive message + _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + QueueUrl: "http://localhost:4100/queue/waiting-queue", + }, true) + start = time.Now() + status, resp := ReceiveMessageV1(r) + elapsed = time.Since(start) + + assert.Equal(t, http.StatusOK, status) + if elapsed > 1*time.Second { + t.Fatal("handler waited when message was available, expected not to wait") + } + + assert.Equal(t, "1", string(resp.GetResult().(models.ReceiveMessageResult).Messages[0].Body)) +} + +func TestReceiveMessage_CanceledByClientV1(t *testing.T) { + // create a queue + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + q := &app.Queue{ + Name: "cancel-queue", + ReceiveMessageWaitTimeSeconds: 20, + } + app.SyncQueues.Queues["cancel-queue"] = q + + var wg sync.WaitGroup + ctx, cancelReceive := context.WithCancel(context.Background()) + + wg.Add(1) + go func() { + defer wg.Done() + // receive message (that will be canceled) + _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + QueueUrl: "http://localhost:4100/queue/cancel-queue", + }, true) + r = r.WithContext(ctx) + + status, resp := ReceiveMessageV1(r) + assert.Equal(t, http.StatusOK, status) + + if len(resp.GetResult().(models.ReceiveMessageResult).Messages) != 0 { + t.Fatal("expecting this ReceiveMessage() to not pickup this message as it should canceled before the Send()") + } + }() + time.Sleep(100 * time.Millisecond) // let enought time for the Receive go to wait mode + cancelReceive() // cancel the first ReceiveMessage(), make sure it will not pickup the sent message below + time.Sleep(5 * time.Millisecond) + + // send a message + _, r := utils.GenerateRequestInfo("POST", "/", models.SendMessageRequest{ + QueueUrl: "http://localhost:4100/queue/cancel-queue", + MessageBody: "12345", + }, true) + status, _ := SendMessageV1(r) + if status != http.StatusOK { + t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) + } + + // receive message + _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + QueueUrl: "http://localhost:4100/queue/cancel-queue", + }, true) + start := time.Now() + status, resp := ReceiveMessageV1(r) + assert.Equal(t, http.StatusOK, status) + elapsed := time.Since(start) + + result, ok := resp.GetResult().(models.ReceiveMessageResult) + if !ok { + t.Fatal("handler should return a message") + } + + if len(result.Messages) == 0 || string(result.Messages[0].Body) == "12345\n" { + t.Fatal("handler should return a message") + } + if elapsed > 1*time.Second { + t.Fatal("handler waited when message was available, expected not to wait") + } + + if timedout := waitTimeout(&wg, 2*time.Second); timedout { + t.Errorf("expected ReceiveMessage() in goroutine to exit quickly due to cancelReceive() called") + } +} + +func TestReceiveMessage_WithConcurrentDeleteQueueV1(t *testing.T) { + // create a queue + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + app.SyncQueues.Queues["waiting-queue"] = &app.Queue{ + Name: "waiting-queue", + ReceiveMessageWaitTimeSeconds: 1, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + // receive message + _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + QueueUrl: "http://localhost:4100/queue/waiting-queue", + }, true) + status, resp := ReceiveMessageV1(r) + assert.Equal(t, http.StatusBadRequest, status) + + // Check the response body is what we expect. + expected := "QueueNotFound" + result := resp.GetResult().(models.ErrorResult) + if result.Type != "Not Found" { + t.Errorf("handler returned unexpected body: got %v want %v", + result.Message, expected) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(10 * time.Millisecond) // 10ms to let the ReceiveMessage() block + + // delete queue message + form := url.Values{} + form.Add("Action", "DeleteQueue") + form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") + form.Add("Version", "2012-11-05") + _, r := utils.GenerateRequestInfo("POST", "/", form, false) + + rr := httptest.NewRecorder() + http.HandlerFunc(DeleteQueue).ServeHTTP(rr, r) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got \n%v want %v", + status, http.StatusOK) + } + }() + + if timedout := waitTimeout(&wg, 2*time.Second); timedout { + t.Errorf("concurrent handlers timeout, expecting both to return within timeout") + } +} + +func TestReceiveMessageDelaySecondsV1(t *testing.T) { + // create a queue + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + q := &app.Queue{ + Name: "delay-seconds-queue", + DelaySeconds: 2, + } + app.SyncQueues.Queues["delay-seconds-queue"] = q + + // send a message + _, r := utils.GenerateRequestInfo("POST", "/", models.SendMessageRequest{ + QueueUrl: "http://localhost:4100/queue/delay-seconds-queue", + MessageBody: "1", + }, true) + status, _ := SendMessageV1(r) + if status != http.StatusOK { + t.Errorf("handler returned wrong status code: got \n%v want %v", status, http.StatusOK) + } + + // receive message before delay is up + _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{QueueUrl: "http://localhost:4100/queue/delay-seconds-queue"}, true) + status, _ = ReceiveMessageV1(r) + assert.Equal(t, http.StatusOK, status) + + // receive message with wait should return after delay + _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + QueueUrl: "http://localhost:4100/queue/delay-seconds-queue", + WaitTimeSeconds: 10, + }, true) + start := time.Now() + status, _ = ReceiveMessageV1(r) + elapsed := time.Since(start) + assert.Equal(t, http.StatusOK, status) + if elapsed < 1*time.Second { + t.Errorf("handler didn't wait at all") + } + if elapsed > 4*time.Second { + t.Errorf("handler didn't need to wait all WaitTimeSeconds=10, only DelaySeconds=2") + } +} + +func TestReceiveMessageAttributesV1(t *testing.T) { + // create a queue + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + utils.ResetApp() + }() + + q := &app.Queue{Name: "waiting-queue"} + app.SyncQueues.Queues["waiting-queue"] = q + + // send a message + q.Messages = append(q.Messages, app.Message{ + MessageBody: []byte("1"), + MessageAttributes: map[string]app.MessageAttributeValue{ + "TestMessageAttrName": { + Name: "TestMessageAttrName", + DataType: "String", + Value: "TestMessageAttrValue", + }, + }, + }) + + // receive message + _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{QueueUrl: "http://localhost:4100/queue/waiting-queue"}, true) + status, resp := ReceiveMessageV1(r) + result := resp.GetResult().(models.ReceiveMessageResult) + + assert.Equal(t, http.StatusOK, status) + assert.Equal(t, "1", string(result.Messages[0].Body)) + assert.Equal(t, 1, len(result.Messages[0].MessageAttributes)) + assert.Equal(t, "TestMessageAttrName", result.Messages[0].MessageAttributes[0].Name) + assert.Equal(t, "String", result.Messages[0].MessageAttributes[0].Value.DataType) + assert.Equal(t, "TestMessageAttrValue", result.Messages[0].MessageAttributes[0].Value.StringValue) +} diff --git a/app/models/models.go b/app/models/models.go index cebdf655..5c10832e 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -30,18 +30,6 @@ var AVAILABLE_QUEUE_ATTRIBUTES = map[string]bool{ "QueueArn": true, } -func NewCreateQueueRequest() *CreateQueueRequest { - return &CreateQueueRequest{ - Attributes: Attributes{ - DelaySeconds: 0, - MaximumMessageSize: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize), - MessageRetentionPeriod: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod), - ReceiveMessageWaitTimeSeconds: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds), - VisibilityTimeout: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout), - }, - } -} - type CreateQueueRequest struct { QueueName string `json:"QueueName" schema:"QueueName"` Attributes Attributes `json:"Attributes" schema:"Attribute"` @@ -295,3 +283,55 @@ func (r *RedrivePolicy) UnmarshalJSON(data []byte) error { } return nil } + +func NewReceiveMessageRequest() *ReceiveMessageRequest { + return &ReceiveMessageRequest{} +} + +type ReceiveMessageRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` + AttributeNames []string `json:"AttributeNames" schema:"AttributeNames"` + MessageSystemAttributeNames []string `json:"MessageSystemAttributeNames" schema:"MessageSystemAttributeNames"` + MessageAttributeNames []string `json:"MessageAttributeNames" schema:"MessageAttributeNames"` + MaxNumberOfMessages int `json:"MaxNumberOfMessages" schema:"MaxNumberOfMessages"` + VisibilityTimeout int `json:"VisibilityTimeout" schema:"VisibilityTimeout"` + WaitTimeSeconds int `json:"WaitTimeSeconds" schema:"WaitTimeSeconds"` + ReceiveRequestAttemptId string `json:"ReceiveRequestAttemptId" schema:"ReceiveRequestAttemptId"` +} + +func (r *ReceiveMessageRequest) SetAttributesFromForm(values url.Values) {} + +func NewCreateQueueRequest() *CreateQueueRequest { + return &CreateQueueRequest{ + Attributes: Attributes{ + DelaySeconds: 0, + MaximumMessageSize: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize), + MessageRetentionPeriod: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod), + ReceiveMessageWaitTimeSeconds: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds), + VisibilityTimeout: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout), + }, + } +} + +func NewChangeMessageVisibilityRequest() *ChangeMessageVisibilityRequest { + return &ChangeMessageVisibilityRequest{} +} + +type ChangeMessageVisibilityRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` + ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` + VisibilityTimeout int `json:"VisibilityTimeout" schema:"VisibilityTimeout"` +} + +func (r *ChangeMessageVisibilityRequest) SetAttributesFromForm(values url.Values) {} + +func NewDeleteMessageRequest() *DeleteMessageRequest { + return &DeleteMessageRequest{} +} + +type DeleteMessageRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` + ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` +} + +func (r *DeleteMessageRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/models/responses.go b/app/models/responses.go index dac764a8..133357f7 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -1,6 +1,12 @@ package models -import "github.com/Admiral-Piett/goaws/app" +import ( + "encoding/json" + + "github.com/Admiral-Piett/goaws/app" + "github.com/aws/aws-sdk-go-v2/aws" + sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" +) // NOTE: Every response in here MUST implement the `AbstractResponseBody` interface in order to be used // in `encodeResponse` @@ -25,6 +31,94 @@ func (r ErrorResponse) GetRequestId() string { return r.RequestId } +/*** Receive Message Response */ +type ReceiveMessageResult struct { + Messages []*ResultMessage `json:"Messages" xml:"Message,omitempty"` +} + +type ReceiveMessageResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result ReceiveMessageResult `xml:"ReceiveMessageResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r ReceiveMessageResponse) GetResult() interface{} { + return r.Result +} + +func (r ReceiveMessageResponse) GetRequestId() string { + return r.Metadata.RequestId +} + +type ResultMessage struct { + MessageId string `xml:"MessageId,omitempty"` + ReceiptHandle string `xml:"ReceiptHandle,omitempty"` + MD5OfBody string `xml:"MD5OfBody,omitempty"` + Body []byte `xml:"Body,omitempty"` + MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes,omitempty"` + MessageAttributes []*ResultMessageAttribute `xml:"MessageAttribute,omitempty"` + Attributes []*ResultAttribute `xml:"Attribute,omitempty"` +} + +// MarshalJSON first converts the ResultMessage to the shape which the SDKs +// expect. When receiving a response from the JSON API, it apparently expects +// Attributes and MessageAttributes to be maps, rather than the former slice +// shape. +func (r *ResultMessage) MarshalJSON() ([]byte, error) { + m := &sqstypes.Message{ + MessageId: &r.MessageId, + ReceiptHandle: &r.ReceiptHandle, + MD5OfBody: &r.MD5OfBody, + Body: aws.String(string(r.Body)), + MD5OfMessageAttributes: &r.MD5OfMessageAttributes, + Attributes: map[string]string{}, + MessageAttributes: map[string]sqstypes.MessageAttributeValue{}, + } + + for _, attr := range r.Attributes { + m.Attributes[attr.Name] = attr.Value + } + + for _, attr := range r.MessageAttributes { + m.MessageAttributes[attr.Name] = sqstypes.MessageAttributeValue{ + DataType: &attr.Value.DataType, + StringValue: &attr.Value.StringValue, + BinaryValue: []byte(attr.Value.BinaryValue), + } + } + + return json.Marshal(m) +} + +type ResultMessageAttributeValue struct { + DataType string `xml:"DataType,omitempty"` + StringValue string `xml:"StringValue,omitempty"` + BinaryValue string `xml:"BinaryValue,omitempty"` +} + +type ResultMessageAttribute struct { + Name string `xml:"Name,omitempty"` + Value *ResultMessageAttributeValue `xml:"Value,omitempty"` +} + +type ResultAttribute struct { + Name string `xml:"Name,omitempty"` + Value string `xml:"Value,omitempty"` +} + +type ChangeMessageVisibilityResult struct { + Xmlns string `xml:"xmlns,attr"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r ChangeMessageVisibilityResult) GetResult() interface{} { + return nil +} + +func (r ChangeMessageVisibilityResult) GetRequestId() string { + return r.Metadata.RequestId +} + /*** Create Queue Response */ type CreateQueueResult struct { QueueUrl string `json:"QueueUrl" xml:"QueueUrl"` @@ -115,3 +209,17 @@ func (r SendMessageResponse) GetResult() interface{} { func (r SendMessageResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Delete Message Response */ +type DeleteMessageResponse struct { + Xmlns string `xml:"xmlns,attr"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r DeleteMessageResponse) GetResult() interface{} { + return nil +} + +func (r DeleteMessageResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index e3168efa..482ed0b9 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -41,6 +41,7 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // Stupidly these `WriteHeader` calls have to be here, if they're at the start // they lock the headers, at the end they're ignored. w.WriteHeader(statusCode) + err := json.NewEncoder(w).Encode(body.GetResult()) if err != nil { log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) @@ -60,23 +61,23 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // V1 - includes JSON Support (and of course the old XML). var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "ListQueues": sqs.ListQueuesV1, - "GetQueueAttributes": sqs.GetQueueAttributesV1, - "SendMessage": sqs.SendMessageV1, + "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, + "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SendMessage": sqs.SendMessageV1, + "ReceiveMessage": sqs.ReceiveMessageV1, + "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, + "DeleteMessage": sqs.DeleteMessageV1, } var routingTable = map[string]http.HandlerFunc{ // SQS - "SetQueueAttributes": sqs.SetQueueAttributes, - "SendMessageBatch": sqs.SendMessageBatch, - "ReceiveMessage": sqs.ReceiveMessage, - "DeleteMessage": sqs.DeleteMessage, - "DeleteMessageBatch": sqs.DeleteMessageBatch, - "GetQueueUrl": sqs.GetQueueUrl, - "PurgeQueue": sqs.PurgeQueue, - "DeleteQueue": sqs.DeleteQueue, - "ChangeMessageVisibility": sqs.ChangeMessageVisibility, + "SetQueueAttributes": sqs.SetQueueAttributes, + "SendMessageBatch": sqs.SendMessageBatch, + "DeleteMessageBatch": sqs.DeleteMessageBatch, + "GetQueueUrl": sqs.GetQueueUrl, + "PurgeQueue": sqs.PurgeQueue, + "DeleteQueue": sqs.DeleteQueue, // SNS "ListTopics": sns.ListTopics, diff --git a/app/router/router_test.go b/app/router/router_test.go index 3d4216a3..dbc66773 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -260,22 +260,22 @@ func TestActionHandler_v1_xml(t *testing.T) { func TestActionHandler_v0_xml(t *testing.T) { defer func() { routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "ListQueues": sqs.ListQueuesV1, - "GetQueueAttributes": sqs.GetQueueAttributesV1, - "SendMessage": sqs.SendMessageV1, + "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, + "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SendMessage": sqs.SendMessageV1, + "ReceiveMessage": sqs.ReceiveMessageV1, + "DeleteMessage": sqs.DeleteMessageV1, + "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, } routingTable = map[string]http.HandlerFunc{ // SQS - "SetQueueAttributes": sqs.SetQueueAttributes, - "SendMessageBatch": sqs.SendMessageBatch, - "ReceiveMessage": sqs.ReceiveMessage, - "DeleteMessage": sqs.DeleteMessage, - "DeleteMessageBatch": sqs.DeleteMessageBatch, - "GetQueueUrl": sqs.GetQueueUrl, - "PurgeQueue": sqs.PurgeQueue, - "DeleteQueue": sqs.DeleteQueue, - "ChangeMessageVisibility": sqs.ChangeMessageVisibility, + "SetQueueAttributes": sqs.SetQueueAttributes, + "SendMessageBatch": sqs.SendMessageBatch, + "DeleteMessageBatch": sqs.DeleteMessageBatch, + "GetQueueUrl": sqs.GetQueueUrl, + "PurgeQueue": sqs.PurgeQueue, + "DeleteQueue": sqs.DeleteQueue, // SNS "ListTopics": sns.ListTopics, diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 5885a54c..60156d37 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -11,55 +11,6 @@ type ListQueuesResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Receive Message Response */ - -type ResultMessage struct { - MessageId string `xml:"MessageId,omitempty"` - ReceiptHandle string `xml:"ReceiptHandle,omitempty"` - MD5OfBody string `xml:"MD5OfBody,omitempty"` - Body []byte `xml:"Body,omitempty"` - MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes,omitempty"` - MessageAttributes []*ResultMessageAttribute `xml:"MessageAttribute,omitempty"` - Attributes []*ResultAttribute `xml:"Attribute,omitempty"` -} - -type ResultMessageAttributeValue struct { - DataType string `xml:"DataType,omitempty"` - StringValue string `xml:"StringValue,omitempty"` - BinaryValue string `xml:"BinaryValue,omitempty"` -} - -type ResultMessageAttribute struct { - Name string `xml:"Name,omitempty"` - Value *ResultMessageAttributeValue `xml:"Value,omitempty"` -} - -type ResultAttribute struct { - Name string `xml:"Name,omitempty"` - Value string `xml:"Value,omitempty"` -} - -type ReceiveMessageResult struct { - Message []*ResultMessage `xml:"Message,omitempty"` -} - -type ReceiveMessageResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result ReceiveMessageResult `xml:"ReceiveMessageResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - -type ChangeMessageVisibilityResult struct { - Xmlns string `xml:"xmlns,attr"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - -/*** Delete Message Response */ -type DeleteMessageResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} - type DeleteQueueResponse struct { Xmlns string `xml:"xmlns,attr,omitempty"` Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` diff --git a/smoke_tests/README.md b/smoke_tests/README.md new file mode 100644 index 00000000..d2430b4c --- /dev/null +++ b/smoke_tests/README.md @@ -0,0 +1,8 @@ +# Smoke Tests + +Set environment variables prior to running tests: + +```sh +export AWS_ACCESS_KEY_ID=x +export AWS_SECRET_ACCESS_KEY=x +``` diff --git a/smoke_tests/fixtures/requests.go b/smoke_tests/fixtures/requests.go index 6c18fb7f..14f3c7be 100644 --- a/smoke_tests/fixtures/requests.go +++ b/smoke_tests/fixtures/requests.go @@ -42,3 +42,49 @@ var CreateQueueV1RequestXML_NoAttributes = struct { Version: "2012-11-05", QueueName: af.QueueName, } + +var SendMessageRequestBodyXML = struct { + Action string `xml:"Action"` + QueuUrl string `xml:"QueueUrl"` + MessageBody string `xml:"MessageBody"` + Version string `xml:"Version"` +}{ + Action: "SendMessage", + QueuUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL), + MessageBody: "Hello World", + Version: "2012-11-05", +} + +var ReceiveMessageRequestBodyXML = struct { + Action string `xml:"Action"` + QueuUrl string `xml:"QueueUrl"` + Version string `xml:"Version"` +}{ + Action: "ReceiveMessage", + QueuUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL), + Version: "2012-11-05", +} + +var ChangeMessageVisibilityRequestBodyXML = struct { + Action string `xml:"Action"` + QueuUrl string `xml:"QueueUrl"` + ReceiptHandle string `xml:"ReceiptHandle"` + VisibilityTimeout int `xml:"VisibilityTimeout"` + Version string `xml:"Version"` +}{ + Action: "ChangeMessageVisibility", + QueuUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL), + VisibilityTimeout: 2, + Version: "2012-11-05", +} + +var DeleteMessageRequestBodyXML = struct { + Action string `xml:"Action"` + QueuUrl string `xml:"QueueUrl"` + ReceiptHandle string `xml:"ReceiptHandle"` + Version string `xml:"Version"` +}{ + Action: "DeleteMessage", + QueuUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL), + Version: "2012-11-05", +} diff --git a/smoke_tests/sqs_change_message_visibility_test.go b/smoke_tests/sqs_change_message_visibility_test.go new file mode 100644 index 00000000..6ac00694 --- /dev/null +++ b/smoke_tests/sqs_change_message_visibility_test.go @@ -0,0 +1,96 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/utils" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_ChangeMessageVisibilityV1_json(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *createQueueResponse.QueueUrl) + + e.POST("/queue/new-queue-1"). + WithForm(sf.SendMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + receiveMessageResponse, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + + _, err := sqsClient.ChangeMessageVisibility(context.TODO(), &sqs.ChangeMessageVisibilityInput{ + QueueUrl: createQueueResponse.QueueUrl, + ReceiptHandle: receiveMessageResponse.Messages[0].ReceiptHandle, + VisibilityTimeout: 2, + }) + + if err != nil { + t.Fatalf("Error changing message visibility: %v", err) + } +} + +func Test_ChangeMessageVisibilityV1_xml(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + ctx := context.Background() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(ctx) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(ctx, &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *createQueueResponse.QueueUrl) + + e.POST("/queue/new-queue-1"). + WithForm(sf.SendMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + receiveMessageResponse, _ := sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + + requestBody := sf.ChangeMessageVisibilityRequestBodyXML + requestBody.ReceiptHandle = *receiveMessageResponse.Messages[0].ReceiptHandle + e.POST("/queue/new-queue-1"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() +} diff --git a/smoke_tests/sqs_delete_message_test.go b/smoke_tests/sqs_delete_message_test.go new file mode 100644 index 00000000..89c5a4f8 --- /dev/null +++ b/smoke_tests/sqs_delete_message_test.go @@ -0,0 +1,94 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/utils" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_DeleteMessageV1_json(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *createQueueResponse.QueueUrl) + + e.POST("/queue/new-queue-1"). + WithForm(sf.SendMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + receiveMessageResponse, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + + _, err := sqsClient.DeleteMessage(context.TODO(), &sqs.DeleteMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + ReceiptHandle: receiveMessageResponse.Messages[0].ReceiptHandle, + }) + + if err != nil { + t.Fatalf("Error deleting message: %v", err) + } +} + +func Test_DeleteMessageV1_xml(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *createQueueResponse.QueueUrl) + + e.POST("/queue/new-queue-1"). + WithForm(sf.SendMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + receiveMessageResponse, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + + requestBody := sf.DeleteMessageRequestBodyXML + requestBody.ReceiptHandle = *receiveMessageResponse.Messages[0].ReceiptHandle + e.POST("/queue/new-queue-1"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() +} diff --git a/smoke_tests/sqs_receive_message_test.go b/smoke_tests/sqs_receive_message_test.go new file mode 100644 index 00000000..c6253a16 --- /dev/null +++ b/smoke_tests/sqs_receive_message_test.go @@ -0,0 +1,94 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_ReceiveMessageV1_json(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *createQueueResponse.QueueUrl) + + e.POST("/queue/new-queue-1"). + WithForm(sf.SendMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + receiveMessageResponse, err := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + + if err != nil { + t.Fatalf("Error receiving message: %v", err) + } + + assert.Equal(t, 1, len(receiveMessageResponse.Messages)) + assert.Equal(t, sf.SendMessageRequestBodyXML.MessageBody, *receiveMessageResponse.Messages[0].Body) +} + +func Test_ReceiveMessageV1_xml(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + assert.Equal(t, fmt.Sprintf("%s/new-queue-1", af.BASE_URL), *createQueueResponse.QueueUrl) + + e.POST("/queue/new-queue-1"). + WithForm(sf.SendMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + r := e.POST("/queue/new-queue-1"). + WithForm(sf.ReceiveMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + var r1 models.ReceiveMessageResponse + xml.Unmarshal([]byte(r), &r1) + + assert.Equal(t, 1, len(r1.Result.Messages)) + assert.Equal(t, sf.SendMessageRequestBodyXML.MessageBody, string(r1.Result.Messages[0].Body)) +} diff --git a/smoke_tests/sqs_send_message_test.go b/smoke_tests/sqs_send_message_test.go index 23e713b3..0bf4fc88 100644 --- a/smoke_tests/sqs_send_message_test.go +++ b/smoke_tests/sqs_send_message_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/Admiral-Piett/goaws/app" af "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/utils" @@ -55,25 +54,11 @@ func Test_SendMessageV1_json_no_attributes(t *testing.T) { assert.Equal(t, "1", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) // Receive message and check attribute - receiveMessageBodyXML := struct { - Action string `xml:"Action"` - Version string `xml:"Version"` - QueueUrl string `xml:"QueueUrl"` - }{ - Action: "ReceiveMessage", - Version: "2012-11-05", - QueueUrl: *targetQueueUrl, - } - e := httpexpect.Default(t, server.URL) - r := e.POST("/"). - WithForm(receiveMessageBodyXML). - Expect(). - Status(http.StatusOK). - Body().Raw() - r3 := app.ReceiveMessageResponse{} - xml.Unmarshal([]byte(r), &r3) - message := r3.Result.Message[0] - assert.Equal(t, targetMessageBody, string(message.Body)) + r3, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: targetQueueUrl, + }) + message := r3.Messages[0] + assert.Equal(t, targetMessageBody, string(*message.Body)) assert.Equal(t, 0, len(message.MessageAttributes)) } @@ -131,34 +116,35 @@ func Test_SendMessageV1_json_with_attributes(t *testing.T) { assert.Equal(t, "1", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) // Receive message and check attribute - receiveMessageBodyXML := struct { - Action string `xml:"Action"` - Version string `xml:"Version"` - QueueUrl string `xml:"QueueUrl"` - }{ - Action: "ReceiveMessage", - Version: "2012-11-05", - QueueUrl: *targetQueueUrl, - } - e := httpexpect.Default(t, server.URL) - r := e.POST("/"). - WithForm(receiveMessageBodyXML). - Expect(). - Status(http.StatusOK). - Body().Raw() - r3 := app.ReceiveMessageResponse{} - xml.Unmarshal([]byte(r), &r3) - message := r3.Result.Message[0] - assert.Equal(t, targetMessageBody, string(message.Body)) + r3, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: targetQueueUrl, + }) + message := r3.Messages[0] + assert.Equal(t, targetMessageBody, string(*message.Body)) assert.Equal(t, 3, len(message.MessageAttributes)) - var attr1, attr2, attr3 app.ResultMessageAttribute - for _, attr := range message.MessageAttributes { - if attr.Name == "attr1" { - attr1 = *attr - } else if attr.Name == "attr2" { - attr2 = *attr - } else if attr.Name == "attr3" { - attr3 = *attr + var attr1, attr2, attr3 models.ResultMessageAttribute + for k, attr := range message.MessageAttributes { + if k == "attr1" { + attr1.Name = k + attr1.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + BinaryValue: string(attr.BinaryValue), + } + } else if k == "attr2" { + attr2.Name = k + attr2.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + BinaryValue: string(attr.BinaryValue), + } + } else if k == "attr3" { + attr3.Name = k + attr3.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + BinaryValue: string(attr.BinaryValue), + } } } assert.Equal(t, "attr1", attr1.Name) @@ -346,12 +332,12 @@ func Test_SendMessageV1_xml_with_attributes(t *testing.T) { Expect(). Status(http.StatusOK). Body().Raw() - r4 := app.ReceiveMessageResponse{} + r4 := models.ReceiveMessageResponse{} xml.Unmarshal([]byte(r), &r4) - message := r4.Result.Message[0] + message := r4.Result.Messages[0] assert.Equal(t, "Test Message", string(message.Body)) assert.Equal(t, 3, len(message.MessageAttributes)) - var attr1, attr2, attr3 app.ResultMessageAttribute + var attr1, attr2, attr3 models.ResultMessageAttribute for _, attr := range message.MessageAttributes { if attr.Name == "attr1" { attr1 = *attr From b3d57628f914fd313abd87d74ea6e1f1f1534916 Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Fri, 17 May 2024 17:50:58 -0400 Subject: [PATCH 23/41] Add SetQueueAttributesV1 for JSON support --- app/fixtures/sqs.go | 35 ++- app/gosqs/change_message_visibility.go | 2 +- app/gosqs/create_queue.go | 2 +- app/gosqs/create_queue_test.go | 38 +-- app/gosqs/delete_message.go | 2 +- app/gosqs/get_queue_attributes.go | 2 +- app/gosqs/get_queue_attributes_test.go | 14 +- app/gosqs/gosqs.go | 37 --- app/gosqs/gosqs_test.go | 34 --- app/gosqs/list_queues.go | 2 +- app/gosqs/list_queues_test.go | 12 +- app/gosqs/receive_message.go | 2 +- app/gosqs/receive_message_test.go | 7 +- app/gosqs/send_message.go | 2 +- app/gosqs/send_message_test.go | 12 +- app/gosqs/set_queue_attributes.go | 49 ++++ app/gosqs/set_queue_attributes_test.go | 168 +++++++++++++ app/models/models.go | 114 ++++++++- app/models/models_test.go | 142 +++++++++++ app/models/responses.go | 13 + app/router/router.go | 6 +- app/router/router_test.go | 2 +- app/utils/tests.go | 7 +- app/utils/utils.go | 6 +- app/utils/utils_test.go | 22 +- smoke_tests/fixtures/requests.go | 14 +- smoke_tests/sqs_get_queue_attributes_test.go | 13 +- smoke_tests/sqs_list_queues_test.go | 4 +- smoke_tests/sqs_set_queue_attributes_test.go | 243 +++++++++++++++++++ 29 files changed, 848 insertions(+), 158 deletions(-) create mode 100644 app/gosqs/set_queue_attributes.go create mode 100644 app/gosqs/set_queue_attributes_test.go create mode 100644 smoke_tests/sqs_set_queue_attributes_test.go diff --git a/app/fixtures/sqs.go b/app/fixtures/sqs.go index 6fbb9ec8..4ecaff43 100644 --- a/app/fixtures/sqs.go +++ b/app/fixtures/sqs.go @@ -2,20 +2,46 @@ package fixtures import ( "fmt" + "time" + + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/models" ) var QueueName = "new-queue-1" +var QueueUrl = fmt.Sprintf("%s/%s", BASE_URL, QueueName) var DeadLetterQueueName = "dead-letter-queue-1" +var FullyPopulatedQueue = &app.Queue{ + Name: QueueName, + URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", + LOCAL_ENVIRONMENT.Region, + LOCAL_ENVIRONMENT.Host, + LOCAL_ENVIRONMENT.Port, + LOCAL_ENVIRONMENT.AccountID, + QueueName, + ), + Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", + LOCAL_ENVIRONMENT.Region, + LOCAL_ENVIRONMENT.AccountID, + QueueName, + ), + VisibilityTimeout: 5, + ReceiveMessageWaitTimeSeconds: 4, + DelaySeconds: 1, + MaximumMessageSize: 2, + MessageRetentionPeriod: 3, + Duplicates: make(map[string]time.Time), +} + var CreateQueueRequest = models.CreateQueueRequest{ QueueName: QueueName, - Attributes: CreateQueueAttributes, + Attributes: QueueAttributes, Tags: map[string]string{"my": "tag"}, } -var CreateQueueAttributes = models.Attributes{ +var QueueAttributes = models.Attributes{ DelaySeconds: 1, MaximumMessageSize: 2, MessageRetentionPeriod: 3, @@ -90,3 +116,8 @@ var GetQueueAttributesResponse = models.GetQueueAttributesResponse{ }}, Metadata: models.BASE_RESPONSE_METADATA, } + +var SetQueueAttributesRequest = models.SetQueueAttributesRequest{ + QueueUrl: fmt.Sprintf("%s/%s", BASE_URL, "unit-queue1"), + Attributes: QueueAttributes, +} diff --git a/app/gosqs/change_message_visibility.go b/app/gosqs/change_message_visibility.go index adf29a0d..8678cd4c 100644 --- a/app/gosqs/change_message_visibility.go +++ b/app/gosqs/change_message_visibility.go @@ -15,7 +15,7 @@ import ( func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewChangeMessageVisibilityRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - ChangeMessageVisibilityV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/create_queue.go b/app/gosqs/create_queue.go index aac0c5eb..f4c89d29 100644 --- a/app/gosqs/create_queue.go +++ b/app/gosqs/create_queue.go @@ -13,7 +13,7 @@ import ( func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewCreateQueueRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - CreateQueueV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/create_queue_test.go b/app/gosqs/create_queue_test.go index ecabef7a..65a3b805 100644 --- a/app/gosqs/create_queue_test.go +++ b/app/gosqs/create_queue_test.go @@ -22,34 +22,12 @@ func TestCreateQueueV1_success(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.CreateQueueRequest) *v = fixtures.CreateQueueRequest return true } - expectedQueue := &app.Queue{ - Name: fixtures.QueueName, - URL: fmt.Sprintf("http://%s.%s:%s/%s/%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.Host, - fixtures.LOCAL_ENVIRONMENT.Port, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - Arn: fmt.Sprintf("arn:aws:sqs:%s:%s:%s", - fixtures.LOCAL_ENVIRONMENT.Region, - fixtures.LOCAL_ENVIRONMENT.AccountID, - fixtures.QueueName, - ), - VisibilityTimeout: 5, - ReceiveMessageWaitTimeSeconds: 4, - DelaySeconds: 1, - MaximumMessageSize: 2, - MessageRetentionPeriod: 3, - Duplicates: make(map[string]time.Time), - } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) code, response := CreateQueueV1(r) @@ -57,7 +35,7 @@ func TestCreateQueueV1_success(t *testing.T) { assert.Equal(t, fixtures.CreateQueueResponse, response) actualQueue := app.SyncQueues.Queues[fixtures.QueueName] - assert.Equal(t, expectedQueue, actualQueue) + assert.Equal(t, fixtures.FullyPopulatedQueue, actualQueue) } func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { @@ -67,7 +45,7 @@ func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) c, _ := dupe.(models.CreateQueueRequest) c.Attributes.RedrivePolicy = models.RedrivePolicy{ @@ -126,7 +104,7 @@ func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.CreateQueueRequest) *v = fixtures.CreateQueueRequest return true @@ -154,7 +132,7 @@ func TestCreateQueueV1_success_with_no_request_attributes_falls_back_to_default( utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) c, _ := dupe.(models.CreateQueueRequest) c.Attributes = models.Attributes{} @@ -204,7 +182,7 @@ func TestCreateQueueV1_success_no_configured_region_for_queue_url(t *testing.T) utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) c, _ := dupe.(models.CreateQueueRequest) c.Attributes = models.Attributes{} @@ -238,7 +216,7 @@ func TestCreateQueueV1_request_transformer_error(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { return false } @@ -255,7 +233,7 @@ func TestCreateQueueV1_invalid_dead_letter_queue_error(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) c, _ := dupe.(models.CreateQueueRequest) c.Attributes.RedrivePolicy = models.RedrivePolicy{ diff --git a/app/gosqs/delete_message.go b/app/gosqs/delete_message.go index aab988e0..50de84d1 100644 --- a/app/gosqs/delete_message.go +++ b/app/gosqs/delete_message.go @@ -14,7 +14,7 @@ import ( func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewDeleteMessageRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - DeleteMessageV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/get_queue_attributes.go b/app/gosqs/get_queue_attributes.go index 63b1d116..280c819a 100644 --- a/app/gosqs/get_queue_attributes.go +++ b/app/gosqs/get_queue_attributes.go @@ -18,7 +18,7 @@ import ( func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewGetQueueAttributesRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - GetQueueAttributesV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/get_queue_attributes_test.go b/app/gosqs/get_queue_attributes_test.go index ec713551..79ecfe26 100644 --- a/app/gosqs/get_queue_attributes_test.go +++ b/app/gosqs/get_queue_attributes_test.go @@ -23,7 +23,7 @@ func TestGetQueueAttributesV1_success_all(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.GetQueueAttributesRequest) *v = fixtures.GetQueueAttributesRequest return true @@ -43,7 +43,7 @@ func TestGetQueueAttributesV1_success_no_request_attrs_returns_all(t *testing.T) utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.GetQueueAttributesRequest) *v = models.GetQueueAttributesRequest{ QueueUrl: "unit-queue1", @@ -65,7 +65,7 @@ func TestGetQueueAttributesV1_success_all_with_redrive_queue(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.GetQueueAttributesRequest) *v = models.GetQueueAttributesRequest{ QueueUrl: "unit-queue2", @@ -98,7 +98,7 @@ func TestGetQueueAttributesV1_success_specific_fields(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.GetQueueAttributesRequest) *v = models.GetQueueAttributesRequest{ QueueUrl: fmt.Sprintf("%s/unit-queue1", fixtures.BASE_URL), @@ -130,7 +130,7 @@ func TestGetQueueAttributesV1_request_transformer_error(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { return false } @@ -145,7 +145,7 @@ func TestGetQueueAttributesV1_missing_queue_url_in_request_returns_error(t *test utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.GetQueueAttributesRequest) *v = models.GetQueueAttributesRequest{ QueueUrl: "", @@ -165,7 +165,7 @@ func TestGetQueueAttributesV1_missing_queue_returns_error(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.GetQueueAttributesRequest) *v = fixtures.GetQueueAttributesRequest return true diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 33ca4bbf..8a678bf7 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -415,43 +415,6 @@ func GetQueueUrl(w http.ResponseWriter, req *http.Request) { } } -func SetQueueAttributes(w http.ResponseWriter, req *http.Request) { - // Sent response type - w.Header().Set("Content-Type", "application/xml") - - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - log.Println("Set Queue Attributes:", queueName) - app.SyncQueues.Lock() - if queue, ok := app.SyncQueues.Queues[queueName]; ok { - if err := validateAndSetQueueAttributesFromForm(queue, req.Form); err != nil { - createErrorResponse(w, req, err.Error()) - app.SyncQueues.Unlock() - return - } - - respStruct := app.SetQueueAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } - } else { - log.Println("Get Queue URL:", queueName, ", queue does not exist!!!") - createErrorResponse(w, req, "QueueNotFound") - } - app.SyncQueues.Unlock() -} - func getQueueFromPath(formVal string, theUrl string) string { if formVal != "" { return formVal diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 3b205249..5957b1ba 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -600,40 +600,6 @@ func TestDeadLetterQueue(t *testing.T) { } } -func TestSetQueueAttributes_POST_QueueNotFound(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("Action", "SetQueueAttributes") - form.Add("QueueUrl", "http://localhost:4100/queue/not-existing") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SetQueueAttributes) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "NonExistentQueue" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) { done := make(chan struct{}, 0) go PeriodicTasks(1*time.Second, done) diff --git a/app/gosqs/list_queues.go b/app/gosqs/list_queues.go index 35a84698..5336b30d 100644 --- a/app/gosqs/list_queues.go +++ b/app/gosqs/list_queues.go @@ -18,7 +18,7 @@ import ( // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ListQueues.html func ListQueuesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewListQueuesRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, true) if !ok { log.Error("Invalid Request - ListQueuesV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/list_queues_test.go b/app/gosqs/list_queues_test.go index f1102dbd..30156265 100644 --- a/app/gosqs/list_queues_test.go +++ b/app/gosqs/list_queues_test.go @@ -20,7 +20,7 @@ func TestListQueuesV1_success(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.ListQueueRequest) *v = models.ListQueueRequest{} return true @@ -42,7 +42,7 @@ func TestListQueuesV1_success_no_queues(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.ListQueueRequest) *v = models.ListQueueRequest{} return true @@ -63,7 +63,7 @@ func TestListQueuesV1_success_with_queue_name_prefix(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.ListQueueRequest) *v = models.ListQueueRequest{QueueNamePrefix: "other"} return true @@ -84,7 +84,7 @@ func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testi utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.ListQueueRequest) *v = models.ListQueueRequest{QueueNamePrefix: "garbage"} return true @@ -99,13 +99,11 @@ func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testi } func TestListQueuesV1_request_transformer_error(t *testing.T) { - //conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { utils.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { return false } diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go index 0464df3c..613ded75 100644 --- a/app/gosqs/receive_message.go +++ b/app/gosqs/receive_message.go @@ -17,7 +17,7 @@ import ( func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewReceiveMessageRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - ReceiveMessageV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/receive_message_test.go b/app/gosqs/receive_message_test.go index 12c6997c..ea43c57e 100644 --- a/app/gosqs/receive_message_test.go +++ b/app/gosqs/receive_message_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO - figure out a better way to handle the wait time in these tests. Maybe in the smoke tests alone +// if there's nothing else? func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { @@ -25,6 +27,7 @@ func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { q := &app.Queue{ Name: "waiting-queue", ReceiveMessageWaitTimeSeconds: 2, + //MaximumMessageSize: 262144, } app.SyncQueues.Queues["waiting-queue"] = q @@ -34,12 +37,12 @@ func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { }, true) start := time.Now() - status, _ := ReceiveMessageV1(r) + status, response := ReceiveMessageV1(r) elapsed := time.Since(start) assert.Equal(t, http.StatusOK, status) if elapsed < 2*time.Second { - t.Fatal("handler didn't wait ReceiveMessageWaitTimeSeconds") + t.Fatalf("handler didn't wait ReceiveMessageWaitTimeSeconds %s", response) } // mock sending a message diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index 701eb636..a20931e5 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -19,7 +19,7 @@ import ( func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewSendMessageRequest() - ok := utils.REQUEST_TRANSFORMER(requestBody, req) + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - CreateQueueV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) diff --git a/app/gosqs/send_message_test.go b/app/gosqs/send_message_test.go index a0fc1431..2c43ec6e 100644 --- a/app/gosqs/send_message_test.go +++ b/app/gosqs/send_message_test.go @@ -24,7 +24,7 @@ func TestSendMessageV1_Success(t *testing.T) { QueueUrl: "http://localhost:4200/new-queue-1", MessageBody: "Test Message", } - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.SendMessageRequest) *v = sendMessageRequest_success return true @@ -64,7 +64,7 @@ func TestSendMessageV1_Success_FIFOQueue(t *testing.T) { QueueUrl: "http://localhost:4200/new-queue-1", MessageBody: "Test Message", } - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.SendMessageRequest) *v = sendMessageRequest_success return true @@ -106,7 +106,7 @@ func TestSendMessageV1_Success_Deduplication(t *testing.T) { MessageBody: "Test Message", MessageDeduplicationId: "1", } - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.SendMessageRequest) *v = sendMessageRequest_success return true @@ -144,7 +144,7 @@ func TestSendMessageV1_request_transformer_error(t *testing.T) { utils.REQUEST_TRANSFORMER = utils.TransformRequest }() - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { return false } @@ -165,7 +165,7 @@ func TestSendMessageV1_MaximumMessageSize_MessageTooBig(t *testing.T) { QueueUrl: "http://localhost:4200/new-queue-1", MessageBody: "Test Message", } - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.SendMessageRequest) *v = sendMessageRequest_success return true @@ -198,7 +198,7 @@ func TestSendMessageV1_POST_QueueNonExistant(t *testing.T) { QueueUrl: "http://localhost:4200/new-queue-1", MessageBody: "Test Message", } - utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.SendMessageRequest) *v = sendMessageRequest_success return true diff --git a/app/gosqs/set_queue_attributes.go b/app/gosqs/set_queue_attributes.go new file mode 100644 index 00000000..f47876a6 --- /dev/null +++ b/app/gosqs/set_queue_attributes.go @@ -0,0 +1,49 @@ +package gosqs + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + log "github.com/sirupsen/logrus" +) + +func SetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewSetQueueAttributesRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - GetQueueAttributesV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + if requestBody.QueueUrl == "" { + log.Error("Missing QueueUrl - GetQueueAttributesV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + // NOTE: I tore out the handling for devining the url from a param. I can't find documentation that + // that is valid any longer. + uriSegments := strings.Split(requestBody.QueueUrl, "/") + queueName := uriSegments[len(uriSegments)-1] + + log.Infof("Set Queue Attributes: %s", queueName) + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + queue, ok := app.SyncQueues.Queues[queueName] + if !ok { + log.Warningf("Get Queue URL: %s, queue does not exist!!!", queueName) + return createErrorResponseV1("QueueNotFound") + } + if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { + return createErrorResponseV1(err.Error()) + } + + respStruct := models.SetQueueAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + return http.StatusOK, respStruct +} diff --git a/app/gosqs/set_queue_attributes_test.go b/app/gosqs/set_queue_attributes_test.go new file mode 100644 index 00000000..06a202ec --- /dev/null +++ b/app/gosqs/set_queue_attributes_test.go @@ -0,0 +1,168 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestSetQueueAttributesV1_success_multiple_attributes(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetQueueAttributesRequest) + *v = fixtures.SetQueueAttributesRequest + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := SetQueueAttributesV1(r) + + expectedResponse := models.SetQueueAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) + + actualQueue := app.SyncQueues.Queues["unit-queue1"] + assert.Equal(t, 5, actualQueue.VisibilityTimeout) + assert.Equal(t, 4, actualQueue.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 1, actualQueue.DelaySeconds) + assert.Equal(t, 2, actualQueue.MaximumMessageSize) + assert.Equal(t, 3, actualQueue.MessageRetentionPeriod) +} + +func TestSetQueueAttributesV1_success_single_attribute(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetQueueAttributesRequest) + *v = models.SetQueueAttributesRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + Attributes: models.Attributes{ + VisibilityTimeout: 5, + }, + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := SetQueueAttributesV1(r) + + expectedResponse := models.SetQueueAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) + + actualQueue := app.SyncQueues.Queues["unit-queue1"] + assert.Equal(t, 5, actualQueue.VisibilityTimeout) + assert.Equal(t, 0, actualQueue.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, 0, actualQueue.DelaySeconds) + assert.Equal(t, 0, actualQueue.MaximumMessageSize) + assert.Equal(t, 345600, actualQueue.MessageRetentionPeriod) +} + +func TestSetQueueAttributesV1_invalid_request_body(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestSetQueueAttributesV1_missing_queue_url(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetQueueAttributesRequest) + *v = models.SetQueueAttributesRequest{ + Attributes: models.Attributes{}, + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestSetQueueAttributesV1_missing_expected_queue(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetQueueAttributesRequest) + *v = models.SetQueueAttributesRequest{ + QueueUrl: "garbage", + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestSetQueueAttributesV1_invalid_redrive_queue(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetQueueAttributesRequest) + *v = models.SetQueueAttributesRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + Attributes: models.Attributes{ + RedrivePolicy: models.RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", "garbage"), + }, + }, + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetQueueAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/models/models.go b/app/models/models.go index 5c10832e..2f4ad18a 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -158,7 +158,6 @@ func (r *ListQueueRequest) SetAttributesFromForm(values url.Values) { r.QueueNamePrefix = values.Get("QueueNamePrefix") } -// TODO - test models and responses func NewGetQueueAttributesRequest() *GetQueueAttributesRequest { return &GetQueueAttributesRequest{} } @@ -182,7 +181,6 @@ func (r *GetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { } /*** Send Message Request */ - func NewSendMessageRequest() *SendMessageRequest { return &SendMessageRequest{ MessageAttributes: make(map[string]MessageAttributeValue), @@ -244,6 +242,118 @@ func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { } } +func NewSetQueueAttributesRequest() *SetQueueAttributesRequest { + return &SetQueueAttributesRequest{} +} + +type SetQueueAttributesRequest struct { + QueueUrl string `json:"QueueUrl"` + Attributes Attributes `json:"Attributes"` +} + +func (r *SetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { + r.QueueUrl = values.Get("QueueUrl") + // TODO - could we share with CreateQueueRequest? + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attribute.%d.Name", i) + attrName := values.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attribute.%d.Value", i) + attrValue := values.Get(valueKey) + if attrValue == "" { + continue + } + switch attrName { + case "DelaySeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.DelaySeconds = StringToInt(tmp) + case "MaximumMessageSize": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MaximumMessageSize = StringToInt(tmp) + case "MessageRetentionPeriod": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MessageRetentionPeriod = StringToInt(tmp) + case "Policy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.Policy = tmp + case "ReceiveMessageWaitTimeSeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.ReceiveMessageWaitTimeSeconds = StringToInt(tmp) + case "VisibilityTimeout": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.VisibilityTimeout = StringToInt(tmp) + case "RedrivePolicy": + tmp := RedrivePolicy{} + var decodedPolicy struct { + MaxReceiveCount interface{} `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + } + err := json.Unmarshal([]byte(attrValue), &decodedPolicy) + if err != nil || decodedPolicy.DeadLetterTargetArn == "" { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + // Support both int and string types (historic processing), set a default of 10 if not provided. + // Go will default into float64 for interface{} types when parsing numbers + receiveCount, ok := decodedPolicy.MaxReceiveCount.(float64) + if !ok { + receiveCount = 10 + t, ok := decodedPolicy.MaxReceiveCount.(string) + if ok { + r, err := strconv.ParseFloat(t, 64) + if err == nil { + receiveCount = r + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } + tmp.MaxReceiveCount = StringToInt(receiveCount) + tmp.DeadLetterTargetArn = decodedPolicy.DeadLetterTargetArn + r.Attributes.RedrivePolicy = tmp + case "RedriveAllowPolicy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.RedriveAllowPolicy = tmp + } + } + return +} + // TODO - copy Attributes for SNS // TODO - there are FIFO attributes and things too diff --git a/app/models/models_test.go b/app/models/models_test.go index 9df80860..97eded66 100644 --- a/app/models/models_test.go +++ b/app/models/models_test.go @@ -319,3 +319,145 @@ func TestSendMessageRequest_SetAttributesFromForm_success(t *testing.T) { assert.Equal(t, "", attr2.StringValue) assert.Equal(t, "VmFsdWUy", attr2.BinaryValue) } + +func TestSetQueueAttributesRequest_SetAttributesFromForm_success(t *testing.T) { + expectedRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Action", "CreateQueue") + form.Add("QueueName", "new-queue") + form.Add("Version", "2012-11-05") + form.Add("Attribute.1.Name", "DelaySeconds") + form.Add("Attribute.1.Value", "1") + form.Add("Attribute.2.Name", "MaximumMessageSize") + form.Add("Attribute.2.Value", "2") + form.Add("Attribute.3.Name", "MessageRetentionPeriod") + form.Add("Attribute.3.Value", "3") + form.Add("Attribute.4.Name", "Policy") + form.Add("Attribute.4.Value", "{\"i-am\":\"the-policy\"}") + form.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") + form.Add("Attribute.5.Value", "4") + form.Add("Attribute.6.Name", "VisibilityTimeout") + form.Add("Attribute.6.Value", "5") + form.Add("Attribute.7.Name", "RedrivePolicy") + form.Add("Attribute.7.Value", "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + form.Add("Attribute.8.Name", "RedriveAllowPolicy") + form.Add("Attribute.8.Value", "{\"i-am\":\"the-redrive-allow-policy\"}") + + cqr := &SetQueueAttributesRequest{ + Attributes: Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 262144, + MessageRetentionPeriod: 345600, + ReceiveMessageWaitTimeSeconds: 10, + VisibilityTimeout: 30, + }, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, StringToInt(1), cqr.Attributes.DelaySeconds) + assert.Equal(t, StringToInt(2), cqr.Attributes.MaximumMessageSize) + assert.Equal(t, StringToInt(3), cqr.Attributes.MessageRetentionPeriod) + assert.Equal(t, map[string]interface{}{"i-am": "the-policy"}, cqr.Attributes.Policy) + assert.Equal(t, StringToInt(4), cqr.Attributes.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, StringToInt(5), cqr.Attributes.VisibilityTimeout) + assert.Equal(t, expectedRedrivePolicy, cqr.Attributes.RedrivePolicy) + assert.Equal(t, map[string]interface{}{"i-am": "the-redrive-allow-policy"}, cqr.Attributes.RedriveAllowPolicy) +} + +func TestSetQueueAttributesRequest_SetAttributesFromForm_success_handles_redrive_recieve_count_int(t *testing.T) { + expectedRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Attribute.1.Name", "RedrivePolicy") + form.Add("Attribute.1.Value", "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + + cqr := &SetQueueAttributesRequest{ + Attributes: Attributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, expectedRedrivePolicy, cqr.Attributes.RedrivePolicy) +} + +func TestSetQueueAttributesRequest_SetAttributesFromForm_success_handles_redrive_recieve_count_string(t *testing.T) { + expectedRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 100, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Attribute.1.Name", "RedrivePolicy") + form.Add("Attribute.1.Value", "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + + cqr := &SetQueueAttributesRequest{ + Attributes: Attributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, expectedRedrivePolicy, cqr.Attributes.RedrivePolicy) +} + +func TestSetQueueAttributesRequest_SetAttributesFromForm_success_default_unparsable_redrive_recieve_count(t *testing.T) { + defaultRedrivePolicy := RedrivePolicy{ + MaxReceiveCount: 10, + DeadLetterTargetArn: "dead-letter-queue-arn", + } + + form := url.Values{} + form.Add("Attribute.1.Name", "RedrivePolicy") + form.Add("Attribute.1.Value", "{\"maxReceiveCount\": null, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") + + cqr := &SetQueueAttributesRequest{ + Attributes: Attributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, defaultRedrivePolicy, cqr.Attributes.RedrivePolicy) +} + +func TestSetQueueAttributesRequest_SetAttributesFromForm_success_skips_invalid_values(t *testing.T) { + form := url.Values{} + form.Add("Attribute.1.Name", "DelaySeconds") + form.Add("Attribute.1.Value", "garbage") + form.Add("Attribute.2.Name", "MaximumMessageSize") + form.Add("Attribute.2.Value", "garbage") + form.Add("Attribute.3.Name", "MessageRetentionPeriod") + form.Add("Attribute.3.Value", "garbage") + form.Add("Attribute.4.Name", "Policy") + form.Add("Attribute.4.Value", "garbage") + form.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") + form.Add("Attribute.5.Value", "garbage") + form.Add("Attribute.6.Name", "VisibilityTimeout") + form.Add("Attribute.6.Value", "garbage") + form.Add("Attribute.7.Name", "RedrivePolicy") + form.Add("Attribute.7.Value", "garbage") + form.Add("Attribute.8.Name", "RedriveAllowPolicy") + form.Add("Attribute.8.Value", "garbage") + + cqr := &SetQueueAttributesRequest{ + Attributes: Attributes{ + DelaySeconds: 1, + MaximumMessageSize: 262144, + MessageRetentionPeriod: 345600, + ReceiveMessageWaitTimeSeconds: 10, + VisibilityTimeout: 30, + }, + } + cqr.SetAttributesFromForm(form) + + assert.Equal(t, StringToInt(1), cqr.Attributes.DelaySeconds) + assert.Equal(t, StringToInt(262144), cqr.Attributes.MaximumMessageSize) + assert.Equal(t, StringToInt(345600), cqr.Attributes.MessageRetentionPeriod) + assert.Equal(t, map[string]interface{}(nil), cqr.Attributes.Policy) + assert.Equal(t, StringToInt(10), cqr.Attributes.ReceiveMessageWaitTimeSeconds) + assert.Equal(t, StringToInt(30), cqr.Attributes.VisibilityTimeout) + assert.Equal(t, RedrivePolicy{}, cqr.Attributes.RedrivePolicy) + assert.Equal(t, map[string]interface{}(nil), cqr.Attributes.RedriveAllowPolicy) +} diff --git a/app/models/responses.go b/app/models/responses.go index 133357f7..38bd3e5f 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -223,3 +223,16 @@ func (r DeleteMessageResponse) GetResult() interface{} { func (r DeleteMessageResponse) GetRequestId() string { return r.Metadata.RequestId } + +type SetQueueAttributesResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r SetQueueAttributesResponse) GetResult() interface{} { + return nil +} + +func (r SetQueueAttributesResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index 482ed0b9..4eac5bd4 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -41,7 +41,9 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // Stupidly these `WriteHeader` calls have to be here, if they're at the start // they lock the headers, at the end they're ignored. w.WriteHeader(statusCode) - + if body.GetResult() == nil { + return + } err := json.NewEncoder(w).Encode(body.GetResult()) if err != nil { log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) @@ -64,6 +66,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "CreateQueue": sqs.CreateQueueV1, "ListQueues": sqs.ListQueuesV1, "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SetQueueAttributes": sqs.SetQueueAttributesV1, "SendMessage": sqs.SendMessageV1, "ReceiveMessage": sqs.ReceiveMessageV1, "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, @@ -72,7 +75,6 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR var routingTable = map[string]http.HandlerFunc{ // SQS - "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, "GetQueueUrl": sqs.GetQueueUrl, diff --git a/app/router/router_test.go b/app/router/router_test.go index dbc66773..e703bae8 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -263,6 +263,7 @@ func TestActionHandler_v0_xml(t *testing.T) { "CreateQueue": sqs.CreateQueueV1, "ListQueues": sqs.ListQueuesV1, "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SetQueueAttributes": sqs.SetQueueAttributesV1, "SendMessage": sqs.SendMessageV1, "ReceiveMessage": sqs.ReceiveMessageV1, "DeleteMessage": sqs.DeleteMessageV1, @@ -270,7 +271,6 @@ func TestActionHandler_v0_xml(t *testing.T) { } routingTable = map[string]http.HandlerFunc{ // SQS - "SetQueueAttributes": sqs.SetQueueAttributes, "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, "GetQueueUrl": sqs.GetQueueUrl, diff --git a/app/utils/tests.go b/app/utils/tests.go index bac67374..3be85742 100644 --- a/app/utils/tests.go +++ b/app/utils/tests.go @@ -35,12 +35,13 @@ func GenerateRequestInfo(method, url string, body interface{}, isJson bool) (*ht var req *http.Request var err error if isJson { - if body != nil { + // Default request body when none is provided + if body == nil { + req, err = http.NewRequest(method, url, http.NoBody) + } else { b, _ := json.Marshal(body) request_body := bytes.NewBuffer(b) req, err = http.NewRequest(method, url, request_body) - } else { - req, err = http.NewRequest(method, url, nil) } if err != nil { panic(err) diff --git a/app/utils/utils.go b/app/utils/utils.go index 3e1aa5fd..d2030b9e 100644 --- a/app/utils/utils.go +++ b/app/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "encoding/json" "fmt" + "io" "net/http" "net/url" @@ -24,13 +25,16 @@ func InitializeDecoders() { // QUESTION - alternately we could have the router.actionHandler method call this, but then our router maps // need to track the request type AND the function call. I think there'd be a lot of interface switching // back and forth. -func TransformRequest(resultingStruct interfaces.AbstractRequestBody, req *http.Request) (success bool) { +func TransformRequest(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { switch req.Header.Get("Content-Type") { case "application/x-amz-json-1.0": //Read body data to parse json decoder := json.NewDecoder(req.Body) err := decoder.Decode(resultingStruct) if err != nil { + if emptyRequestValid && err == io.EOF { + return true + } log.Debugf("TransformRequest Failure - %s", err.Error()) return false } diff --git a/app/utils/utils_test.go b/app/utils/utils_test.go index ea35f45f..418761cd 100644 --- a/app/utils/utils_test.go +++ b/app/utils/utils_test.go @@ -20,13 +20,25 @@ func TestTransformRequest_success_json(t *testing.T) { mock := &mocks.MockRequestBody{} - ok := TransformRequest(mock, r) + ok := TransformRequest(mock, r, false) assert.True(t, ok) assert.Equal(t, "mock-value", mock.RequestFieldStr) assert.False(t, mock.SetAttributesFromFormCalled) } +func TestTransformRequest_success_json_empty_request_accepted(t *testing.T) { + _, r := GenerateRequestInfo("POST", "url", nil, true) + + mock := &mocks.MockRequestBody{} + + ok := TransformRequest(mock, r, true) + + assert.True(t, ok) + //assert.Equal(t, "mock-value", mock.RequestFieldStr) + assert.False(t, mock.SetAttributesFromFormCalled) +} + func TestTransformRequest_success_xml(t *testing.T) { _, r := GenerateRequestInfo("POST", "url", nil, false) form := url.Values{} @@ -40,7 +52,7 @@ func TestTransformRequest_success_xml(t *testing.T) { mock := &mocks.MockRequestBody{} - ok := TransformRequest(mock, r) + ok := TransformRequest(mock, r, false) assert.True(t, ok) assert.True(t, mock.SetAttributesFromFormCalled) @@ -52,7 +64,7 @@ func TestTransformRequest_error_invalid_request_body_json(t *testing.T) { mock := &mocks.MockRequestBody{} - ok := TransformRequest(mock, r) + ok := TransformRequest(mock, r, false) assert.False(t, ok) assert.Equal(t, "", mock.RequestFieldStr) @@ -64,7 +76,7 @@ func TestTransformRequest_error_failure_to_parse_form_xml(t *testing.T) { mock := &mocks.MockRequestBody{} - ok := TransformRequest(mock, r) + ok := TransformRequest(mock, r, false) assert.False(t, ok) assert.False(t, mock.SetAttributesFromFormCalled) @@ -79,7 +91,7 @@ func TestTransformRequest_error_invalid_request_body_xml(t *testing.T) { mock := &mocks.MockRequestBody{} - ok := TransformRequest(mock, r) + ok := TransformRequest(mock, r, false) assert.False(t, ok) assert.False(t, mock.SetAttributesFromFormCalled) diff --git a/smoke_tests/fixtures/requests.go b/smoke_tests/fixtures/requests.go index 14f3c7be..39cf15be 100644 --- a/smoke_tests/fixtures/requests.go +++ b/smoke_tests/fixtures/requests.go @@ -24,13 +24,23 @@ var GetQueueAttributesRequestBodyXML = struct { Action: "GetQueueAttributes", Version: "2012-11-05", Attribute1: "All", - QueueUrl: fmt.Sprintf("%s/new-queue-1", af.BASE_URL), + QueueUrl: af.QueueUrl, +} + +var SetQueueAttributesRequestBodyXML = struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` +}{ + Action: "SetQueueAttributes", + Version: "2012-11-05", + QueueUrl: af.QueueUrl, } var CreateQueueV1RequestBodyJSON = models.CreateQueueRequest{ QueueName: af.QueueName, Version: "2012-11-05", - Attributes: af.CreateQueueAttributes, + Attributes: af.QueueAttributes, } var CreateQueueV1RequestXML_NoAttributes = struct { diff --git a/smoke_tests/sqs_get_queue_attributes_test.go b/smoke_tests/sqs_get_queue_attributes_test.go index ad7298d3..3a1a8576 100644 --- a/smoke_tests/sqs_get_queue_attributes_test.go +++ b/smoke_tests/sqs_get_queue_attributes_test.go @@ -55,9 +55,8 @@ func Test_GetQueueAttributes_json_all(t *testing.T) { Attributes: attributes, }) - queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ - QueueUrl: &queueUrl, + QueueUrl: &af.QueueUrl, AttributeNames: []types.QueueAttributeName{"All"}, }) @@ -93,9 +92,8 @@ func Test_GetQueueAttributes_json_specific_attributes(t *testing.T) { }, }) - queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ - QueueUrl: &queueUrl, + QueueUrl: &af.QueueUrl, AttributeNames: []types.QueueAttributeName{"DelaySeconds"}, }) @@ -137,9 +135,8 @@ func Test_GetQueueAttributes_json_missing_attribute_name_returns_all(t *testing. Attributes: attributes, }) - queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ - QueueUrl: &queueUrl, + QueueUrl: &af.QueueUrl, }) dupe, _ := copystructure.Copy(attributes) @@ -242,7 +239,7 @@ func Test_GetQueueAttributes_xml_select_attributes(t *testing.T) { }{ Action: "GetQueueAttributes", Version: "2012-11-05", - QueueUrl: fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName), + QueueUrl: af.QueueUrl, } r := e.POST("/"). @@ -308,7 +305,7 @@ func Test_GetQueueAttributes_xml_missing_attribute_name_returns_all(t *testing.T }{ Action: "GetQueueAttributes", Version: "2012-11-05", - QueueUrl: fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName), + QueueUrl: af.QueueUrl, } r := e.POST("/"). diff --git a/smoke_tests/sqs_list_queues_test.go b/smoke_tests/sqs_list_queues_test.go index 58fd44b2..a33c56db 100644 --- a/smoke_tests/sqs_list_queues_test.go +++ b/smoke_tests/sqs_list_queues_test.go @@ -67,7 +67,7 @@ func Test_ListQueues_json_multiple_queues(t *testing.T) { assert.Nil(t, err) - assert.Contains(t, sdkResponse.QueueUrls, fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName)) + assert.Contains(t, sdkResponse.QueueUrls, af.QueueUrl) assert.Contains(t, sdkResponse.QueueUrls, fmt.Sprintf("%s/new-queue-2", af.BASE_URL)) assert.Contains(t, sdkResponse.QueueUrls, fmt.Sprintf("%s/new-queue-3", af.BASE_URL)) } @@ -164,7 +164,7 @@ func Test_ListQueues_xml_multiple_queues(t *testing.T) { response := models.ListQueuesResponse{} xml.Unmarshal([]byte(r), &response) - assert.Contains(t, response.Result.QueueUrls, fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName)) + assert.Contains(t, response.Result.QueueUrls, af.QueueUrl) assert.Contains(t, response.Result.QueueUrls, fmt.Sprintf("%s/new-queue-2", af.BASE_URL)) assert.Contains(t, response.Result.QueueUrls, fmt.Sprintf("%s/new-queue-3", af.BASE_URL)) } diff --git a/smoke_tests/sqs_set_queue_attributes_test.go b/smoke_tests/sqs_set_queue_attributes_test.go new file mode 100644 index 00000000..513a1dfd --- /dev/null +++ b/smoke_tests/sqs_set_queue_attributes_test.go @@ -0,0 +1,243 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" + "github.com/gavv/httpexpect/v2" + + "github.com/mitchellh/copystructure" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/aws/aws-sdk-go-v2/service/sqs/types" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/stretchr/testify/assert" +) + +func Test_SetQueueAttributes_json_multiple(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + queueName := "unit-queue1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName, + }) + attributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", + } + + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, queueName) + _, err := sqsClient.SetQueueAttributes(context.TODO(), &sqs.SetQueueAttributesInput{ + QueueUrl: &queueUrl, + Attributes: attributes, + }) + + assert.Nil(t, err) + + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &queueUrl, + AttributeNames: []types.QueueAttributeName{"All"}, + }) + + dupe, _ := copystructure.Copy(attributes) + expectedAttributes, _ := dupe.(map[string]string) + expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue) + expectedAttributes["ApproximateNumberOfMessages"] = "0" + expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" + expectedAttributes["CreatedTimestamp"] = "0000000000" + expectedAttributes["LastModifiedTimestamp"] = "0000000000" + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, queueName) + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} + +func Test_SetQueueAttributes_json_single(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + queueName := "unit-queue1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName, + }) + attributes := map[string]string{ + "DelaySeconds": "1", + } + + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, queueName) + _, err := sqsClient.SetQueueAttributes(context.TODO(), &sqs.SetQueueAttributesInput{ + QueueUrl: &queueUrl, + Attributes: attributes, + }) + + assert.Nil(t, err) + + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &queueUrl, + AttributeNames: []types.QueueAttributeName{"All"}, + }) + + dupe, _ := copystructure.Copy(attributes) + expectedAttributes, _ := dupe.(map[string]string) + expectedAttributes["ReceiveMessageWaitTimeSeconds"] = "0" + expectedAttributes["VisibilityTimeout"] = "0" + expectedAttributes["MaximumMessageSize"] = "0" + expectedAttributes["MessageRetentionPeriod"] = "0" + expectedAttributes["ApproximateNumberOfMessages"] = "0" + expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" + expectedAttributes["CreatedTimestamp"] = "0000000000" + expectedAttributes["LastModifiedTimestamp"] = "0000000000" + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, queueName) + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} + +func Test_SetQueueAttributes_xml_all(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + e.POST("/"). + WithForm(sf.SetQueueAttributesRequestBodyXML). + WithFormField("Attribute.1.Name", "VisibilityTimeout"). + WithFormField("Attribute.1.Value", "5"). + WithFormField("Attribute.2.Name", "MaximumMessageSize"). + WithFormField("Attribute.2.Value", "2"). + WithFormField("Attribute.3.Name", "DelaySeconds"). + WithFormField("Attribute.3.Value", "1"). + WithFormField("Attribute.4.Name", "MessageRetentionPeriod"). + WithFormField("Attribute.4.Value", "3"). + WithFormField("Attribute.5.Name", "Policy"). + WithFormField("Attribute.5.Value", "{\"this-is\": \"the-policy\"}"). + WithFormField("Attribute.6.Name", "ReceiveMessageWaitTimeSeconds"). + WithFormField("Attribute.6.Value", "4"). + WithFormField("Attribute.7.Name", "RedrivePolicy"). + WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:%s\"}", af.BASE_ARN, redriveQueue)). + WithFormField("Attribute.8.Name", "RedriveAllowPolicy"). + WithFormField("Attribute.8.Value", "{\"this-is\": \"the-redrive-allow-policy\"}"). + Expect(). + Status(http.StatusOK). + Body().Raw() + + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &af.QueueUrl, + AttributeNames: []types.QueueAttributeName{"All"}, + }) + + expectedAttributes := map[string]string{ + "DelaySeconds": "1", + "MaximumMessageSize": "2", + "MessageRetentionPeriod": "3", + //"Policy": "{\"this-is\": \"the-policy\"}", + "ReceiveMessageWaitTimeSeconds": "4", + "VisibilityTimeout": "5", + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "0000000000", + "LastModifiedTimestamp": "0000000000", + "QueueArn": fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName), + } + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} + +func Test_SetQueueAttributes_xml_single(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + redriveQueue := "redrive-queue" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &redriveQueue, + }) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + e.POST("/"). + WithForm(sf.SetQueueAttributesRequestBodyXML). + WithFormField("Attribute.1.Name", "VisibilityTimeout"). + WithFormField("Attribute.1.Value", "5"). + Expect(). + Status(http.StatusOK). + Body().Raw() + + sdkResponse, err := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &af.QueueUrl, + AttributeNames: []types.QueueAttributeName{"All"}, + }) + + expectedAttributes := map[string]string{ + "DelaySeconds": "0", + "MaximumMessageSize": "0", + "MessageRetentionPeriod": "0", + "ReceiveMessageWaitTimeSeconds": "0", + "VisibilityTimeout": "5", + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "0000000000", + "LastModifiedTimestamp": "0000000000", + "QueueArn": fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName), + } + assert.Nil(t, err) + assert.Equal(t, expectedAttributes, sdkResponse.Attributes) +} From fcdd20863ddaf0f07af61d497a41eba7803a7af9 Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Mon, 3 Jun 2024 16:41:31 -0400 Subject: [PATCH 24/41] Add PurgeQueueV1 for JSON support --- app/gosqs/get_queue_attributes.go | 3 +- app/gosqs/gosqs.go | 30 ------- app/gosqs/purge_queue.go | 43 +++++++++ app/gosqs/purge_queue_test.go | 125 +++++++++++++++++++++++++++ app/gosqs/send_message.go | 2 +- app/models/models.go | 10 +++ app/models/responses.go | 14 +++ app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sqs_messages.go | 6 -- smoke_tests/sqs_create_queue_test.go | 3 - smoke_tests/sqs_purge_queue_test.go | 125 +++++++++++++++++++++++++++ 12 files changed, 321 insertions(+), 44 deletions(-) create mode 100644 app/gosqs/purge_queue.go create mode 100644 app/gosqs/purge_queue_test.go create mode 100644 smoke_tests/sqs_purge_queue_test.go diff --git a/app/gosqs/get_queue_attributes.go b/app/gosqs/get_queue_attributes.go index 280c819a..32af7dff 100644 --- a/app/gosqs/get_queue_attributes.go +++ b/app/gosqs/get_queue_attributes.go @@ -61,10 +61,10 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo queueAttributes := make([]models.Attribute, 0, 0) app.SyncQueues.RLock() + defer app.SyncQueues.RUnlock() queue, ok := app.SyncQueues.Queues[queueName] if !ok { log.Errorf("Get Queue URL: %s queue does not exist!!!", queueName) - app.SyncQueues.RUnlock() return createErrorResponseV1(ErrInvalidParameterValue.Type) } @@ -126,7 +126,6 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo attr := models.Attribute{Name: "RedrivePolicy", Value: fmt.Sprintf(`{"maxReceiveCount":"%d", "deadLetterTargetArn":"%s"}`, queue.MaxReceiveCount, queue.DeadLetterQueue.Arn)} queueAttributes = append(queueAttributes, attr) } - app.SyncQueues.RUnlock() respStruct := models.GetQueueAttributesResponse{ models.BASE_XMLNS, diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 8a678bf7..033b3179 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -362,36 +362,6 @@ func DeleteQueue(w http.ResponseWriter, req *http.Request) { } } -func PurgeQueue(w http.ResponseWriter, req *http.Request) { - // Sent response type - w.Header().Set("Content-Type", "application/xml") - - // Retrieve FormValues required - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - - uriSegments := strings.Split(queueUrl, "/") - queueName := uriSegments[len(uriSegments)-1] - - log.Println("Purging Queue:", queueName) - - app.SyncQueues.Lock() - if _, ok := app.SyncQueues.Queues[queueName]; ok { - app.SyncQueues.Queues[queueName].Messages = nil - app.SyncQueues.Queues[queueName].Duplicates = make(map[string]time.Time) - respStruct := app.PurgeQueueResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - createErrorResponse(w, req, "GeneralError") - } - } else { - log.Println("Purge Queue:", queueName, ", queue does not exist!!!") - createErrorResponse(w, req, "QueueNotFound") - } - app.SyncQueues.Unlock() -} - func GetQueueUrl(w http.ResponseWriter, req *http.Request) { // Sent response type w.Header().Set("Content-Type", "application/xml") diff --git a/app/gosqs/purge_queue.go b/app/gosqs/purge_queue.go new file mode 100644 index 00000000..b2562f4a --- /dev/null +++ b/app/gosqs/purge_queue.go @@ -0,0 +1,43 @@ +package gosqs + +import ( + "net/http" + "strings" + "time" + + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + log "github.com/sirupsen/logrus" +) + +func PurgeQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewPurgeQueueRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - PurgeQueueV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + uriSegments := strings.Split(requestBody.QueueUrl, "/") + queueName := uriSegments[len(uriSegments)-1] + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + log.Errorf("Purge Queue: %s, queue does not exist!!!", queueName) + return createErrorResponseV1("QueueNotFound") + } + + log.Infof("Purging Queue: %s", queueName) + app.SyncQueues.Queues[queueName].Messages = nil + app.SyncQueues.Queues[queueName].Duplicates = make(map[string]time.Time) + + respStruct := models.PurgeQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + return http.StatusOK, respStruct +} diff --git a/app/gosqs/purge_queue_test.go b/app/gosqs/purge_queue_test.go new file mode 100644 index 00000000..d5a14c4d --- /dev/null +++ b/app/gosqs/purge_queue_test.go @@ -0,0 +1,125 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/Admiral-Piett/goaws/app/conf" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestPurgeQueueV1_success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PurgeQueueRequest) + *v = models.PurgeQueueRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + return true + } + + // Put a message on the queue + targetQueue := app.SyncQueues.Queues["unit-queue1"] + app.SyncQueues.Lock() + targetQueue.Messages = []app.Message{app.Message{}} + targetQueue.Duplicates = map[string]time.Time{ + "dedupe-id": time.Now(), + } + app.SyncQueues.Unlock() + + expectedResponse := models.PurgeQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := PurgeQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) + + assert.Nil(t, targetQueue.Messages) + assert.Equal(t, map[string]time.Time{}, targetQueue.Duplicates) +} + +func TestPurgeQueueV1_success_no_messages_on_queue(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PurgeQueueRequest) + *v = models.PurgeQueueRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + return true + } + + expectedResponse := models.PurgeQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := PurgeQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) + + targetQueue := app.SyncQueues.Queues["unit-queue1"] + assert.Nil(t, targetQueue.Messages) + assert.Equal(t, map[string]time.Time{}, targetQueue.Duplicates) +} + +func TestPurgeQueueV1_request_transformer_error(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PurgeQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestPurgeQueueV1_requested_queue_does_not_exist(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PurgeQueueRequest) + *v = models.PurgeQueueRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "garbage"), + } + return true + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := PurgeQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index a20931e5..e362019d 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -21,7 +21,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewSendMessageRequest() ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { - log.Error("Invalid Request - CreateQueueV1") + log.Error("Invalid Request - SendMessageV1") return createErrorResponseV1(ErrInvalidParameterValue.Type) } messageBody := requestBody.MessageBody diff --git a/app/models/models.go b/app/models/models.go index 2f4ad18a..dcba66dc 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -445,3 +445,13 @@ type DeleteMessageRequest struct { } func (r *DeleteMessageRequest) SetAttributesFromForm(values url.Values) {} + +func NewPurgeQueueRequest() *PurgeQueueRequest { + return &PurgeQueueRequest{} +} + +type PurgeQueueRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} + +func (r *PurgeQueueRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/models/responses.go b/app/models/responses.go index 38bd3e5f..a3f999d8 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -236,3 +236,17 @@ func (r SetQueueAttributesResponse) GetResult() interface{} { func (r SetQueueAttributesResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Purge Queue Response */ +type PurgeQueueResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r PurgeQueueResponse) GetResult() interface{} { + return nil +} + +func (r PurgeQueueResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index 4eac5bd4..ae8fc821 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -71,6 +71,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "ReceiveMessage": sqs.ReceiveMessageV1, "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, "DeleteMessage": sqs.DeleteMessageV1, + "PurgeQueue": sqs.PurgeQueueV1, } var routingTable = map[string]http.HandlerFunc{ @@ -78,7 +79,6 @@ var routingTable = map[string]http.HandlerFunc{ "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, "GetQueueUrl": sqs.GetQueueUrl, - "PurgeQueue": sqs.PurgeQueue, "DeleteQueue": sqs.DeleteQueue, // SNS diff --git a/app/router/router_test.go b/app/router/router_test.go index e703bae8..e53f1956 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -268,13 +268,13 @@ func TestActionHandler_v0_xml(t *testing.T) { "ReceiveMessage": sqs.ReceiveMessageV1, "DeleteMessage": sqs.DeleteMessageV1, "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, + "PurgeQueue": sqs.PurgeQueueV1, } routingTable = map[string]http.HandlerFunc{ // SQS "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, "GetQueueUrl": sqs.GetQueueUrl, - "PurgeQueue": sqs.PurgeQueue, "DeleteQueue": sqs.DeleteQueue, // SNS diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 60156d37..976bfec0 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -59,12 +59,6 @@ type SendMessageBatchResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` } -/*** Purge Queue Response */ -type PurgeQueueResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} - /*** Get Queue Url Response */ type GetQueueUrlResult struct { QueueUrl string `xml:"QueueUrl,omitempty"` diff --git a/smoke_tests/sqs_create_queue_test.go b/smoke_tests/sqs_create_queue_test.go index 1789badc..b7c0c303 100644 --- a/smoke_tests/sqs_create_queue_test.go +++ b/smoke_tests/sqs_create_queue_test.go @@ -27,9 +27,6 @@ import ( "github.com/gavv/httpexpect/v2" ) -// TODO - Is there a way to also capture the defaults we set and/or load from the config here? (review the xml -// code below) -// NOTE: Actually I think you can just adjust the app.CurrentEnvironment memory space...it travels across tests it seems. func Test_CreateQueueV1_json_no_attributes(t *testing.T) { server := generateServer() defer func() { diff --git a/smoke_tests/sqs_purge_queue_test.go b/smoke_tests/sqs_purge_queue_test.go new file mode 100644 index 00000000..d4d6db5e --- /dev/null +++ b/smoke_tests/sqs_purge_queue_test.go @@ -0,0 +1,125 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + "time" + + "github.com/Admiral-Piett/goaws/app/models" + "github.com/gavv/httpexpect/v2" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + "github.com/stretchr/testify/assert" + + af "github.com/Admiral-Piett/goaws/app/fixtures" +) + +func Test_PurgeQueueV1_json(t *testing.T) { + defaultEnvironment := app.CurrentEnvironment + app.CurrentEnvironment = app.Environment{ + EnableDuplicates: true, + } + server := generateServer() + defer func() { + server.Close() + utils.ResetApp() + app.CurrentEnvironment = defaultEnvironment + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + qName := fmt.Sprintf("%s.fifo", af.QueueName) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &qName, + }) + + messageBody := "test-message" + dedupeId := "dedupe-id" + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + QueueUrl: &qName, + MessageBody: &messageBody, + MessageDeduplicationId: &dedupeId, + }) + + sdkResponse, err := sqsClient.PurgeQueue(context.TODO(), &sqs.PurgeQueueInput{ + QueueUrl: &qName, + }) + + assert.Nil(t, err) + assert.NotNil(t, sdkResponse) + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + targetQueue := app.SyncQueues.Queues[qName] + assert.Nil(t, targetQueue.Messages) + assert.Equal(t, map[string]time.Time{}, targetQueue.Duplicates) +} + +func Test_PurgeQueueV1_xml(t *testing.T) { + defaultEnvironment := app.CurrentEnvironment + app.CurrentEnvironment = app.Environment{ + EnableDuplicates: true, + } + server := generateServer() + defer func() { + server.Close() + utils.ResetApp() + app.CurrentEnvironment = defaultEnvironment + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + qName := fmt.Sprintf("%s.fifo", af.QueueName) + sdkResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &qName, + }) + + messageBody := "test-message" + dedupeId := "dedupe-id" + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + QueueUrl: &qName, + MessageBody: &messageBody, + MessageDeduplicationId: &dedupeId, + }) + + r := e.POST("/"). + WithForm(struct { + Action string `xml:"Action"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "PurgeQueue", + QueueUrl: *sdkResponse.QueueUrl, + }). + Expect(). + Status(http.StatusOK). + Body().Raw() + + expected := models.PurgeQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + response := models.PurgeQueueResponse{} + xml.Unmarshal([]byte(r), &response) + assert.Equal(t, expected, response) + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + targetQueue := app.SyncQueues.Queues[qName] + assert.Nil(t, targetQueue.Messages) + assert.Equal(t, map[string]time.Time{}, targetQueue.Duplicates) +} From a65cc66785b369de7d89af8805ad8332dfbc4878 Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Mon, 3 Jun 2024 15:58:15 +0900 Subject: [PATCH 25/41] Add GetQueueUrlV1 for JSON support --- app/gosqs/get_queue_url.go | 38 ++++++++ app/gosqs/get_queue_url_test.go | 102 +++++++++++++++++++ app/gosqs/gosqs.go | 23 ----- app/models/models.go | 12 +++ app/models/responses.go | 19 ++++ app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sqs_messages.go | 11 --- smoke_tests/sqs_get_queue_url_test.go | 135 ++++++++++++++++++++++++++ 9 files changed, 308 insertions(+), 36 deletions(-) create mode 100644 app/gosqs/get_queue_url.go create mode 100644 app/gosqs/get_queue_url_test.go create mode 100644 smoke_tests/sqs_get_queue_url_test.go diff --git a/app/gosqs/get_queue_url.go b/app/gosqs/get_queue_url.go new file mode 100644 index 00000000..9c467f49 --- /dev/null +++ b/app/gosqs/get_queue_url.go @@ -0,0 +1,38 @@ +package gosqs + +import ( + "net/http" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + log "github.com/sirupsen/logrus" +) + +func GetQueueUrlV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + + requestBody := models.NewGetQueueUrlRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - GetQueueUrlV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + queueName := requestBody.QueueName + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + log.Error("Get Queue URL:", queueName, ", queue does not exist!!!") + return createErrorResponseV1("QueueNotFound") + } + + queue := app.SyncQueues.Queues[queueName] + log.Debug("Get Queue URL:", queue.Name) + + result := models.GetQueueUrlResult{QueueUrl: queue.URL} + respStruct := models.GetQueueUrlResponse{ + Xmlns: models.BASE_XMLNS, + Result: result, + Metadata: models.BASE_RESPONSE_METADATA, + } + return http.StatusOK, respStruct +} diff --git a/app/gosqs/get_queue_url_test.go b/app/gosqs/get_queue_url_test.go new file mode 100644 index 00000000..2dc70352 --- /dev/null +++ b/app/gosqs/get_queue_url_test.go @@ -0,0 +1,102 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/stretchr/testify/assert" +) + +func TestGetQueueUrlV1_success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.GetQueueUrlRequest) + *v = models.GetQueueUrlRequest{ + QueueName: "unit-queue1", + QueueOwnerAWSAccountId: "fugafuga", + } + return true + } + + _, r := utils.GenerateRequestInfo( + "POST", + "/", + nil, + true) + code, response := GetQueueUrlV1(r) + + get_queue_url_response := response.(models.GetQueueUrlResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Contains(t, get_queue_url_response.Result.QueueUrl, fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1")) + +} + +func TestGetQueueUrlV1_error_no_queue(t *testing.T) { + + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.GetQueueUrlRequest) + *v = models.GetQueueUrlRequest{ + QueueName: "not-exist-unit-queue1", + QueueOwnerAWSAccountId: "fugafuga", + } + return true + } + + _, r := utils.GenerateRequestInfo( + "POST", + "/", + nil, + true) + code, response := GetQueueUrlV1(r) + + expected := models.ErrorResult{ + Type: "Not Found", + Code: "AWS.SimpleQueueService.NonExistentQueue", + Message: "The specified queue does not exist for this wsdl version.", + } + + assert.Equal(t, http.StatusBadRequest, code) + assert.Equal(t, response.GetResult().(models.ErrorResult), expected) +} + +func TestGetQueueUrlV1_error_request_transformer(t *testing.T) { + + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo( + "POST", + "/", + nil, + true) + code, _ := GetQueueUrlV1(r) + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 033b3179..0fc32ece 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -362,29 +362,6 @@ func DeleteQueue(w http.ResponseWriter, req *http.Request) { } } -func GetQueueUrl(w http.ResponseWriter, req *http.Request) { - // Sent response type - w.Header().Set("Content-Type", "application/xml") - // - //// Retrieve FormValues required - queueName := req.FormValue("QueueName") - if queue, ok := app.SyncQueues.Queues[queueName]; ok { - url := queue.URL - log.Println("Get Queue URL:", queueName) - // Create, encode/xml and send response - result := app.GetQueueUrlResult{QueueUrl: url} - respStruct := app.GetQueueUrlResponse{"http://queue.amazonaws.com/doc/2012-11-05/", result, app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } - } else { - log.Println("Get Queue URL:", queueName, ", queue does not exist!!!") - createErrorResponse(w, req, "QueueNotFound") - } -} - func getQueueFromPath(formVal string, theUrl string) string { if formVal != "" { return formVal diff --git a/app/models/models.go b/app/models/models.go index dcba66dc..6efd1501 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -242,6 +242,18 @@ func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { } } +// Get Queue Url Request +func NewGetQueueUrlRequest() *GetQueueUrlRequest { + return &GetQueueUrlRequest{} +} + +type GetQueueUrlRequest struct { + QueueName string `json:"QueueName"` + QueueOwnerAWSAccountId string `json:"QueueOwnerAWSAccountId"` // NOTE: not implemented +} + +func (r *GetQueueUrlRequest) SetAttributesFromForm(values url.Values) {} + func NewSetQueueAttributesRequest() *SetQueueAttributesRequest { return &SetQueueAttributesRequest{} } diff --git a/app/models/responses.go b/app/models/responses.go index a3f999d8..157f69bf 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -224,6 +224,25 @@ func (r DeleteMessageResponse) GetRequestId() string { return r.Metadata.RequestId } +/*** Get Queue Url Response */ +type GetQueueUrlResult struct { + QueueUrl string `xml:"QueueUrl,omitempty"` +} + +type GetQueueUrlResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Result GetQueueUrlResult `xml:"GetQueueUrlResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r GetQueueUrlResponse) GetResult() interface{} { + return r.Result +} + +func (r GetQueueUrlResponse) GetRequestId() string { + return r.Metadata.RequestId +} + type SetQueueAttributesResponse struct { Xmlns string `xml:"xmlns,attr,omitempty"` Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` diff --git a/app/router/router.go b/app/router/router.go index ae8fc821..049db0ce 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -71,6 +71,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "ReceiveMessage": sqs.ReceiveMessageV1, "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, "DeleteMessage": sqs.DeleteMessageV1, + "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, } @@ -78,7 +79,6 @@ var routingTable = map[string]http.HandlerFunc{ // SQS "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, - "GetQueueUrl": sqs.GetQueueUrl, "DeleteQueue": sqs.DeleteQueue, // SNS diff --git a/app/router/router_test.go b/app/router/router_test.go index e53f1956..8f79a2b1 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -268,13 +268,13 @@ func TestActionHandler_v0_xml(t *testing.T) { "ReceiveMessage": sqs.ReceiveMessageV1, "DeleteMessage": sqs.DeleteMessageV1, "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, + "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, } routingTable = map[string]http.HandlerFunc{ // SQS "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, - "GetQueueUrl": sqs.GetQueueUrl, "DeleteQueue": sqs.DeleteQueue, // SNS diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 976bfec0..10fb0e01 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -59,17 +59,6 @@ type SendMessageBatchResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` } -/*** Get Queue Url Response */ -type GetQueueUrlResult struct { - QueueUrl string `xml:"QueueUrl,omitempty"` -} - -type GetQueueUrlResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Result GetQueueUrlResult `xml:"GetQueueUrlResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} - type SetQueueAttributesResponse struct { Xmlns string `xml:"xmlns,attr,omitempty"` Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` diff --git a/smoke_tests/sqs_get_queue_url_test.go b/smoke_tests/sqs_get_queue_url_test.go new file mode 100644 index 00000000..1d075729 --- /dev/null +++ b/smoke_tests/sqs_get_queue_url_test.go @@ -0,0 +1,135 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_GetQueueUrlV1_json_success_retrieve_queue_url(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + getQueueUrlOutput, _ := sqsClient.GetQueueUrl(context.TODO(), &sqs.GetQueueUrlInput{ + QueueName: &af.QueueName, + }) + assert.Contains(t, string(*getQueueUrlOutput.QueueUrl), fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName)) +} + +func Test_GetQueueUrlV1_json_error_not_found_queue(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + getQueueUrlOutput, err := sqsClient.GetQueueUrl(context.TODO(), &sqs.GetQueueUrlInput{ + QueueName: &af.QueueName}) + + assert.Contains(t, err.Error(), "400") + assert.Contains(t, err.Error(), "AWS.SimpleQueueService.NonExistentQueue") + assert.Contains(t, err.Error(), "The specified queue does not exist for this wsdl version.") + assert.Nil(t, getQueueUrlOutput) +} + +func Test_GetQueueUrlV1_xml_success_retrieve_queue_url(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + e := httpexpect.Default(t, server.URL) + + getQueueUrlRequestBodyXML := struct { + Action string `xml:"Action"` + QueueName string `xml:"QueueName"` + QueueOwnerAWSAccountId string `xml:"QueueOwnerAWSAccountId"` + Version string `xml:"Version"` + }{ + Action: "GetQueueUrl", + QueueName: af.QueueName, + QueueOwnerAWSAccountId: "hogehoge", + Version: "2012-11-05", + } + + r := e.POST("/"). + WithForm(getQueueUrlRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + r1 := models.GetQueueUrlResponse{} + xml.Unmarshal([]byte(r), &r1) + + assert.Contains(t, string(r1.Result.QueueUrl), fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName)) +} + +func Test_GetQueueUrlV1_xml_error_not_found_queue(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + getQueueUrlRequestBodyXML := struct { + Action string `xml:"Action"` + QueueName string `xml:"QueueName"` + QueueOwnerAWSAccountId string `xml:"QueueOwnerAWSAccountId"` + Version string `xml:"Version"` + }{ + Action: "GetQueueUrl", + QueueName: af.QueueName, + QueueOwnerAWSAccountId: "hogehoge", + Version: "2012-11-05", + } + + r := e.POST("/"). + WithForm(getQueueUrlRequestBodyXML). + Expect(). + Status(http.StatusBadRequest). + Body().Raw() + + r1 := models.ErrorResponse{} + xml.Unmarshal([]byte(r), &r1) + + assert.Contains(t, r1.Result.Type, "Not Found") + assert.Contains(t, r1.Result.Code, "AWS.SimpleQueueService.NonExistentQueue") + assert.Contains(t, r1.Result.Message, "The specified queue does not exist for this wsdl version.") +} From 20c9e6cd73835b07941bb2f0374cabb00f20b8c5 Mon Sep 17 00:00:00 2001 From: ksaiki Date: Mon, 10 Jun 2024 10:01:46 +0900 Subject: [PATCH 26/41] update --- app/gosqs/change_message_visibility.go | 5 +++-- app/gosqs/delete_message.go | 5 ++++- app/gosqs/get_queue_attributes.go | 6 +++--- app/gosqs/receive_message.go | 8 +++----- app/gosqs/send_message.go | 6 ++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/gosqs/change_message_visibility.go b/app/gosqs/change_message_visibility.go index 8678cd4c..be5efa87 100644 --- a/app/gosqs/change_message_visibility.go +++ b/app/gosqs/change_message_visibility.go @@ -75,8 +75,9 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo } respStruct := models.ChangeMessageVisibilityResult{ - "http://queue.amazonaws.com/doc/2012-11-05/", - app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } return http.StatusOK, &respStruct } diff --git a/app/gosqs/delete_message.go b/app/gosqs/delete_message.go index 50de84d1..b43eabbe 100644 --- a/app/gosqs/delete_message.go +++ b/app/gosqs/delete_message.go @@ -50,7 +50,10 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { delete(app.SyncQueues.Queues[queueName].Duplicates, msg.DeduplicationID) // Create, encode/xml and send response - respStruct := models.DeleteMessageResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} + respStruct := models.DeleteMessageResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } return 200, &respStruct } } diff --git a/app/gosqs/get_queue_attributes.go b/app/gosqs/get_queue_attributes.go index 32af7dff..cd7102ca 100644 --- a/app/gosqs/get_queue_attributes.go +++ b/app/gosqs/get_queue_attributes.go @@ -128,9 +128,9 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo } respStruct := models.GetQueueAttributesResponse{ - models.BASE_XMLNS, - models.GetQueueAttributesResult{Attrs: queueAttributes}, - models.BASE_RESPONSE_METADATA, + Xmlns: models.BASE_XMLNS, + Result: models.GetQueueAttributesResult{Attrs: queueAttributes}, + Metadata: models.BASE_RESPONSE_METADATA, } return http.StatusOK, respStruct } diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go index 613ded75..9a5cc73c 100644 --- a/app/gosqs/receive_message.go +++ b/app/gosqs/receive_message.go @@ -67,11 +67,9 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) case <-req.Context().Done(): continueTimer.Stop() return http.StatusOK, models.ReceiveMessageResponse{ - "http://queue.amazonaws.com/doc/2012-11-05/", - models.ReceiveMessageResult{}, - app.ResponseMetadata{ - RequestId: "00000000-0000-0000-0000-000000000000", - }, + Xmlns: models.BASE_XMLNS, + Result: models.ReceiveMessageResult{}, + Metadata: models.BASE_RESPONSE_METADATA, } case <-continueTimer.C: continueTimer.Stop() diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index e362019d..ef3f2b2e 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -88,16 +88,14 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) respStruct := models.SendMessageResponse{ - Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Xmlns: models.BASE_XMLNS, Result: models.SendMessageResult{ MD5OfMessageAttributes: msg.MD5OfMessageAttributes, MD5OfMessageBody: msg.MD5OfMessageBody, MessageId: msg.Uuid, SequenceNumber: fifoSeqNumber, }, - Metadata: app.ResponseMetadata{ - RequestId: "00000000-0000-0000-0000-000000000000", - }, + Metadata: models.BASE_RESPONSE_METADATA, } return http.StatusOK, respStruct From 1fd4ded847c99298937f047d802b64189259c3d1 Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Tue, 18 Jun 2024 14:08:09 -0400 Subject: [PATCH 27/41] Add DeleteQueueV1 for JSON support --- app/gosqs/delete_queue.go | 38 ++++++++++ app/gosqs/delete_queue_test.go | 91 ++++++++++++++++++++++++ app/gosqs/get_queue_url.go | 1 - app/gosqs/gosqs.go | 29 -------- app/gosqs/receive_message.go | 3 + app/gosqs/receive_message_test.go | 60 ---------------- app/models/models.go | 10 +++ app/models/responses.go | 14 ++++ app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sqs_messages.go | 21 ------ smoke_tests/sqs_delete_queue_test.go | 92 +++++++++++++++++++++++++ smoke_tests/sqs_receive_message_test.go | 42 ++++++++++- 13 files changed, 289 insertions(+), 116 deletions(-) create mode 100644 app/gosqs/delete_queue.go create mode 100644 app/gosqs/delete_queue_test.go create mode 100644 smoke_tests/sqs_delete_queue_test.go diff --git a/app/gosqs/delete_queue.go b/app/gosqs/delete_queue.go new file mode 100644 index 00000000..4d1d9194 --- /dev/null +++ b/app/gosqs/delete_queue.go @@ -0,0 +1,38 @@ +package gosqs + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app/interfaces" + + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + log "github.com/sirupsen/logrus" +) + +func DeleteQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewDeleteQueueRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - DeleteQueueV1") + return createErrorResponseV1(ErrInvalidParameterValue.Type) + } + + uriSegments := strings.Split(requestBody.QueueUrl, "/") + queueName := uriSegments[len(uriSegments)-1] + + log.Infof("Deleting Queue: %s", queueName) + + app.SyncQueues.Lock() + delete(app.SyncQueues.Queues, queueName) + app.SyncQueues.Unlock() + + respStruct := models.DeleteQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + return http.StatusOK, respStruct +} diff --git a/app/gosqs/delete_queue_test.go b/app/gosqs/delete_queue_test.go new file mode 100644 index 00000000..8c2348cb --- /dev/null +++ b/app/gosqs/delete_queue_test.go @@ -0,0 +1,91 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + + "github.com/stretchr/testify/assert" + + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/fixtures" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" +) + +func TestDeleteQueueV1_success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.DeleteQueueRequest) + *v = models.DeleteQueueRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + return true + } + + expectedResponse := models.DeleteQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := DeleteQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) + + _, ok := app.SyncQueues.Queues["unit-queue1"] + assert.False(t, ok) +} + +func TestDeleteQueueV1_success_unknown_queue(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.DeleteQueueRequest) + *v = models.DeleteQueueRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unknown-queue1"), + } + return true + } + + expectedResponse := models.DeleteQueueResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: models.BASE_RESPONSE_METADATA, + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, response := DeleteQueueV1(r) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, expectedResponse, response) +} + +func TestDeleteQueueV1_success_invalid_request(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + utils.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + return false + } + + _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + code, _ := DeleteQueueV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosqs/get_queue_url.go b/app/gosqs/get_queue_url.go index 9c467f49..109bb51c 100644 --- a/app/gosqs/get_queue_url.go +++ b/app/gosqs/get_queue_url.go @@ -11,7 +11,6 @@ import ( ) func GetQueueUrlV1(req *http.Request) (int, interfaces.AbstractResponseBody) { - requestBody := models.NewGetQueueUrlRequest() ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 0fc32ece..7c463c8e 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -333,35 +333,6 @@ func DeleteMessageBatch(w http.ResponseWriter, req *http.Request) { } } -func DeleteQueue(w http.ResponseWriter, req *http.Request) { - // Sent response type - w.Header().Set("Content-Type", "application/xml") - - // Retrieve FormValues required - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - log.Println("Deleting Queue:", queueName) - app.SyncQueues.Lock() - delete(app.SyncQueues.Queues, queueName) - app.SyncQueues.Unlock() - - // Create, encode/xml and send response - respStruct := app.DeleteQueueResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } -} - func getQueueFromPath(formVal string, theUrl string) string { if formVal != "" { return formVal diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go index 9a5cc73c..349aa558 100644 --- a/app/gosqs/receive_message.go +++ b/app/gosqs/receive_message.go @@ -15,6 +15,9 @@ import ( log "github.com/sirupsen/logrus" ) +// TODO - Admiral-Piett - could we refactor the way we hide messages? Change data structure to a queue +// organized by "reveal time" or a map with the key being a timestamp of when it could be shown? +// Ordered Map - https://github.com/elliotchance/orderedmap func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewReceiveMessageRequest() ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) diff --git a/app/gosqs/receive_message_test.go b/app/gosqs/receive_message_test.go index ea43c57e..2f301d7c 100644 --- a/app/gosqs/receive_message_test.go +++ b/app/gosqs/receive_message_test.go @@ -3,8 +3,6 @@ package gosqs import ( "context" "net/http" - "net/http/httptest" - "net/url" "sync" "testing" "time" @@ -136,64 +134,6 @@ func TestReceiveMessage_CanceledByClientV1(t *testing.T) { } } -func TestReceiveMessage_WithConcurrentDeleteQueueV1(t *testing.T) { - // create a queue - app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT - defer func() { - utils.ResetApp() - }() - - app.SyncQueues.Queues["waiting-queue"] = &app.Queue{ - Name: "waiting-queue", - ReceiveMessageWaitTimeSeconds: 1, - } - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - // receive message - _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ - QueueUrl: "http://localhost:4100/queue/waiting-queue", - }, true) - status, resp := ReceiveMessageV1(r) - assert.Equal(t, http.StatusBadRequest, status) - - // Check the response body is what we expect. - expected := "QueueNotFound" - result := resp.GetResult().(models.ErrorResult) - if result.Type != "Not Found" { - t.Errorf("handler returned unexpected body: got %v want %v", - result.Message, expected) - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - time.Sleep(10 * time.Millisecond) // 10ms to let the ReceiveMessage() block - - // delete queue message - form := url.Values{} - form.Add("Action", "DeleteQueue") - form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") - form.Add("Version", "2012-11-05") - _, r := utils.GenerateRequestInfo("POST", "/", form, false) - - rr := httptest.NewRecorder() - http.HandlerFunc(DeleteQueue).ServeHTTP(rr, r) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got \n%v want %v", - status, http.StatusOK) - } - }() - - if timedout := waitTimeout(&wg, 2*time.Second); timedout { - t.Errorf("concurrent handlers timeout, expecting both to return within timeout") - } -} - func TestReceiveMessageDelaySecondsV1(t *testing.T) { // create a queue app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT diff --git a/app/models/models.go b/app/models/models.go index 6efd1501..3116c499 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -467,3 +467,13 @@ type PurgeQueueRequest struct { } func (r *PurgeQueueRequest) SetAttributesFromForm(values url.Values) {} + +func NewDeleteQueueRequest() *DeleteQueueRequest { + return &DeleteQueueRequest{} +} + +type DeleteQueueRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} + +func (r *DeleteQueueRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/models/responses.go b/app/models/responses.go index 157f69bf..f223941c 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -269,3 +269,17 @@ func (r PurgeQueueResponse) GetResult() interface{} { func (r PurgeQueueResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Delete Queue Response */ +type DeleteQueueResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r DeleteQueueResponse) GetResult() interface{} { + return nil +} + +func (r DeleteQueueResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/router/router.go b/app/router/router.go index 049db0ce..70d4a6ca 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -73,13 +73,13 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "DeleteMessage": sqs.DeleteMessageV1, "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, + "DeleteQueue": sqs.DeleteQueueV1, } var routingTable = map[string]http.HandlerFunc{ // SQS "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, - "DeleteQueue": sqs.DeleteQueue, // SNS "ListTopics": sns.ListTopics, diff --git a/app/router/router_test.go b/app/router/router_test.go index 8f79a2b1..5c1c017b 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -270,12 +270,12 @@ func TestActionHandler_v0_xml(t *testing.T) { "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, + "DeleteQueue": sqs.DeleteQueueV1, } routingTable = map[string]http.HandlerFunc{ // SQS "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, - "DeleteQueue": sqs.DeleteQueue, // SNS "ListTopics": sns.ListTopics, diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 10fb0e01..20da794e 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -1,21 +1,5 @@ package app -/*** List Queues Response */ -type ListQueuesResult struct { - QueueUrl []string `xml:"QueueUrl"` -} - -type ListQueuesResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result ListQueuesResult `xml:"ListQueuesResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - -type DeleteQueueResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} - type DeleteMessageBatchResultEntry struct { Id string `xml:"Id"` } @@ -58,8 +42,3 @@ type SendMessageBatchResponse struct { Result SendMessageBatchResult `xml:"SendMessageBatchResult"` Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` } - -type SetQueueAttributesResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} diff --git a/smoke_tests/sqs_delete_queue_test.go b/smoke_tests/sqs_delete_queue_test.go new file mode 100644 index 00000000..811d7b1e --- /dev/null +++ b/smoke_tests/sqs_delete_queue_test.go @@ -0,0 +1,92 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/gavv/httpexpect/v2" + + "github.com/aws/aws-sdk-go-v2/service/sqs" + + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + + "github.com/Admiral-Piett/goaws/app" + "github.com/stretchr/testify/assert" + + af "github.com/Admiral-Piett/goaws/app/fixtures" +) + +func Test_DeleteQueueV1_json(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + qName := "unit-queue1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &qName, + }) + + qUrl := fmt.Sprintf("%s/%s", af.BASE_URL, qName) + _, err := sqsClient.DeleteQueue(context.TODO(), &sqs.DeleteQueueInput{ + QueueUrl: &qUrl, + }) + + assert.Nil(t, err) + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + + targetQueue, ok := app.SyncQueues.Queues["unit-queue1"] + assert.False(t, ok) + assert.Nil(t, targetQueue) +} + +func Test_DeleteQueueV1_xml(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + qName := "unit-queue1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &qName, + }) + + qUrl := fmt.Sprintf("%s/%s", af.BASE_URL, qName) + + e.POST("/"). + WithForm(struct { + Action string `xml:"Action"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "DeleteQueue", + QueueUrl: qUrl, + }). + Expect(). + Status(http.StatusOK). + Body().Raw() + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + + targetQueue, ok := app.SyncQueues.Queues["unit-queue1"] + assert.False(t, ok) + assert.Nil(t, targetQueue) +} diff --git a/smoke_tests/sqs_receive_message_test.go b/smoke_tests/sqs_receive_message_test.go index c6253a16..7c798647 100644 --- a/smoke_tests/sqs_receive_message_test.go +++ b/smoke_tests/sqs_receive_message_test.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "fmt" "net/http" + "sync" "testing" af "github.com/Admiral-Piett/goaws/app/fixtures" @@ -47,14 +48,49 @@ func Test_ReceiveMessageV1_json(t *testing.T) { QueueUrl: createQueueResponse.QueueUrl, }) - if err != nil { - t.Fatalf("Error receiving message: %v", err) - } + assert.Nil(t, err) assert.Equal(t, 1, len(receiveMessageResponse.Messages)) assert.Equal(t, sf.SendMessageRequestBodyXML.MessageBody, *receiveMessageResponse.Messages[0].Body) } +func Test_ReceiveMessageV1_json_while_concurrent_delete(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + utils.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + Attributes: map[string]string{"ReceiveMessageWaitTimeSeconds": "1"}, + }) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _, err := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + assert.Contains(t, err.Error(), "AWS.SimpleQueueService.NonExistentQueue") + }() + + wg.Add(1) + go func() { + defer wg.Done() + _, err := sqsClient.DeleteQueue(context.TODO(), &sqs.DeleteQueueInput{ + QueueUrl: createQueueResponse.QueueUrl, + }) + assert.Nil(t, err) + }() + wg.Wait() +} + func Test_ReceiveMessageV1_xml(t *testing.T) { server := generateServer() defer func() { From 740700f1bd0b792319a5b461c0c74b1e537961ea Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Tue, 25 Jun 2024 16:06:29 -0400 Subject: [PATCH 28/41] Add SNS SubscribeV1 for new pattern support --- app/cmd/goaws.go | 4 - app/common/common.go | 1 + app/conf/config.go | 7 + app/conf/config_test.go | 18 +- app/conf/goaws.yaml | 1 + app/conf/mock-data/mock-config.yaml | 80 +-- app/examples/java/SnsSample.java | 68 --- app/examples/java/SqsSample.java | 87 ---- app/examples/python/boto_sns_sample.py | 95 ---- app/examples/python/boto_sqs_sample.py | 71 --- app/fixtures/fixtures.go | 3 +- app/fixtures/sqs.go | 2 +- app/gosns/gosns.go | 119 +---- app/gosns/gosns_test.go | 117 +---- app/gosns/subscribe.go | 103 ++++ app/gosns/subscribe_test.go | 170 +++++++ app/gosqs/change_message_visibility.go | 8 +- app/gosqs/change_message_visibility_test.go | 7 +- app/gosqs/create_queue.go | 4 +- app/gosqs/create_queue_test.go | 34 +- app/gosqs/delete_message.go | 4 +- app/gosqs/delete_message_test.go | 7 +- app/gosqs/delete_queue.go | 2 +- app/gosqs/delete_queue_test.go | 16 +- app/gosqs/get_queue_attributes.go | 8 +- app/gosqs/get_queue_attributes_test.go | 28 +- app/gosqs/get_queue_url.go | 4 +- app/gosqs/get_queue_url_test.go | 14 +- app/gosqs/gosqs.go | 37 +- app/gosqs/gosqs_test.go | 7 +- app/gosqs/list_queues.go | 2 +- app/gosqs/list_queues_test.go | 22 +- app/gosqs/purge_queue.go | 4 +- app/gosqs/purge_queue_test.go | 18 +- app/gosqs/queue_attributes.go | 90 +--- app/gosqs/queue_attributes_test.go | 70 +-- app/gosqs/receive_message.go | 6 +- app/gosqs/receive_message_test.go | 29 +- app/gosqs/send_message.go | 6 +- app/gosqs/send_message_test.go | 26 +- app/gosqs/set_queue_attributes.go | 10 +- app/gosqs/set_queue_attributes_test.go | 32 +- app/interfaces/interfaces.go | 7 + app/models/conversions_test.go | 8 +- app/models/errors.go | 61 +++ app/models/models.go | 454 ----------------- app/models/responses.go | 30 +- app/models/sns.go | 68 +++ app/models/sns_test.go | 58 +++ app/models/sqs.go | 457 ++++++++++++++++++ app/models/{models_test.go => sqs_test.go} | 28 +- app/router/router.go | 2 +- app/router/router_test.go | 26 +- app/servertest/server_test.go | 7 - app/sns.go | 9 - app/sns_messages.go | 19 - app/sqs.go | 13 - app/{utils => test}/tests.go | 2 +- app/utils/utils.go | 19 +- app/utils/utils_test.go | 19 +- go.mod | 10 +- go.sum | 12 + smoke_tests/fixtures/responses.go | 2 +- smoke_tests/main_test.go | 12 - smoke_tests/sns_subscribe_test.go | 151 ++++++ .../sqs_change_message_visibility_test.go | 7 +- smoke_tests/sqs_create_queue_test.go | 36 +- smoke_tests/sqs_delete_message_test.go | 7 +- smoke_tests/sqs_delete_queue_test.go | 7 +- smoke_tests/sqs_get_queue_attributes_test.go | 36 +- smoke_tests/sqs_get_queue_url_test.go | 11 +- smoke_tests/sqs_list_queues_test.go | 16 +- smoke_tests/sqs_purge_queue_test.go | 8 +- smoke_tests/sqs_receive_message_test.go | 9 +- smoke_tests/sqs_send_message_test.go | 15 +- smoke_tests/sqs_set_queue_attributes_test.go | 28 +- 76 files changed, 1533 insertions(+), 1562 deletions(-) delete mode 100644 app/examples/java/SnsSample.java delete mode 100644 app/examples/java/SqsSample.java delete mode 100644 app/examples/python/boto_sns_sample.py delete mode 100644 app/examples/python/boto_sqs_sample.py create mode 100644 app/gosns/subscribe.go create mode 100644 app/gosns/subscribe_test.go create mode 100644 app/models/errors.go create mode 100644 app/models/sns.go create mode 100644 app/models/sns_test.go create mode 100644 app/models/sqs.go rename app/models/{models_test.go => sqs_test.go} (97%) rename app/{utils => test}/tests.go (98%) delete mode 100644 smoke_tests/main_test.go create mode 100644 smoke_tests/sns_subscribe_test.go diff --git a/app/cmd/goaws.go b/app/cmd/goaws.go index 4e37de01..709fe0a9 100644 --- a/app/cmd/goaws.go +++ b/app/cmd/goaws.go @@ -6,8 +6,6 @@ import ( "os" "time" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/Admiral-Piett/goaws/app" log "github.com/sirupsen/logrus" @@ -63,8 +61,6 @@ func main() { quit := make(chan struct{}, 0) go gosqs.PeriodicTasks(1*time.Second, quit) - utils.InitializeDecoders() - if len(portNumbers) == 1 { log.Warnf("GoAws listening on: 0.0.0.0:%s", portNumbers[0]) err := http.ListenAndServe("0.0.0.0:"+portNumbers[0], r) diff --git a/app/common/common.go b/app/common/common.go index de4823b1..b1ca694b 100644 --- a/app/common/common.go +++ b/app/common/common.go @@ -17,6 +17,7 @@ import ( var LogMessages bool var LogFile string +// TODO - Admiral-Piett replace with `github.com/google/uuid` - `uuid.NewString()` func NewUUID() (string, error) { uuid := make([]byte, 16) n, err := io.ReadFull(rand.Reader, uuid) diff --git a/app/conf/config.go b/app/conf/config.go index b0052fb3..46792866 100644 --- a/app/conf/config.go +++ b/app/conf/config.go @@ -35,6 +35,13 @@ func LoadYamlConfig(filename string, env string) []string { return ports } } + + filename, _ = filepath.Abs(filename) + if _, err := os.Stat(filename); err != nil { + log.Warnf("Failure to find config file: %s", filename) + return ports + } + log.Infof("Loading config file: %s", filename) yamlFile, err := os.ReadFile(filename) if err != nil { diff --git a/app/conf/config_test.go b/app/conf/config_test.go index 7923f997..f84b1a0a 100644 --- a/app/conf/config_test.go +++ b/app/conf/config_test.go @@ -73,7 +73,7 @@ func TestConfig_QueueAttributes(t *testing.T) { assert.Equal(t, 1024, app.SyncQueues.Queues["local-queue1"].MaximumMessageSize) assert.Equal(t, emptyQueue, app.SyncQueues.Queues["local-queue1"].DeadLetterQueue) assert.Equal(t, 0, app.SyncQueues.Queues["local-queue1"].MaxReceiveCount) - assert.Equal(t, 445600, app.SyncQueues.Queues["local-queue1"].MessageRetentionPeriod) + assert.Equal(t, 345600, app.SyncQueues.Queues["local-queue1"].MessageRetentionPeriod) assert.Equal(t, 100, app.SyncQueues.Queues["local-queue3"].MaxReceiveCount) assert.Equal(t, "local-queue3-dlq", app.SyncQueues.Queues["local-queue3"].DeadLetterQueue.Name) @@ -148,3 +148,19 @@ func TestConfig_LoadYamlConfig_finds_default_config(t *testing.T) { assert.True(t, ok) } } + +func TestConfig_LoadYamlConfig_missing_config_loads_nothing(t *testing.T) { + app.CurrentEnvironment = app.Environment{} + ports := LoadYamlConfig("/garbage", "Local") + + assert.Equal(t, []string{"4100"}, ports) + assert.Equal(t, app.CurrentEnvironment, app.Environment{}) +} + +func TestConfig_LoadYamlConfig_invalid_config_loads_nothing(t *testing.T) { + app.CurrentEnvironment = app.Environment{} + ports := LoadYamlConfig("../common/common.go", "Local") + + assert.Equal(t, []string{"4100"}, ports) + assert.Equal(t, app.CurrentEnvironment, app.Environment{}) +} diff --git a/app/conf/goaws.yaml b/app/conf/goaws.yaml index a844fb10..a9eb77b1 100755 --- a/app/conf/goaws.yaml +++ b/app/conf/goaws.yaml @@ -15,6 +15,7 @@ Local: # Environment name that can be passed on the VisibilityTimeout: 30 # message visibility timeout ReceiveMessageWaitTimeSeconds: 0 # receive message max wait time MaximumMessageSize: 262144 # maximum message size (bytes) +# MessageRetentionPeriod: 445600 # time period to retain messages (seconds) NOTE: Functionality not implemented Queues: # List of queues to create at startup - Name: local-queue1 # Queue name - Name: local-queue2 # Queue name diff --git a/app/conf/mock-data/mock-config.yaml b/app/conf/mock-data/mock-config.yaml index e5f1c31b..45ecd342 100644 --- a/app/conf/mock-data/mock-config.yaml +++ b/app/conf/mock-data/mock-config.yaml @@ -1,37 +1,35 @@ -Local: # Environment name that can be passed on the command line - # (i.e.: ./goaws [Local | Dev] -- defaults to 'Local') - Host: localhost # hostname of the goaws system (for docker-compose this is the tag name of the container) - Port: 4100 # port to listen on. +Local: + Host: localhost + Port: 4100 Region: us-east-1 AccountId: "100010001000" - LogMessages: true # Log messages (true/false) - LogFile: ./goaws_messages.log # Log filename (for message logging - QueueAttributeDefaults: # default attributes for all queues - VisibilityTimeout: 10 # message visibility timeout - ReceiveMessageWaitTimeSeconds: 10 # receive message max wait time - MaximumMessageSize: 1024 # maximum message size (bytes) - MessageRetentionPeriod: 445600 # time period to retain messages (seconds) NOTE: Functionality not implemented - Queues: # List of queues to create at startup - - Name: local-queue1 # Queue name - - Name: local-queue2 # Queue name - ReceiveMessageWaitTimeSeconds: 20 # Queue receive message max wait time - MaximumMessageSize: 128 # Queue maximum message size (bytes) - VisibilityTimeout: 150 # Queue visibility timeout + LogMessages: true + LogFile: ./goaws_messages.log + QueueAttributeDefaults: + VisibilityTimeout: 10 + ReceiveMessageWaitTimeSeconds: 10 + MaximumMessageSize: 1024 + Queues: + - Name: local-queue1 + - Name: local-queue2 + ReceiveMessageWaitTimeSeconds: 20 + MaximumMessageSize: 128 + VisibilityTimeout: 150 MessageRetentionPeriod: 245600 - - Name: local-queue3 # Queue name + - Name: local-queue3 RedrivePolicy: '{"maxReceiveCount": 100, "deadLetterTargetArn":"arn:aws:sqs:us-east-1:100010001000:local-queue3-dlq"}' - - Name: local-queue3-dlq # Queue name - Topics: # List of topic to create at startup - - Name: local-topic1 # Topic name - with some Subscriptions - Subscriptions: # List of Subscriptions to create for this topic (queues will be created as required) - - QueueName: local-queue4 # Queue name - Raw: false # Raw message delivery (true/false) - - QueueName: local-queue5 # Queue name - Raw: true # Raw message delivery (true/false) - FilterPolicy: '{"foo":["bar"]}' # Subscription's FilterPolicy, json like a string - - Name: local-topic2 # Topic name - no Subscriptions + - Name: local-queue3-dlq + Topics: + - Name: local-topic1 + Subscriptions: + - QueueName: local-queue4 + Raw: false + - QueueName: local-queue5 + Raw: true + FilterPolicy: '{"foo":["bar"]}' + - Name: local-topic2 -NoQueuesOrTopics: # Another environment +NoQueuesOrTopics: Host: localhost Port: 4100 LogMessages: true @@ -50,15 +48,21 @@ NoQueueAttributeDefaults: ReceiveMessageWaitTimeSeconds: 20 BaseUnitTests: - # (i.e.: ./goaws [Local | Dev] -- defaults to 'Local') - Host: host # hostname of the goaws system (for docker-compose this is the tag name of the container) - Port: port # port to listen on. + Host: host + Port: port Region: region AccountId: accountID - LogMessages: true # Log messages (true/false) - LogFile: ./goaws_messages.log # Log filename (for message logging - Queues: # List of queues to create at startup - - Name: unit-queue1 # Queue name - - Name: unit-queue2 # Queue name + LogMessages: true + LogFile: ./goaws_messages.log + Queues: + - Name: unit-queue1 + - Name: unit-queue2 RedrivePolicy: '{"maxReceiveCount": 100, "deadLetterTargetArn":"arn:aws:sqs:us-east-1:100010001000:other-queue1"}' - - Name: other-queue1 # Queue name + - Name: other-queue1 + - Name: subscribed-queue2 + Topics: + - Name: unit-topic1 + Subscriptions: + - QueueName: subscribed-queue2 + Raw: true + - Name: unit-topic2 diff --git a/app/examples/java/SnsSample.java b/app/examples/java/SnsSample.java deleted file mode 100644 index 143cad04..00000000 --- a/app/examples/java/SnsSample.java +++ /dev/null @@ -1,68 +0,0 @@ -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.sns.model.*; -import com.amazonaws.services.sns.AmazonSNS; -import com.amazonaws.services.sns.AmazonSNSClient; - -import java.util.List; -import java.util.Map.Entry; - -/*** - * Make sure you have the aws-java-sdk-1.8.11.jar + dependancies in your classpath -***/ - -public class SnsSample { - - public static void main(String[] args) throws Exception { - - AmazonSNS sns = new AmazonSNSClient(new BasicAWSCredentials("x", "x")); - sns.setEndpoint("http://localhost:4100"); - - System.out.println("==========================================="); - System.out.println("Getting Started with Amazon SQS"); - System.out.println("===========================================\n"); - - try { - // Create a queue - System.out.println("Creating a new SNS topic called MyTopic.\n"); - CreateTopicRequest createTopicRequest = new CreateTopicRequest("MyTopic"); - String topicArn = sns.createTopic(createTopicRequest).getTopicArn(); - - // List queues - System.out.println("Listing all topics in your account.\n"); - for (Topic topic : sns.listTopics().withTopics().getTopics()) { - System.out.println(" TopicArn: " + topic.getTopicArn()); - } - System.out.println(); - - SubscribeResult sr = sns.subscribe(new SubscribeRequest(topicArn, "sqs", "http://localhost:4100/queue/local-queue1")); - System.out.println("SubscriptionArn: " + sr.getSubscriptionArn()); - System.out.println(); - - PublishRequest publishRequest = new PublishRequest(topicArn, "Sent to MyTopic!!!"); - PublishResult pr = sns.publish(publishRequest); - System.out.println("Message sent: " + pr.getMessageId()); - System.out.println(); - - DeleteTopicRequest str = new DeleteTopicRequest(); - str.setTopicArn(topicArn); - sns.deleteTopic(str); - System.out.println("Topic Delected: " + topicArn); - System.out.println(); - } catch (AmazonServiceException ase) { - System.out.println("Caught an AmazonServiceException, which means your request made it " + - "to Amazon SQS, but was rejected with an error response for some reason."); - System.out.println("Error Message: " + ase.getMessage()); - System.out.println("HTTP Status Code: " + ase.getStatusCode()); - System.out.println("AWS Error Code: " + ase.getErrorCode()); - System.out.println("Error Type: " + ase.getErrorType()); - System.out.println("Request ID: " + ase.getRequestId()); - } catch (AmazonClientException ace) { - System.out.println("Caught an AmazonClientException, which means the client encountered " + - "a serious internal problem while trying to communicate with SQS, such as not " + - "being able to access the network."); - System.out.println("Error Message: " + ace.getMessage()); - } - } -} diff --git a/app/examples/java/SqsSample.java b/app/examples/java/SqsSample.java deleted file mode 100644 index dfa8e6e8..00000000 --- a/app/examples/java/SqsSample.java +++ /dev/null @@ -1,87 +0,0 @@ -import java.util.List; -import java.util.Map.Entry; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.sqs.AmazonSQS; -import com.amazonaws.services.sqs.AmazonSQSClient; -import com.amazonaws.services.sqs.model.CreateQueueRequest; -import com.amazonaws.services.sqs.model.DeleteMessageRequest; -import com.amazonaws.services.sqs.model.DeleteQueueRequest; -import com.amazonaws.services.sqs.model.Message; -import com.amazonaws.services.sqs.model.ReceiveMessageRequest; -import com.amazonaws.services.sqs.model.SendMessageRequest; - -/*** - * Make sure you have the aws-java-sdk-1.8.11.jar + dependancies in your classpath -***/ - -public class SqsSample { - - public static void main(String[] args) throws Exception { - AmazonSQS sqs = new AmazonSQSClient(new BasicAWSCredentials("x", "x")); - sqs.setEndpoint("http://localhost:4100"); - - System.out.println("==========================================="); - System.out.println("Getting Started with Amazon SQS"); - System.out.println("===========================================\n"); - - try { - // Create a queue - System.out.println("Creating a new SQS queue called MyQueue.\n"); - CreateQueueRequest createQueueRequest = new CreateQueueRequest("MyQueue"); - String myQueueUrl = sqs.createQueue(createQueueRequest).getQueueUrl(); - - // List queues - System.out.println("Listing all queues in your account.\n"); - for (String queueUrl : sqs.listQueues().getQueueUrls()) { - System.out.println(" QueueUrl: " + queueUrl); - } - System.out.println(); - - // Send a message - System.out.println("Sending a message to MyQueue.\n"); - sqs.sendMessage(new SendMessageRequest(myQueueUrl, "This is my message text.")); - - // Receive messages - System.out.println("Receiving messages from MyQueue.\n"); - ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(myQueueUrl); - List messages = sqs.receiveMessage(receiveMessageRequest).getMessages(); - for (Message message : messages) { - System.out.println(" Message"); - System.out.println(" MessageId: " + message.getMessageId()); - System.out.println(" ReceiptHandle: " + message.getReceiptHandle()); - System.out.println(" MD5OfBody: " + message.getMD5OfBody()); - System.out.println(" Body: " + message.getBody()); - for (Entry entry : message.getAttributes().entrySet()) { - System.out.println(" Attribute"); - System.out.println(" Name: " + entry.getKey()); - System.out.println(" Value: " + entry.getValue()); - } - } - System.out.println(); - - // Delete a message - System.out.println("Deleting a message.\n"); - String messageReceiptHandle = messages.get(0).getReceiptHandle(); - sqs.deleteMessage(new DeleteMessageRequest(myQueueUrl, messageReceiptHandle)); - - // Delete a queue - System.out.println("Deleting the test queue.\n"); - sqs.deleteQueue(new DeleteQueueRequest(myQueueUrl)); - } catch (AmazonServiceException ase) { - System.out.println("Caught an AmazonServiceException, which means your request made it " + - "to Amazon SQS, but was rejected with an error response for some reason."); - System.out.println("Error Message: " + ase.getMessage()); - System.out.println("HTTP Status Code: " + ase.getStatusCode()); - System.out.println("AWS Error Code: " + ase.getErrorCode()); - System.out.println("Error Type: " + ase.getErrorType()); - System.out.println("Request ID: " + ase.getRequestId()); - } catch (AmazonClientException ace) { - System.out.println("Caught an AmazonClientException, which means the client encountered " + - "a serious internal problem while trying to communicate with SQS, such as not " + - "being able to access the network."); - System.out.println("Error Message: " + ace.getMessage()); - } - } -} diff --git a/app/examples/python/boto_sns_sample.py b/app/examples/python/boto_sns_sample.py deleted file mode 100644 index 5e0060c7..00000000 --- a/app/examples/python/boto_sns_sample.py +++ /dev/null @@ -1,95 +0,0 @@ -import boto -import boto.sqs -import boto.sns - -""" -Integration test for boto using GoAws SNS interface - - Create a virtual environment (pyvenv venv) - - Activate the venv (source venv/bin/activate) - - Install boto (pip install boto) - - run this script (python boto_sns_integration_tests.py) -""" - -""" -boto doesn't (yet) expose SetSubscriptionAttributes, so here's a -monkeypatch specifically for turning on the RawMessageDelivery attribute. -""" - -def SetRawSubscriptionAttribute(snsConnection, subscriptionArn): - """ - Works around boto's lack of a SetSubscriptionAttributes call. - """ - params = { - 'AttributeName': 'RawMessageDelivery', - 'AttributeValue': 'true', - 'SubscriptionArn': subscriptionArn - } - return snsConnection._make_request('SetSubscriptionAttributes', params) - -boto.sns.SNSConnection.set_raw_subscription_attribute = SetRawSubscriptionAttribute - - -# Connect GOAws in Python -endpoint='localhost' -region = boto.sqs.regioninfo.RegionInfo(name='local', endpoint=endpoint) -conn = boto.connect_sns(aws_access_key_id='x', aws_secret_access_key='x', is_secure=False, port='4100', region=region) - - -# Get all Topics in Python -print(conn.get_all_topics()) -print() -print() - - -# Get all Subscriptions in Python -print(conn.get_all_subscriptions()) -print() -print() - - -# Create a topic in Python -topicname = "trialBotoTopic" -topicarn = conn.create_topic(topicname) -print(topicname, "has been successfully created with a topic ARN of", topicarn) -print() -print() - - -# Print the topic Arn in python -print(topicarn['Result']['TopicArn']) -print() -print() - - -## Subscribe a Queue to a Topic in Python -subscription1 = conn.subscribe(topicarn['Result']['TopicArn'], "sqs", "http://localhost:4100/queue/local-queue2") -print(subscription1['Result']['SubscriptionArn']) -print() -print() - -## Set topic attribute Raw in Python -attr_results = conn.set_raw_subscription_attribute(subscription1['Result']['SubscriptionArn']) -print(attr_results) -print() -print() - - -## Publish to a topic in Python -message = "Hello Boto" -message_subject = "trialBotoTopic" -publication = conn.publish(topicarn['Result']['TopicArn'], message, subject=message_subject) -print(publication) - - -## Unsubscribe in Python -subscription1 = conn.unsubscribe(subscription1['Result']['SubscriptionArn']) -print(subscription1) -print() -print() - - -## Delete Topic in Python -deletion1 = conn.delete_topic(topicarn['Result']['TopicArn']) -print(deletion1) -print() -print() diff --git a/app/examples/python/boto_sqs_sample.py b/app/examples/python/boto_sqs_sample.py deleted file mode 100644 index 2ebd4317..00000000 --- a/app/examples/python/boto_sqs_sample.py +++ /dev/null @@ -1,71 +0,0 @@ -import boto -import boto.sqs - -""" -Integration test for boto using GoAws SNS interface - - Create a virtual environment (pyvenv venv) - - Activate the venv (source venv/bin/activate) - - Install boto (pip install boto) - - run this script (python boto_sns_integration_tests.py) -""" - -# Connect GOAws in Python -endpoint='localhost' -region = boto.sqs.regioninfo.RegionInfo(name='local', endpoint=endpoint) -conn = boto.connect_sqs(aws_access_key_id='x', aws_secret_access_key='x', is_secure=False, port='4100', region=region) - - -# Get all Queues in Python -print(conn.get_all_queues()) -print() -print() - -# Create a queue in Python -q = conn.create_queue('myqueue') -print(q) -print() -print() - -# Get Queue Attributes in Python -attribs = conn.get_queue_attributes(q) -print(attribs) -print() -print() - -# Get A Queue in Python -qi = conn.get_queue('myqueue') -print(qi) -print() -print() - -# Lookup a queue in Python (same as get a queue) -qi = conn.lookup('myqueue') -print(qi) -print() -print() - -# Send a message to a queue in Python -resp = conn.send_message(qi, "This is a test!!!") -print(resp) -print() -print() - -# Receive a message from a queue in Python -resp2 = conn.receive_message(qi) -for result in resp2: - print(result.get_body()) - - # Delete a message from a queue in Python - resp3 = conn.delete_message(qi, result) - print("\tDelete:", resp3) - -print() -print() - -# Delete a queue in Python -dq = conn.delete_queue(q) -print(dq) -print() -print() - - diff --git a/app/fixtures/fixtures.go b/app/fixtures/fixtures.go index 1db22736..470194f6 100644 --- a/app/fixtures/fixtures.go +++ b/app/fixtures/fixtures.go @@ -1,6 +1,7 @@ package fixtures var BASE_URL = "http://region.host:port/accountID" -var BASE_ARN = "arn:aws:sqs:region:accountID" +var BASE_SQS_ARN = "arn:aws:sqs:region:accountID" +var BASE_SNS_ARN = "arn:aws:sns:region:accountID" var REQUEST_ID = "request-id" diff --git a/app/fixtures/sqs.go b/app/fixtures/sqs.go index 4ecaff43..7130f591 100644 --- a/app/fixtures/sqs.go +++ b/app/fixtures/sqs.go @@ -41,7 +41,7 @@ var CreateQueueRequest = models.CreateQueueRequest{ Tags: map[string]string{"my": "tag"}, } -var QueueAttributes = models.Attributes{ +var QueueAttributes = models.QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 2, MessageRetentionPeriod: 3, diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 33de7ffb..b448e71f 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/google/uuid" + "github.com/Admiral-Piett/goaws/app/models" "bytes" @@ -42,15 +44,6 @@ func init() { app.SyncTopics.Topics = make(map[string]*app.Topic) TOPIC_DATA = make(map[string]*pendingConfirm) - app.SnsErrors = make(map[string]app.SnsErrorType) - err1 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentTopic", Message: "The specified topic does not exist for this wsdl version."} - app.SnsErrors["TopicNotFound"] = err1 - err2 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."} - app.SnsErrors["SubscriptionNotFound"] = err2 - err3 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleNotificationService.TopicAlreadyExists", Message: "The specified topic already exists."} - app.SnsErrors["TopicExists"] = err3 - err4 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."} - app.SnsErrors["ValidationError"] = err4 PrivateKEY, PemKEY, _ = createPemFile() } @@ -131,109 +124,6 @@ func CreateTopic(w http.ResponseWriter, req *http.Request) { SendResponseBack(w, req, respStruct, content) } -// aws --endpoint-url http://localhost:47194 sns subscribe --topic-arn arn:aws:sns:us-west-2:0123456789012:my-topic --protocol email --notification-endpoint my-email@example.com -func Subscribe(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - topicArn := req.FormValue("TopicArn") - protocol := req.FormValue("Protocol") - endpoint := req.FormValue("Endpoint") - filterPolicy := &app.FilterPolicy{} - raw := false - - for attrIndex := 1; req.FormValue("Attributes.entry."+strconv.Itoa(attrIndex)+".key") != ""; attrIndex++ { - value := req.FormValue("Attributes.entry." + strconv.Itoa(attrIndex) + ".value") - switch key := req.FormValue("Attributes.entry." + strconv.Itoa(attrIndex) + ".key"); key { - case "FilterPolicy": - json.Unmarshal([]byte(value), filterPolicy) - case "RawMessageDelivery": - raw = (value == "true") - } - } - - uriSegments := strings.Split(topicArn, ":") - topicName := uriSegments[len(uriSegments)-1] - log.WithFields(log.Fields{ - "content": content, - "topicArn": topicArn, - "topicName": topicName, - "protocol": protocol, - "endpoint": endpoint, - "filterPolicy": filterPolicy, - "raw": raw, - }).Info("Creating Subscription") - - subscription := &app.Subscription{EndPoint: endpoint, Protocol: protocol, TopicArn: topicArn, Raw: raw, FilterPolicy: filterPolicy} - subArn, _ := common.NewUUID() - subArn = topicArn + ":" + subArn - subscription.SubscriptionArn = subArn - - if app.SyncTopics.Topics[topicName] != nil { - app.SyncTopics.Lock() - isDuplicate := false - // Duplicate check - for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { - if subscription.EndPoint == endpoint && subscription.TopicArn == topicArn { - isDuplicate = true - subArn = subscription.SubscriptionArn - } - } - if !isDuplicate { - app.SyncTopics.Topics[topicName].Subscriptions = append(app.SyncTopics.Topics[topicName].Subscriptions, subscription) - log.WithFields(log.Fields{ - "topic": topicName, - "endpoint": endpoint, - "topicArn": topicArn, - }).Debug("Created subscription") - } - app.SyncTopics.Unlock() - - //Create the response - uuid, _ := common.NewUUID() - if app.Protocol(subscription.Protocol) == app.ProtocolHTTP || app.Protocol(subscription.Protocol) == app.ProtocolHTTPS { - id, _ := common.NewUUID() - token, _ := common.NewUUID() - - TOPIC_DATA[topicArn] = &pendingConfirm{ - subArn: subArn, - token: token, - } - - respStruct := app.SubscribeResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.SubscribeResult{SubscriptionArn: subArn}, app.ResponseMetadata{RequestId: uuid}} - SendResponseBack(w, req, respStruct, content) - time.Sleep(time.Second) - - snsMSG := &app.SNSMessage{ - Type: "SubscriptionConfirmation", - MessageId: id, - Token: token, - TopicArn: topicArn, - Message: "You have chosen to subscribe to the topic " + topicArn + ".\nTo confirm the subscription, visit the SubscribeURL included in this message.", - SigningCertURL: "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/SimpleNotificationService/" + uuid + ".pem", - SignatureVersion: "1", - SubscribeURL: "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/?Action=ConfirmSubscription&TopicArn=" + topicArn + "&Token=" + token, - Timestamp: time.Now().UTC().Format(time.RFC3339), - } - signature, err := signMessage(PrivateKEY, snsMSG) - if err != nil { - log.Error("Error signing message") - } else { - snsMSG.Signature = signature - } - err = callEndpoint(subscription.EndPoint, uuid, *snsMSG, subscription.Raw) - if err != nil { - log.Error("Error posting to url ", err) - } - } else { - respStruct := app.SubscribeResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.SubscribeResult{SubscriptionArn: subArn}, app.ResponseMetadata{RequestId: uuid}} - - SendResponseBack(w, req, respStruct, content) - } - - } else { - createErrorResponse(w, req, "TopicNotFound") - } -} - func signMessage(privkey *rsa.PrivateKey, snsMsg *app.SNSMessage) (string, error) { fs, err := formatSignature(snsMsg) if err != nil { @@ -286,8 +176,7 @@ func ConfirmSubscription(w http.ResponseWriter, req *http.Request) { confirmToken := req.Form.Get("Token") pendingConfirm := TOPIC_DATA[topicArn] if pendingConfirm.token == confirmToken { - uuid, _ := common.NewUUID() - respStruct := app.ConfirmSubscriptionResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.SubscribeResult{SubscriptionArn: pendingConfirm.subArn}, app.ResponseMetadata{RequestId: uuid}} + respStruct := models.ConfirmSubscriptionResponse{"http://queue.amazonaws.com/doc/2012-11-05/", models.SubscribeResult{SubscriptionArn: pendingConfirm.subArn}, app.ResponseMetadata{RequestId: uuid.NewString()}} SendResponseBack(w, req, respStruct, "application/xml") } else { @@ -770,7 +659,7 @@ func extractMessageFromJSON(msg string, protocol string) (string, error) { } func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { - er := app.SnsErrors[err] + er := models.SnsErrors[err] respStruct := models.ErrorResponse{ Result: models.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, RequestId: "00000000-0000-0000-0000-000000000000", diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index 4308d89f..fd1b99fe 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -6,9 +6,9 @@ import ( "net/url" "strings" "testing" - "time" - "github.com/gorilla/mux" + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/test" "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/common" @@ -257,99 +257,6 @@ func TestPublishHandler_POST_FilterPolicyPassesTheMessage(t *testing.T) { } } -func TestSubscribehandler_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. - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") - form.Add("Protocol", "sqs") - form.Add("Endpoint", "http://localhost:4100/queue/noqueue1") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(Subscribe) - - // 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) - } -} - -func TestSubscribehandler_HTTP_POST_Success(t *testing.T) { - done := make(chan bool) - - r := mux.NewRouter() - r.HandleFunc("/sns_post", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - close(done) - - })) - - ts := httptest.NewServer(r) - defer ts.Close() - - // 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:UnitTestTopic1") - form.Add("Protocol", "http") - form.Add("Endpoint", ts.URL+"/sns_post") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(Subscribe) - - // Create ResponseRecorder for http side - - // 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) - } - - select { - case <-done: - case <-time.After(2 * time.Second): - t.Fatal("http sns handler must be called") - } -} - func TestPublish_No_Queue_Error_handler_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. @@ -385,7 +292,12 @@ func TestPublish_No_Queue_Error_handler_POST_Success(t *testing.T) { } } +// 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") + defer func() { + test.ResetApp() + }() // set accountID to test value so it can be populated in response app.CurrentEnvironment.AccountID = "100010001000" @@ -398,7 +310,7 @@ func TestListSubscriptionByTopicResponse_No_Owner(t *testing.T) { } form := url.Values{} - form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") + form.Add("TopicArn", "arn:aws:sns:local:000000000000:local-topic1") req.PostForm = form // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. @@ -424,6 +336,10 @@ 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" @@ -436,7 +352,7 @@ func TestListSubscriptionsResponse_No_Owner(t *testing.T) { } form := url.Values{} - form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") + form.Add("TopicArn", "arn:aws:sns:local:000000000000:local-topic1") req.PostForm = form // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. @@ -462,6 +378,11 @@ func TestListSubscriptionsResponse_No_Owner(t *testing.T) { } func TestDeleteTopichandler_POST_Success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + }() + // 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) @@ -470,7 +391,7 @@ func TestDeleteTopichandler_POST_Success(t *testing.T) { } form := url.Values{} - form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") + form.Add("TopicArn", "arn:aws:sns:local:000000000000:local-topic1") form.Add("Message", "TestMessage1") req.PostForm = form diff --git a/app/gosns/subscribe.go b/app/gosns/subscribe.go new file mode 100644 index 00000000..ad8cea10 --- /dev/null +++ b/app/gosns/subscribe.go @@ -0,0 +1,103 @@ +package gosns + +import ( + "fmt" + "net/http" + "strings" + "time" + + "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" +) + +// aws --endpoint-url http://localhost:47194 sns subscribe --topic-arn arn:aws:sns:us-west-2:0123456789012:my-topic --protocol email --notification-endpoint my-email@example.com +func SubscribeV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewSubscribeRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - SubscribeV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + uriSegments := strings.Split(requestBody.TopicArn, ":") + topicName := uriSegments[len(uriSegments)-1] + extraLogFields := log.Fields{ + "topicArn": requestBody.TopicArn, + "topicName": topicName, + "protocol": requestBody.Protocol, + "endpoint": requestBody.Endpoint, + "filterPolicy": requestBody.Attributes.FilterPolicy, + "raw": requestBody.Attributes.RawMessageDelivery, + } + log.WithFields(extraLogFields).Info("Creating Subscription") + + subscription := &app.Subscription{EndPoint: requestBody.Endpoint, Protocol: requestBody.Protocol, TopicArn: requestBody.TopicArn, Raw: requestBody.Attributes.RawMessageDelivery, FilterPolicy: &requestBody.Attributes.FilterPolicy} + + subArn := uuid.NewString() + subscription.SubscriptionArn = fmt.Sprintf("%s:%s", requestBody.TopicArn, uuid.NewString()) + + //Create the response + requestId := uuid.NewString() + respStruct := models.SubscribeResponse{Xmlns: models.BASE_XMLNS, Result: models.SubscribeResult{SubscriptionArn: subArn}, Metadata: app.ResponseMetadata{RequestId: requestId}} + if app.SyncTopics.Topics[topicName] != nil { + app.SyncTopics.Lock() + isDuplicate := false + // Duplicate check + for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { + if subscription.EndPoint == requestBody.Endpoint && subscription.TopicArn == requestBody.TopicArn { + isDuplicate = true + subArn = subscription.SubscriptionArn + } + } + if !isDuplicate { + app.SyncTopics.Topics[topicName].Subscriptions = append(app.SyncTopics.Topics[topicName].Subscriptions, subscription) + log.WithFields(extraLogFields).Debug("Created subscription") + } + app.SyncTopics.Unlock() + + if app.Protocol(subscription.Protocol) == app.ProtocolHTTP || app.Protocol(subscription.Protocol) == app.ProtocolHTTPS { + id := uuid.NewString() + token := uuid.NewString() + + TOPIC_DATA[requestBody.TopicArn] = &pendingConfirm{ + subArn: subArn, + token: token, + } + + //QUESTION - do we need this? + time.Sleep(time.Second) + + snsMSG := &app.SNSMessage{ + Type: "SubscriptionConfirmation", + MessageId: id, + Token: token, + TopicArn: requestBody.TopicArn, + Message: fmt.Sprintf("You have chosen to subscribe to the topic %s.\nTo confirm the subscription, visit the SubscribeURL included in this message.", requestBody.TopicArn), + SigningCertURL: fmt.Sprintf("http://%s:%s/SimpleNotificationService/%s.pem", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, requestId), + SignatureVersion: "1", + SubscribeURL: fmt.Sprintf("http://%s:%s/?Action=ConfirmSubscription&TopicArn=%s&Token=%s", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, requestBody.TopicArn, token), + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + signature, err := signMessage(PrivateKEY, snsMSG) + if err != nil { + log.Error("Error signing message") + } else { + snsMSG.Signature = signature + } + err = callEndpoint(subscription.EndPoint, requestId, *snsMSG, subscription.Raw) + if err != nil { + log.Error("Error posting to url ", err) + } + } + + } else { + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + return http.StatusOK, respStruct +} diff --git a/app/gosns/subscribe_test.go b/app/gosns/subscribe_test.go new file mode 100644 index 00000000..dc670cf2 --- /dev/null +++ b/app/gosns/subscribe_test.go @@ -0,0 +1,170 @@ +package gosns + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/fixtures" + "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 TestSubscribeV1_success_no_attributes(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.SubscribeRequest) + *v = models.SubscribeRequest{ + TopicArn: fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic2"), + Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_URL, "unit-queue2"), + Protocol: "sqs", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := SubscribeV1(r) + + response, _ := res.(models.SubscribeResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + // We are populating the request id with a new random value on this request + assert.NotEqual(t, "", response.Metadata) + assert.NotEqual(t, "", response.Result.SubscriptionArn) + + subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions + assert.Len(t, subscriptions, 1) + + assert.Equal(t, fmt.Sprintf("%s:%s", fixtures.BASE_URL, "unit-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.False(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic2")) +} + +func TestSubscribeV1_success_with_attributes(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.SubscribeRequest) + *v = models.SubscribeRequest{ + TopicArn: fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic2"), + Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_URL, "unit-queue2"), + Protocol: "sqs", + Attributes: models.SubscriptionAttributes{ + FilterPolicy: app.FilterPolicy{"filter": []string{"policy"}}, + RawMessageDelivery: true, + }, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := SubscribeV1(r) + + response, _ := res.(models.SubscribeResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + // We are populating the request id with a new random value on this request + assert.NotEqual(t, "", response.Metadata) + assert.NotEqual(t, "", response.Result.SubscriptionArn) + + subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions + assert.Len(t, subscriptions, 1) + + expectedFilterPolicy := app.FilterPolicy{"filter": []string{"policy"}} + assert.Equal(t, fmt.Sprintf("%s:%s", fixtures.BASE_URL, "unit-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, &expectedFilterPolicy, subscriptions[0].FilterPolicy) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.True(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic2")) +} + +func TestSubscribeV1_success_duplicate_subscription(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.SubscribeRequest) + *v = models.SubscribeRequest{ + TopicArn: fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic1"), + Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue2"), + Protocol: "sqs", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SubscribeV1(r) + + assert.Equal(t, http.StatusOK, code) + + subscriptions := app.SyncTopics.Topics["unit-topic1"].Subscriptions + assert.Len(t, subscriptions, 1) + + assert.Equal(t, fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.True(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic1")) + assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic1")) +} + +func TestSubscribeV1_error_invalid_request(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, _ := SubscribeV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestSubscribeV1_error_missing_topic(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.SubscribeRequest) + *v = models.SubscribeRequest{ + TopicArn: fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "garbage"), + Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue2"), + Protocol: "sqs", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SubscribeV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosqs/change_message_visibility.go b/app/gosqs/change_message_visibility.go index be5efa87..6f9b566a 100644 --- a/app/gosqs/change_message_visibility.go +++ b/app/gosqs/change_message_visibility.go @@ -18,7 +18,7 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - ChangeMessageVisibilityV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } vars := mux.Vars(req) @@ -36,11 +36,11 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo visibilityTimeout := requestBody.VisibilityTimeout if visibilityTimeout > 43200 { - return createErrorResponseV1("ValidationError") + return utils.CreateErrorResponseV1("ValidationError", true) } if _, ok := app.SyncQueues.Queues[queueName]; !ok { - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } app.SyncQueues.Lock() @@ -71,7 +71,7 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo } app.SyncQueues.Unlock() if !messageFound { - return createErrorResponseV1("MessageNotInFlight") + return utils.CreateErrorResponseV1("MessageNotInFlight", true) } respStruct := models.ChangeMessageVisibilityResult{ diff --git a/app/gosqs/change_message_visibility_test.go b/app/gosqs/change_message_visibility_test.go index 4ffe7ae7..a34e699c 100644 --- a/app/gosqs/change_message_visibility_test.go +++ b/app/gosqs/change_message_visibility_test.go @@ -4,10 +4,11 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/stretchr/testify/assert" ) @@ -15,7 +16,7 @@ func TestChangeMessageVisibility_POST_SUCCESS(t *testing.T) { // create a queue app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() }() q := &app.Queue{ @@ -30,7 +31,7 @@ func TestChangeMessageVisibility_POST_SUCCESS(t *testing.T) { // The default value for the VisibilityTimeout is the zero value of time.Time assert.Zero(t, q.Messages[0].VisibilityTimeout) - _, r := utils.GenerateRequestInfo("POST", "/", models.ChangeMessageVisibilityRequest{ + _, r := test.GenerateRequestInfo("POST", "/", models.ChangeMessageVisibilityRequest{ QueueUrl: "http://localhost:4100/queue/testing", ReceiptHandle: "123", VisibilityTimeout: 0, diff --git a/app/gosqs/create_queue.go b/app/gosqs/create_queue.go index f4c89d29..51831131 100644 --- a/app/gosqs/create_queue.go +++ b/app/gosqs/create_queue.go @@ -16,7 +16,7 @@ func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - CreateQueueV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } queueName := requestBody.QueueName @@ -39,7 +39,7 @@ func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { Duplicates: make(map[string]time.Time), } if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { - return createErrorResponseV1(err.Error()) + return utils.CreateErrorResponseV1(err.Error(), true) } app.SyncQueues.Lock() app.SyncQueues.Queues[queueName] = queue diff --git a/app/gosqs/create_queue_test.go b/app/gosqs/create_queue_test.go index 65a3b805..ec1e2b7a 100644 --- a/app/gosqs/create_queue_test.go +++ b/app/gosqs/create_queue_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -18,7 +20,7 @@ import ( func TestCreateQueueV1_success(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -28,7 +30,7 @@ func TestCreateQueueV1_success(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := CreateQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -41,7 +43,7 @@ func TestCreateQueueV1_success(t *testing.T) { func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -87,7 +89,7 @@ func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { Duplicates: make(map[string]time.Time), } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := CreateQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -100,7 +102,7 @@ func TestCreateQueueV1_success_with_redrive_policy(t *testing.T) { func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -115,7 +117,7 @@ func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { } app.SyncQueues.Queues[fixtures.QueueName] = q - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := CreateQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -128,14 +130,14 @@ func TestCreateQueueV1_success_with_existing_queue(t *testing.T) { func TestCreateQueueV1_success_with_no_request_attributes_falls_back_to_default(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) c, _ := dupe.(models.CreateQueueRequest) - c.Attributes = models.Attributes{} + c.Attributes = models.QueueAttributes{} v := resultingStruct.(*models.CreateQueueRequest) *v = c @@ -164,7 +166,7 @@ func TestCreateQueueV1_success_with_no_request_attributes_falls_back_to_default( Duplicates: make(map[string]time.Time), } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := CreateQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -178,21 +180,21 @@ func TestCreateQueueV1_success_no_configured_region_for_queue_url(t *testing.T) app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT app.CurrentEnvironment.Region = "" defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { dupe, _ := copystructure.Copy(fixtures.CreateQueueRequest) c, _ := dupe.(models.CreateQueueRequest) - c.Attributes = models.Attributes{} + c.Attributes = models.QueueAttributes{} v := resultingStruct.(*models.CreateQueueRequest) *v = c return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := CreateQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -212,7 +214,7 @@ func TestCreateQueueV1_success_no_configured_region_for_queue_url(t *testing.T) func TestCreateQueueV1_request_transformer_error(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -220,7 +222,7 @@ func TestCreateQueueV1_request_transformer_error(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := CreateQueueV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -229,7 +231,7 @@ func TestCreateQueueV1_request_transformer_error(t *testing.T) { func TestCreateQueueV1_invalid_dead_letter_queue_error(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -246,7 +248,7 @@ func TestCreateQueueV1_invalid_dead_letter_queue_error(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := CreateQueueV1(r) assert.Equal(t, http.StatusBadRequest, code) diff --git a/app/gosqs/delete_message.go b/app/gosqs/delete_message.go index b43eabbe..18f974aa 100644 --- a/app/gosqs/delete_message.go +++ b/app/gosqs/delete_message.go @@ -17,7 +17,7 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - DeleteMessageV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } // Retrieve FormValues required @@ -62,5 +62,5 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { log.Warning("Queue not found") } - return createErrorResponseV1("MessageDoesNotExist") + return utils.CreateErrorResponseV1("MessageDoesNotExist", true) } diff --git a/app/gosqs/delete_message_test.go b/app/gosqs/delete_message_test.go index c6d98089..90d3b46b 100644 --- a/app/gosqs/delete_message_test.go +++ b/app/gosqs/delete_message_test.go @@ -4,17 +4,18 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/stretchr/testify/assert" ) func TestDeleteMessage(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() }() q := &app.Queue{ @@ -27,7 +28,7 @@ func TestDeleteMessage(t *testing.T) { app.SyncQueues.Queues["testing"] = q - _, r := utils.GenerateRequestInfo("POST", "/", models.DeleteMessageRequest{ + _, r := test.GenerateRequestInfo("POST", "/", models.DeleteMessageRequest{ QueueUrl: "http://localhost:4100/queue/testing", ReceiptHandle: "123", }, true) diff --git a/app/gosqs/delete_queue.go b/app/gosqs/delete_queue.go index 4d1d9194..3f5b7500 100644 --- a/app/gosqs/delete_queue.go +++ b/app/gosqs/delete_queue.go @@ -18,7 +18,7 @@ func DeleteQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - DeleteQueueV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } uriSegments := strings.Split(requestBody.QueueUrl, "/") diff --git a/app/gosqs/delete_queue_test.go b/app/gosqs/delete_queue_test.go index 8c2348cb..390364f3 100644 --- a/app/gosqs/delete_queue_test.go +++ b/app/gosqs/delete_queue_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/stretchr/testify/assert" @@ -19,7 +21,7 @@ import ( func TestDeleteQueueV1_success(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -36,7 +38,7 @@ func TestDeleteQueueV1_success(t *testing.T) { Metadata: models.BASE_RESPONSE_METADATA, } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := DeleteQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -49,7 +51,7 @@ func TestDeleteQueueV1_success(t *testing.T) { func TestDeleteQueueV1_success_unknown_queue(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -66,17 +68,17 @@ func TestDeleteQueueV1_success_unknown_queue(t *testing.T) { Metadata: models.BASE_RESPONSE_METADATA, } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := DeleteQueueV1(r) assert.Equal(t, http.StatusOK, code) assert.Equal(t, expectedResponse, response) } -func TestDeleteQueueV1_success_invalid_request(t *testing.T) { +func TestDeleteQueueV1_error_invalid_request(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -84,7 +86,7 @@ func TestDeleteQueueV1_success_invalid_request(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := DeleteQueueV1(r) assert.Equal(t, http.StatusBadRequest, code) diff --git a/app/gosqs/get_queue_attributes.go b/app/gosqs/get_queue_attributes.go index cd7102ca..e6472e64 100644 --- a/app/gosqs/get_queue_attributes.go +++ b/app/gosqs/get_queue_attributes.go @@ -21,11 +21,11 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - GetQueueAttributesV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } if requestBody.QueueUrl == "" { log.Error("Missing QueueUrl - GetQueueAttributesV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } requestedAttributes := func() map[string]bool { @@ -57,7 +57,7 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo uriSegments := strings.Split(requestBody.QueueUrl, "/") queueName := uriSegments[len(uriSegments)-1] - log.Infof("Get Queue Attributes: %s", queueName) + log.Infof("Get Queue QueueAttributes: %s", queueName) queueAttributes := make([]models.Attribute, 0, 0) app.SyncQueues.RLock() @@ -65,7 +65,7 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo queue, ok := app.SyncQueues.Queues[queueName] if !ok { log.Errorf("Get Queue URL: %s queue does not exist!!!", queueName) - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } if _, ok := includedAttributes["DelaySeconds"]; ok { diff --git a/app/gosqs/get_queue_attributes_test.go b/app/gosqs/get_queue_attributes_test.go index 79ecfe26..ba217299 100644 --- a/app/gosqs/get_queue_attributes_test.go +++ b/app/gosqs/get_queue_attributes_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/mitchellh/copystructure" "github.com/Admiral-Piett/goaws/app/conf" @@ -19,7 +21,7 @@ import ( func TestGetQueueAttributesV1_success_all(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -29,7 +31,7 @@ func TestGetQueueAttributesV1_success_all(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := GetQueueAttributesV1(r) assert.Equal(t, http.StatusOK, code) @@ -39,7 +41,7 @@ func TestGetQueueAttributesV1_success_all(t *testing.T) { func TestGetQueueAttributesV1_success_no_request_attrs_returns_all(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -51,7 +53,7 @@ func TestGetQueueAttributesV1_success_no_request_attrs_returns_all(t *testing.T) return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := GetQueueAttributesV1(r) assert.Equal(t, http.StatusOK, code) @@ -61,7 +63,7 @@ func TestGetQueueAttributesV1_success_no_request_attrs_returns_all(t *testing.T) func TestGetQueueAttributesV1_success_all_with_redrive_queue(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -74,16 +76,16 @@ func TestGetQueueAttributesV1_success_all_with_redrive_queue(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := GetQueueAttributesV1(r) dupe, _ := copystructure.Copy(fixtures.GetQueueAttributesResponse) expectedResponse, _ := dupe.(models.GetQueueAttributesResponse) - expectedResponse.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", fixtures.BASE_ARN, "unit-queue2") + expectedResponse.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "unit-queue2") expectedResponse.Result.Attrs = append(expectedResponse.Result.Attrs, models.Attribute{ Name: "RedrivePolicy", - Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, fixtures.BASE_ARN, "other-queue1"), + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, fixtures.BASE_SQS_ARN, "other-queue1"), }, ) @@ -94,7 +96,7 @@ func TestGetQueueAttributesV1_success_all_with_redrive_queue(t *testing.T) { func TestGetQueueAttributesV1_success_specific_fields(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -107,7 +109,7 @@ func TestGetQueueAttributesV1_success_specific_fields(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := GetQueueAttributesV1(r) expectedResponse := models.GetQueueAttributesResponse{ @@ -134,7 +136,7 @@ func TestGetQueueAttributesV1_request_transformer_error(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := GetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -154,7 +156,7 @@ func TestGetQueueAttributesV1_missing_queue_url_in_request_returns_error(t *test return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := GetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -171,7 +173,7 @@ func TestGetQueueAttributesV1_missing_queue_returns_error(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := GetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) diff --git a/app/gosqs/get_queue_url.go b/app/gosqs/get_queue_url.go index 109bb51c..2fe5338e 100644 --- a/app/gosqs/get_queue_url.go +++ b/app/gosqs/get_queue_url.go @@ -15,13 +15,13 @@ func GetQueueUrlV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - GetQueueUrlV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } queueName := requestBody.QueueName if _, ok := app.SyncQueues.Queues[queueName]; !ok { log.Error("Get Queue URL:", queueName, ", queue does not exist!!!") - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } queue := app.SyncQueues.Queues[queueName] diff --git a/app/gosqs/get_queue_url_test.go b/app/gosqs/get_queue_url_test.go index 2dc70352..8b99c49b 100644 --- a/app/gosqs/get_queue_url_test.go +++ b/app/gosqs/get_queue_url_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -17,7 +19,7 @@ func TestGetQueueUrlV1_success(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -30,7 +32,7 @@ func TestGetQueueUrlV1_success(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo( + _, r := test.GenerateRequestInfo( "POST", "/", nil, @@ -49,7 +51,7 @@ func TestGetQueueUrlV1_error_no_queue(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -62,7 +64,7 @@ func TestGetQueueUrlV1_error_no_queue(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo( + _, r := test.GenerateRequestInfo( "POST", "/", nil, @@ -84,7 +86,7 @@ func TestGetQueueUrlV1_error_request_transformer(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -92,7 +94,7 @@ func TestGetQueueUrlV1_error_request_transformer(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo( + _, r := test.GenerateRequestInfo( "POST", "/", nil, diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 7c463c8e..146e81c7 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -8,8 +8,6 @@ import ( "strings" "time" - "github.com/Admiral-Piett/goaws/app/interfaces" - "github.com/Admiral-Piett/goaws/app/models" log "github.com/sirupsen/logrus" @@ -21,30 +19,6 @@ import ( func init() { app.SyncQueues.Queues = make(map[string]*app.Queue) - - app.SqsErrors = make(map[string]app.SqsErrorType) - err1 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleQueueService.NonExistentQueue", Message: "The specified queue does not exist for this wsdl version."} - app.SqsErrors["QueueNotFound"] = err1 - err2 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleQueueService.QueueExists", Message: "The specified queue already exists."} - app.SqsErrors["QueueExists"] = err2 - err3 := app.SqsErrorType{HttpError: http.StatusNotFound, Type: "Not Found", Code: "AWS.SimpleQueueService.QueueExists", Message: "The specified queue does not contain the message specified."} - app.SqsErrors["MessageDoesNotExist"] = err3 - err4 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "GeneralError", Code: "AWS.SimpleQueueService.GeneralError", Message: "General Error."} - app.SqsErrors["GeneralError"] = err4 - err5 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "TooManyEntriesInBatchRequest", Code: "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", Message: "Maximum number of entries per request are 10."} - app.SqsErrors["TooManyEntriesInBatchRequest"] = err5 - err6 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "BatchEntryIdsNotDistinct", Code: "AWS.SimpleQueueService.BatchEntryIdsNotDistinct", Message: "Two or more batch entries in the request have the same Id."} - app.SqsErrors["BatchEntryIdsNotDistinct"] = err6 - err7 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "EmptyBatchRequest", Code: "AWS.SimpleQueueService.EmptyBatchRequest", Message: "The batch request doesn't contain any entries."} - app.SqsErrors["EmptyBatchRequest"] = err7 - err8 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "ValidationError", Code: "AWS.SimpleQueueService.ValidationError", Message: "The visibility timeout is incorrect"} - app.SqsErrors["InvalidVisibilityTimeout"] = err8 - err9 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "MessageNotInFlight", Code: "AWS.SimpleQueueService.MessageNotInFlight", Message: "The message referred to isn't in flight."} - app.SqsErrors["MessageNotInFlight"] = err9 - err10 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "MessageTooBig", Code: "InvalidParameterValue", Message: "The message size exceeds the limit."} - app.SqsErrors["MessageTooBig"] = err10 - app.SqsErrors[ErrInvalidParameterValue.Type] = *ErrInvalidParameterValue - app.SqsErrors[ErrInvalidAttributeValue.Type] = *ErrInvalidAttributeValue } func PeriodicTasks(d time.Duration, quit <-chan struct{}) { @@ -345,7 +319,7 @@ func getQueueFromPath(formVal string, theUrl string) string { } func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { - er := app.SqsErrors[err] + er := models.SqsErrors[err] respStruct := models.ErrorResponse{ Result: models.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, RequestId: "00000000-0000-0000-0000-000000000000", @@ -358,12 +332,3 @@ func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { log.Printf("error: %v\n", err) } } - -func createErrorResponseV1(err string) (int, interfaces.AbstractResponseBody) { - er := app.SqsErrors[err] - respStruct := models.ErrorResponse{ - Result: models.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message}, - RequestId: "00000000-0000-0000-0000-000000000000", // TODO - fix - } - return er.HttpError, respStruct -} diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 5957b1ba..0fc5df2e 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -15,11 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMain(m *testing.M) { - utils.InitializeDecoders() - m.Run() -} - func TestSendMessageBatch_POST_QueueNotFound(t *testing.T) { req, err := http.NewRequest("POST", "/", nil) if err != nil { @@ -1032,7 +1027,7 @@ func TestCreateErrorResponseV1(t *testing.T) { }, RequestId: "00000000-0000-0000-0000-000000000000", } - status, response := createErrorResponseV1("QueueNotFound") + status, response := utils.CreateErrorResponseV1("QueueNotFound", true) assert.Equal(t, http.StatusBadRequest, status) assert.Equal(t, expectedResponse, response) diff --git a/app/gosqs/list_queues.go b/app/gosqs/list_queues.go index 5336b30d..a70f9071 100644 --- a/app/gosqs/list_queues.go +++ b/app/gosqs/list_queues.go @@ -21,7 +21,7 @@ func ListQueuesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, true) if !ok { log.Error("Invalid Request - ListQueuesV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } log.Info("Listing Queues") diff --git a/app/gosqs/list_queues_test.go b/app/gosqs/list_queues_test.go index 30156265..05ffde7f 100644 --- a/app/gosqs/list_queues_test.go +++ b/app/gosqs/list_queues_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -16,7 +18,7 @@ import ( func TestListQueuesV1_success(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -26,7 +28,7 @@ func TestListQueuesV1_success(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := ListQueuesV1(r) r1 := response.(models.ListQueuesResponse) @@ -38,7 +40,7 @@ func TestListQueuesV1_success(t *testing.T) { func TestListQueuesV1_success_no_queues(t *testing.T) { defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -48,7 +50,7 @@ func TestListQueuesV1_success_no_queues(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := ListQueuesV1(r) r1 := response.(models.ListQueuesResponse) @@ -59,7 +61,7 @@ func TestListQueuesV1_success_no_queues(t *testing.T) { func TestListQueuesV1_success_with_queue_name_prefix(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -69,7 +71,7 @@ func TestListQueuesV1_success_with_queue_name_prefix(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := ListQueuesV1(r) r1 := response.(models.ListQueuesResponse) @@ -80,7 +82,7 @@ func TestListQueuesV1_success_with_queue_name_prefix(t *testing.T) { func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -90,7 +92,7 @@ func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testi return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := ListQueuesV1(r) r1 := response.(models.ListQueuesResponse) @@ -100,14 +102,14 @@ func TestListQueuesV1_success_with_queue_name_prefix_no_matching_queues(t *testi func TestListQueuesV1_request_transformer_error(t *testing.T) { defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := ListQueuesV1(r) assert.Equal(t, http.StatusBadRequest, code) diff --git a/app/gosqs/purge_queue.go b/app/gosqs/purge_queue.go index b2562f4a..483d55df 100644 --- a/app/gosqs/purge_queue.go +++ b/app/gosqs/purge_queue.go @@ -18,7 +18,7 @@ func PurgeQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - PurgeQueueV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } uriSegments := strings.Split(requestBody.QueueUrl, "/") @@ -28,7 +28,7 @@ func PurgeQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) { defer app.SyncQueues.Unlock() if _, ok := app.SyncQueues.Queues[queueName]; !ok { log.Errorf("Purge Queue: %s, queue does not exist!!!", queueName) - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } log.Infof("Purging Queue: %s", queueName) diff --git a/app/gosqs/purge_queue_test.go b/app/gosqs/purge_queue_test.go index d5a14c4d..c1c54af0 100644 --- a/app/gosqs/purge_queue_test.go +++ b/app/gosqs/purge_queue_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app" @@ -19,7 +21,7 @@ import ( func TestPurgeQueueV1_success(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -45,7 +47,7 @@ func TestPurgeQueueV1_success(t *testing.T) { Metadata: models.BASE_RESPONSE_METADATA, } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := PurgeQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -58,7 +60,7 @@ func TestPurgeQueueV1_success(t *testing.T) { func TestPurgeQueueV1_success_no_messages_on_queue(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -75,7 +77,7 @@ func TestPurgeQueueV1_success_no_messages_on_queue(t *testing.T) { Metadata: models.BASE_RESPONSE_METADATA, } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := PurgeQueueV1(r) assert.Equal(t, http.StatusOK, code) @@ -89,7 +91,7 @@ func TestPurgeQueueV1_success_no_messages_on_queue(t *testing.T) { func TestPurgeQueueV1_request_transformer_error(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -97,7 +99,7 @@ func TestPurgeQueueV1_request_transformer_error(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := PurgeQueueV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -106,7 +108,7 @@ func TestPurgeQueueV1_request_transformer_error(t *testing.T) { func TestPurgeQueueV1_requested_queue_does_not_exist(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -118,7 +120,7 @@ func TestPurgeQueueV1_requested_queue_does_not_exist(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := PurgeQueueV1(r) assert.Equal(t, http.StatusBadRequest, code) diff --git a/app/gosqs/queue_attributes.go b/app/gosqs/queue_attributes.go index 2d608abc..fbc8c77d 100644 --- a/app/gosqs/queue_attributes.go +++ b/app/gosqs/queue_attributes.go @@ -1,107 +1,21 @@ package gosqs import ( - "encoding/json" "fmt" - "net/http" - "net/url" - "strconv" "strings" log "github.com/sirupsen/logrus" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/Admiral-Piett/goaws/app" ) -var ( - ErrInvalidParameterValue = &app.SqsErrorType{ - HttpError: http.StatusBadRequest, - Type: "InvalidParameterValue", - Code: "AWS.SimpleQueueService.InvalidParameterValue", - Message: "An invalid or out-of-range value was supplied for the input parameter.", - } - ErrInvalidAttributeValue = &app.SqsErrorType{ - HttpError: http.StatusBadRequest, - Type: "InvalidAttributeValue", - Code: "AWS.SimpleQueueService.InvalidAttributeValue", - Message: "Invalid Value for the parameter RedrivePolicy.", - } -) - -// validateAndSetQueueAttributesFromForm applies the requested queue attributes to the given -// queue. -// TODO Currently it only supports VisibilityTimeout, MaximumMessageSize, DelaySeconds, RedrivePolicy and ReceiveMessageWaitTimeSeconds attributes. -func validateAndSetQueueAttributesFromForm(q *app.Queue, u url.Values) error { - attr := utils.ExtractQueueAttributes(u) - - return validateAndSetQueueAttributes(q, attr) -} - -func validateAndSetQueueAttributes(q *app.Queue, attr map[string]string) error { - visibilityTimeout, _ := strconv.Atoi(attr["VisibilityTimeout"]) - if visibilityTimeout != 0 { - q.VisibilityTimeout = visibilityTimeout - } - receiveWaitTime, _ := strconv.Atoi(attr["ReceiveMessageWaitTimeSeconds"]) - if receiveWaitTime != 0 { - q.ReceiveMessageWaitTimeSeconds = receiveWaitTime - } - maximumMessageSize, _ := strconv.Atoi(attr["MaximumMessageSize"]) - if maximumMessageSize != 0 { - q.MaximumMessageSize = maximumMessageSize - } - strRedrivePolicy := attr["RedrivePolicy"] - if strRedrivePolicy != "" { - // support both int and string maxReceiveCount (Amazon clients use string) - redrivePolicy1 := struct { - MaxReceiveCount int `json:"maxReceiveCount"` - DeadLetterTargetArn string `json:"deadLetterTargetArn"` - }{} - redrivePolicy2 := struct { - MaxReceiveCount string `json:"maxReceiveCount"` - DeadLetterTargetArn string `json:"deadLetterTargetArn"` - }{} - err1 := json.Unmarshal([]byte(strRedrivePolicy), &redrivePolicy1) - err2 := json.Unmarshal([]byte(strRedrivePolicy), &redrivePolicy2) - maxReceiveCount := redrivePolicy1.MaxReceiveCount - deadLetterQueueArn := redrivePolicy1.DeadLetterTargetArn - if err1 != nil && err2 != nil { - return ErrInvalidAttributeValue - } else if err1 != nil { - maxReceiveCount, _ = strconv.Atoi(redrivePolicy2.MaxReceiveCount) - deadLetterQueueArn = redrivePolicy2.DeadLetterTargetArn - } - - if (deadLetterQueueArn != "" && maxReceiveCount == 0) || - (deadLetterQueueArn == "" && maxReceiveCount != 0) { - return ErrInvalidParameterValue - } - dlt := strings.Split(deadLetterQueueArn, ":") - deadLetterQueueName := dlt[len(dlt)-1] - deadLetterQueue, ok := app.SyncQueues.Queues[deadLetterQueueName] - if !ok { - return ErrInvalidParameterValue - } - q.DeadLetterQueue = deadLetterQueue - q.MaxReceiveCount = maxReceiveCount - } - delaySecs, _ := strconv.Atoi(attr["DelaySeconds"]) - if delaySecs != 0 { - q.DelaySeconds = delaySecs - } - - return nil -} - // TODO - Support: // - attr.MessageRetentionPeriod // - attr.Policy // - attr.RedriveAllowPolicy -func setQueueAttributesV1(q *app.Queue, attr models.Attributes) error { +func setQueueAttributesV1(q *app.Queue, attr models.QueueAttributes) error { // FIXME - are there better places to put these bottom-limit validations? if attr.DelaySeconds >= 0 { q.DelaySeconds = attr.DelaySeconds.Int() @@ -126,7 +40,7 @@ func setQueueAttributesV1(q *app.Queue, attr models.Attributes) error { deadLetterQueue, ok := app.SyncQueues.Queues[queueName] if !ok { log.Error("Invalid RedrivePolicy Attribute") - return fmt.Errorf(ErrInvalidAttributeValue.Type) + return fmt.Errorf("InvalidAttributeValue") } q.DeadLetterQueue = deadLetterQueue q.MaxReceiveCount = attr.RedrivePolicy.MaxReceiveCount.Int() diff --git a/app/gosqs/queue_attributes_test.go b/app/gosqs/queue_attributes_test.go index da471c78..dae2ac7d 100644 --- a/app/gosqs/queue_attributes_test.go +++ b/app/gosqs/queue_attributes_test.go @@ -2,12 +2,9 @@ package gosqs import ( "fmt" - "net/url" - "reflect" "testing" - "github.com/Admiral-Piett/goaws/app/utils" - + "github.com/Admiral-Piett/goaws/app/test" "github.com/stretchr/testify/assert" "github.com/Admiral-Piett/goaws/app/models" @@ -15,63 +12,10 @@ import ( "github.com/Admiral-Piett/goaws/app" ) -func TestApplyQueueAttributes(t *testing.T) { - t.Run("success", func(t *testing.T) { - deadLetterQueue := &app.Queue{Name: "failed-messages"} - app.SyncQueues.Lock() - app.SyncQueues.Queues["failed-messages"] = deadLetterQueue - app.SyncQueues.Unlock() - q := &app.Queue{VisibilityTimeout: 30} - u := url.Values{} - u.Add("Attribute.1.Name", "DelaySeconds") - u.Add("Attribute.1.Value", "25") - u.Add("Attribute.2.Name", "VisibilityTimeout") - u.Add("Attribute.2.Value", "60") - u.Add("Attribute.3.Name", "Policy") - u.Add("Attribute.4.Name", "RedrivePolicy") - u.Add("Attribute.4.Value", `{"maxReceiveCount":"4", "deadLetterTargetArn":"arn:aws:sqs::000000000000:failed-messages"}`) - u.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") - u.Add("Attribute.5.Value", "20") - if err := validateAndSetQueueAttributesFromForm(q, u); err != nil { - t.Fatalf("expected nil, got %s", err) - } - expected := &app.Queue{ - VisibilityTimeout: 60, - ReceiveMessageWaitTimeSeconds: 20, - DelaySeconds: 25, - MaxReceiveCount: 4, - DeadLetterQueue: deadLetterQueue, - } - if ok := reflect.DeepEqual(q, expected); !ok { - t.Fatalf("expected %+v, got %+v", expected, q) - } - }) - t.Run("missing_deadletter_arn", func(t *testing.T) { - q := &app.Queue{VisibilityTimeout: 30} - u := url.Values{} - u.Add("Attribute.1.Name", "RedrivePolicy") - u.Add("Attribute.1.Value", `{"maxReceiveCount":"4"}`) - err := validateAndSetQueueAttributesFromForm(q, u) - if err != ErrInvalidParameterValue { - t.Fatalf("expected %s, got %s", ErrInvalidParameterValue, err) - } - }) - t.Run("invalid_redrive_policy", func(t *testing.T) { - q := &app.Queue{VisibilityTimeout: 30} - u := url.Values{} - u.Add("Attribute.1.Name", "RedrivePolicy") - u.Add("Attribute.1.Value", `{invalidinput}`) - err := validateAndSetQueueAttributesFromForm(q, u) - if err != ErrInvalidAttributeValue { - t.Fatalf("expected %s, got %s", ErrInvalidAttributeValue, err) - } - }) -} - func TestSetQueueAttributesV1_success_no_redrive_policy(t *testing.T) { var emptyQueue *app.Queue q := &app.Queue{} - attrs := models.Attributes{ + attrs := models.QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 2, MessageRetentionPeriod: 3, @@ -93,7 +37,7 @@ func TestSetQueueAttributesV1_success_no_redrive_policy(t *testing.T) { func TestSetQueueAttributesV1_success_no_request_attributes(t *testing.T) { var emptyQueue *app.Queue q := &app.Queue{} - attrs := models.Attributes{} + attrs := models.QueueAttributes{} err := setQueueAttributesV1(q, attrs) assert.Nil(t, err) @@ -115,7 +59,7 @@ func TestSetQueueAttributesV1_success_can_set_0_values_where_applicable(t *testi ReceiveMessageWaitTimeSeconds: 4, VisibilityTimeout: 5, } - attrs := models.Attributes{} + attrs := models.QueueAttributes{} err := setQueueAttributesV1(q, attrs) assert.Nil(t, err) @@ -130,7 +74,7 @@ func TestSetQueueAttributesV1_success_can_set_0_values_where_applicable(t *testi func TestSetQueueAttributesV1_success_with_redrive_policy(t *testing.T) { defer func() { - utils.ResetApp() + test.ResetApp() }() existingQueueName := "existing-queue" @@ -138,7 +82,7 @@ func TestSetQueueAttributesV1_success_with_redrive_policy(t *testing.T) { app.SyncQueues.Queues[existingQueueName] = existingQueue q := &app.Queue{} - attrs := models.Attributes{ + attrs := models.QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 2, MessageRetentionPeriod: 3, @@ -165,7 +109,7 @@ func TestSetQueueAttributesV1_error_redrive_policy_targets_missing_queue(t *test existingQueueName := "existing-queue" q := &app.Queue{} - attrs := models.Attributes{ + attrs := models.QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 2, MessageRetentionPeriod: 3, diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go index 349aa558..581f7c91 100644 --- a/app/gosqs/receive_message.go +++ b/app/gosqs/receive_message.go @@ -23,7 +23,7 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - ReceiveMessageV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } maxNumberOfMessages := requestBody.MaxNumberOfMessages @@ -41,7 +41,7 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) } if _, ok := app.SyncQueues.Queues[queueName]; !ok { - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } var messages []*models.ResultMessage @@ -60,7 +60,7 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) _, queueFound := app.SyncQueues.Queues[queueName] if !queueFound { app.SyncQueues.RUnlock() - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } messageFound := len(app.SyncQueues.Queues[queueName].Messages)-numberOfHiddenMessagesInQueue(*app.SyncQueues.Queues[queueName]) != 0 app.SyncQueues.RUnlock() diff --git a/app/gosqs/receive_message_test.go b/app/gosqs/receive_message_test.go index 2f301d7c..3a24746d 100644 --- a/app/gosqs/receive_message_test.go +++ b/app/gosqs/receive_message_test.go @@ -7,10 +7,11 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/stretchr/testify/assert" ) @@ -19,7 +20,7 @@ import ( func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() }() q := &app.Queue{ @@ -30,7 +31,7 @@ func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { app.SyncQueues.Queues["waiting-queue"] = q // receive message ensure delay - _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + _, r := test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ QueueUrl: "http://localhost:4100/queue/waiting-queue", }, true) @@ -47,7 +48,7 @@ func TestReceiveMessageWaitTimeEnforcedV1(t *testing.T) { q.Messages = append(q.Messages, app.Message{MessageBody: []byte("1")}) // receive message - _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + _, r = test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ QueueUrl: "http://localhost:4100/queue/waiting-queue", }, true) start = time.Now() @@ -66,7 +67,7 @@ func TestReceiveMessage_CanceledByClientV1(t *testing.T) { // create a queue app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() }() q := &app.Queue{ @@ -82,7 +83,7 @@ func TestReceiveMessage_CanceledByClientV1(t *testing.T) { go func() { defer wg.Done() // receive message (that will be canceled) - _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + _, r := test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ QueueUrl: "http://localhost:4100/queue/cancel-queue", }, true) r = r.WithContext(ctx) @@ -99,7 +100,7 @@ func TestReceiveMessage_CanceledByClientV1(t *testing.T) { time.Sleep(5 * time.Millisecond) // send a message - _, r := utils.GenerateRequestInfo("POST", "/", models.SendMessageRequest{ + _, r := test.GenerateRequestInfo("POST", "/", models.SendMessageRequest{ QueueUrl: "http://localhost:4100/queue/cancel-queue", MessageBody: "12345", }, true) @@ -109,7 +110,7 @@ func TestReceiveMessage_CanceledByClientV1(t *testing.T) { } // receive message - _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + _, r = test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ QueueUrl: "http://localhost:4100/queue/cancel-queue", }, true) start := time.Now() @@ -138,7 +139,7 @@ func TestReceiveMessageDelaySecondsV1(t *testing.T) { // create a queue app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() }() q := &app.Queue{ @@ -148,7 +149,7 @@ func TestReceiveMessageDelaySecondsV1(t *testing.T) { app.SyncQueues.Queues["delay-seconds-queue"] = q // send a message - _, r := utils.GenerateRequestInfo("POST", "/", models.SendMessageRequest{ + _, r := test.GenerateRequestInfo("POST", "/", models.SendMessageRequest{ QueueUrl: "http://localhost:4100/queue/delay-seconds-queue", MessageBody: "1", }, true) @@ -158,12 +159,12 @@ func TestReceiveMessageDelaySecondsV1(t *testing.T) { } // receive message before delay is up - _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{QueueUrl: "http://localhost:4100/queue/delay-seconds-queue"}, true) + _, r = test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{QueueUrl: "http://localhost:4100/queue/delay-seconds-queue"}, true) status, _ = ReceiveMessageV1(r) assert.Equal(t, http.StatusOK, status) // receive message with wait should return after delay - _, r = utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ + _, r = test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{ QueueUrl: "http://localhost:4100/queue/delay-seconds-queue", WaitTimeSeconds: 10, }, true) @@ -183,7 +184,7 @@ func TestReceiveMessageAttributesV1(t *testing.T) { // create a queue app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() }() q := &app.Queue{Name: "waiting-queue"} @@ -202,7 +203,7 @@ func TestReceiveMessageAttributesV1(t *testing.T) { }) // receive message - _, r := utils.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{QueueUrl: "http://localhost:4100/queue/waiting-queue"}, true) + _, r := test.GenerateRequestInfo("POST", "/", models.ReceiveMessageRequest{QueueUrl: "http://localhost:4100/queue/waiting-queue"}, true) status, resp := ReceiveMessageV1(r) result := resp.GetResult().(models.ReceiveMessageResult) diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index ef3f2b2e..ceb237a8 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -22,7 +22,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - SendMessageV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } messageBody := requestBody.MessageBody messageGroupID := requestBody.MessageGroupId @@ -43,13 +43,13 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { if _, ok := app.SyncQueues.Queues[queueName]; !ok { // Queue does not exist - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } if app.SyncQueues.Queues[queueName].MaximumMessageSize > 0 && len(messageBody) > app.SyncQueues.Queues[queueName].MaximumMessageSize { // Message size is too big - return createErrorResponseV1("MessageTooBig") + return utils.CreateErrorResponseV1("MessageTooBig", true) } delaySecs := app.SyncQueues.Queues[queueName].DelaySeconds diff --git a/app/gosqs/send_message_test.go b/app/gosqs/send_message_test.go index 2c43ec6e..0db6ccce 100644 --- a/app/gosqs/send_message_test.go +++ b/app/gosqs/send_message_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -16,7 +18,7 @@ import ( func TestSendMessageV1_Success(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -36,7 +38,7 @@ func TestSendMessageV1_Success(t *testing.T) { } app.SyncQueues.Queues["new-queue-1"] = q - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) status, response := SendMessageV1(r) // Check the queue @@ -56,7 +58,7 @@ func TestSendMessageV1_Success(t *testing.T) { func TestSendMessageV1_Success_FIFOQueue(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -77,7 +79,7 @@ func TestSendMessageV1_Success_FIFOQueue(t *testing.T) { } app.SyncQueues.Queues["new-queue-1"] = q - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) status, response := SendMessageV1(r) // Check the queue @@ -97,7 +99,7 @@ func TestSendMessageV1_Success_FIFOQueue(t *testing.T) { func TestSendMessageV1_Success_Deduplication(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -121,7 +123,7 @@ func TestSendMessageV1_Success_Deduplication(t *testing.T) { } app.SyncQueues.Queues["new-queue-1"] = q - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) status, _ := SendMessageV1(r) // Check the queue @@ -140,7 +142,7 @@ func TestSendMessageV1_Success_Deduplication(t *testing.T) { func TestSendMessageV1_request_transformer_error(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -148,7 +150,7 @@ func TestSendMessageV1_request_transformer_error(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := SendMessageV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -157,7 +159,7 @@ func TestSendMessageV1_request_transformer_error(t *testing.T) { func TestSendMessageV1_MaximumMessageSize_MessageTooBig(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -177,7 +179,7 @@ func TestSendMessageV1_MaximumMessageSize_MessageTooBig(t *testing.T) { } app.SyncQueues.Queues["new-queue-1"] = q - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) status, response := SendMessageV1(r) // Check the response @@ -190,7 +192,7 @@ func TestSendMessageV1_MaximumMessageSize_MessageTooBig(t *testing.T) { func TestSendMessageV1_POST_QueueNonExistant(t *testing.T) { app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -206,7 +208,7 @@ func TestSendMessageV1_POST_QueueNonExistant(t *testing.T) { // No test queue is added to app.SyncQueues - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) status, response := SendMessageV1(r) // Check the status code is what we expect. diff --git a/app/gosqs/set_queue_attributes.go b/app/gosqs/set_queue_attributes.go index f47876a6..4d68789f 100644 --- a/app/gosqs/set_queue_attributes.go +++ b/app/gosqs/set_queue_attributes.go @@ -17,11 +17,11 @@ func SetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - GetQueueAttributesV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } if requestBody.QueueUrl == "" { log.Error("Missing QueueUrl - GetQueueAttributesV1") - return createErrorResponseV1(ErrInvalidParameterValue.Type) + return utils.CreateErrorResponseV1("InvalidParameterValue", true) } // NOTE: I tore out the handling for devining the url from a param. I can't find documentation that @@ -29,16 +29,16 @@ func SetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo uriSegments := strings.Split(requestBody.QueueUrl, "/") queueName := uriSegments[len(uriSegments)-1] - log.Infof("Set Queue Attributes: %s", queueName) + log.Infof("Set Queue QueueAttributes: %s", queueName) app.SyncQueues.Lock() defer app.SyncQueues.Unlock() queue, ok := app.SyncQueues.Queues[queueName] if !ok { log.Warningf("Get Queue URL: %s, queue does not exist!!!", queueName) - return createErrorResponseV1("QueueNotFound") + return utils.CreateErrorResponseV1("QueueNotFound", true) } if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { - return createErrorResponseV1(err.Error()) + return utils.CreateErrorResponseV1(err.Error(), true) } respStruct := models.SetQueueAttributesResponse{ diff --git a/app/gosqs/set_queue_attributes_test.go b/app/gosqs/set_queue_attributes_test.go index 06a202ec..51ffdf09 100644 --- a/app/gosqs/set_queue_attributes_test.go +++ b/app/gosqs/set_queue_attributes_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app/fixtures" @@ -17,7 +19,7 @@ import ( func TestSetQueueAttributesV1_success_multiple_attributes(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -27,7 +29,7 @@ func TestSetQueueAttributesV1_success_multiple_attributes(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := SetQueueAttributesV1(r) expectedResponse := models.SetQueueAttributesResponse{ @@ -48,7 +50,7 @@ func TestSetQueueAttributesV1_success_multiple_attributes(t *testing.T) { func TestSetQueueAttributesV1_success_single_attribute(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -56,14 +58,14 @@ func TestSetQueueAttributesV1_success_single_attribute(t *testing.T) { v := resultingStruct.(*models.SetQueueAttributesRequest) *v = models.SetQueueAttributesRequest{ QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), - Attributes: models.Attributes{ + Attributes: models.QueueAttributes{ VisibilityTimeout: 5, }, } return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, response := SetQueueAttributesV1(r) expectedResponse := models.SetQueueAttributesResponse{ @@ -84,7 +86,7 @@ func TestSetQueueAttributesV1_success_single_attribute(t *testing.T) { func TestSetQueueAttributesV1_invalid_request_body(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -92,7 +94,7 @@ func TestSetQueueAttributesV1_invalid_request_body(t *testing.T) { return false } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := SetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -101,19 +103,19 @@ func TestSetQueueAttributesV1_invalid_request_body(t *testing.T) { func TestSetQueueAttributesV1_missing_queue_url(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { v := resultingStruct.(*models.SetQueueAttributesRequest) *v = models.SetQueueAttributesRequest{ - Attributes: models.Attributes{}, + Attributes: models.QueueAttributes{}, } return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := SetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -122,7 +124,7 @@ func TestSetQueueAttributesV1_missing_queue_url(t *testing.T) { func TestSetQueueAttributesV1_missing_expected_queue(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -134,7 +136,7 @@ func TestSetQueueAttributesV1_missing_expected_queue(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := SetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) @@ -143,7 +145,7 @@ func TestSetQueueAttributesV1_missing_expected_queue(t *testing.T) { func TestSetQueueAttributesV1_invalid_redrive_queue(t *testing.T) { conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") defer func() { - utils.ResetApp() + test.ResetApp() utils.REQUEST_TRANSFORMER = utils.TransformRequest }() @@ -151,7 +153,7 @@ func TestSetQueueAttributesV1_invalid_redrive_queue(t *testing.T) { v := resultingStruct.(*models.SetQueueAttributesRequest) *v = models.SetQueueAttributesRequest{ QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), - Attributes: models.Attributes{ + Attributes: models.QueueAttributes{ RedrivePolicy: models.RedrivePolicy{ MaxReceiveCount: 100, DeadLetterTargetArn: fmt.Sprintf("arn:aws:sqs:us-east-1:100010001000:%s", "garbage"), @@ -161,7 +163,7 @@ func TestSetQueueAttributesV1_invalid_redrive_queue(t *testing.T) { return true } - _, r := utils.GenerateRequestInfo("POST", "/", nil, true) + _, r := test.GenerateRequestInfo("POST", "/", nil, true) code, _ := SetQueueAttributesV1(r) assert.Equal(t, http.StatusBadRequest, code) diff --git a/app/interfaces/interfaces.go b/app/interfaces/interfaces.go index fbe63f53..89fdecac 100644 --- a/app/interfaces/interfaces.go +++ b/app/interfaces/interfaces.go @@ -2,6 +2,8 @@ package interfaces import ( "net/url" + + "github.com/Admiral-Piett/goaws/app/models" ) type AbstractRequestBody interface { @@ -12,3 +14,8 @@ type AbstractResponseBody interface { GetResult() interface{} GetRequestId() string } + +type AbstractErrorResponse interface { + Response() models.ErrorResult + StatusCode() int +} diff --git a/app/models/conversions_test.go b/app/models/conversions_test.go index 144c099a..5c208359 100644 --- a/app/models/conversions_test.go +++ b/app/models/conversions_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "github.com/Admiral-Piett/goaws/app/utils" + "github.com/Admiral-Piett/goaws/app/test" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func TestStringToInt_unmarshalJSON_int(t *testing.T) { Field1: 1, Field2: 2, } - _, r := utils.GenerateRequestInfo("POST", "/", body, true) + _, r := test.GenerateRequestInfo("POST", "/", body, true) result := &StringToIntStruct{} decoder := json.NewDecoder(r.Body) @@ -40,7 +40,7 @@ func TestStringToInt_unmarshalJSON_string(t *testing.T) { Field1: "1", Field2: "2", } - _, r := utils.GenerateRequestInfo("POST", "/", body, true) + _, r := test.GenerateRequestInfo("POST", "/", body, true) result := &StringToIntStruct{} decoder := json.NewDecoder(r.Body) @@ -59,7 +59,7 @@ func TestStringToInt_unmarshalJSON_invalid_type_returns_error(t *testing.T) { Field1: true, Field2: false, } - _, r := utils.GenerateRequestInfo("POST", "/", body, true) + _, r := test.GenerateRequestInfo("POST", "/", body, true) result := &StringToIntStruct{} decoder := json.NewDecoder(r.Body) diff --git a/app/models/errors.go b/app/models/errors.go new file mode 100644 index 00000000..ab421a0e --- /dev/null +++ b/app/models/errors.go @@ -0,0 +1,61 @@ +package models + +import "net/http" + +func init() { + SqsErrors = map[string]SqsErrorType{ + "QueueNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleQueueService.NonExistentQueue", Message: "The specified queue does not exist for this wsdl version."}, + "QueueExists": {HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleQueueService.QueueExists", Message: "The specified queue already exists."}, + "MessageDoesNotExist": {HttpError: http.StatusNotFound, Type: "Not Found", Code: "AWS.SimpleQueueService.QueueExists", Message: "The specified queue does not contain the message specified."}, + "GeneralError": {HttpError: http.StatusBadRequest, Type: "GeneralError", Code: "AWS.SimpleQueueService.GeneralError", Message: "General Error."}, + "TooManyEntriesInBatchRequest": {HttpError: http.StatusBadRequest, Type: "TooManyEntriesInBatchRequest", Code: "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", Message: "Maximum number of entries per request are 10."}, + "BatchEntryIdsNotDistinct": {HttpError: http.StatusBadRequest, Type: "BatchEntryIdsNotDistinct", Code: "AWS.SimpleQueueService.BatchEntryIdsNotDistinct", Message: "Two or more batch entries in the request have the same Id."}, + "EmptyBatchRequest": {HttpError: http.StatusBadRequest, Type: "EmptyBatchRequest", Code: "AWS.SimpleQueueService.EmptyBatchRequest", Message: "The batch request doesn't contain any entries."}, + "InvalidVisibilityTimeout": {HttpError: http.StatusBadRequest, Type: "ValidationError", Code: "AWS.SimpleQueueService.ValidationError", Message: "The visibility timeout is incorrect"}, + "MessageNotInFlight": {HttpError: http.StatusBadRequest, Type: "MessageNotInFlight", Code: "AWS.SimpleQueueService.MessageNotInFlight", Message: "The message referred to isn't in flight."}, + "MessageTooBig": {HttpError: http.StatusBadRequest, Type: "MessageTooBig", Code: "InvalidParameterValue", Message: "The message size exceeds the limit."}, + "InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleQueueService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."}, + "InvalidAttributeValue": {HttpError: http.StatusBadRequest, Type: "InvalidAttributeValue", Code: "AWS.SimpleQueueService.InvalidAttributeValue", Message: "Invalid Value for the parameter RedrivePolicy."}, + } + SnsErrors = map[string]SnsErrorType{ + "InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleNotificationService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."}, + "TopicNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentTopic", Message: "The specified topic does not exist for this wsdl version."}, + "SubscriptionNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."}, + "TopicExists": {HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleNotificationService.TopicAlreadyExists", Message: "The specified topic already exists."}, + "ValidationError": {HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."}, + } +} + +type SqsErrorType struct { + HttpError int + Type string + Code string + Message string +} + +func (s SqsErrorType) StatusCode() int { + return s.HttpError +} + +func (s SqsErrorType) Response() ErrorResult { + return ErrorResult{Type: s.Type, Code: s.Code, Message: s.Message} +} + +var SqsErrors map[string]SqsErrorType + +type SnsErrorType struct { + HttpError int + Type string + Code string + Message string +} + +func (s SnsErrorType) StatusCode() int { + return s.HttpError +} + +func (s SnsErrorType) Response() ErrorResult { + return ErrorResult{Type: s.Type, Code: s.Code, Message: s.Message} +} + +var SnsErrors map[string]SnsErrorType diff --git a/app/models/models.go b/app/models/models.go index 3116c499..5a42bb4f 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -1,13 +1,7 @@ package models import ( - "encoding/json" - "fmt" - "net/url" - "strconv" - "github.com/Admiral-Piett/goaws/app" - log "github.com/sirupsen/logrus" ) var BASE_XMLNS = "http://queue.amazonaws.com/doc/2012-11-05/" @@ -29,451 +23,3 @@ var AVAILABLE_QUEUE_ATTRIBUTES = map[string]bool{ "LastModifiedTimestamp": true, "QueueArn": true, } - -type CreateQueueRequest struct { - QueueName string `json:"QueueName" schema:"QueueName"` - Attributes Attributes `json:"Attributes" schema:"Attribute"` - Tags map[string]string `json:"Tags" schema:"Tags"` - Version string `json:"Version" schema:"Version"` -} - -// TODO - is there an easier way to do this? Similar to the StringToInt type? -func (r *CreateQueueRequest) SetAttributesFromForm(values url.Values) { - for i := 1; true; i++ { - nameKey := fmt.Sprintf("Attribute.%d.Name", i) - attrName := values.Get(nameKey) - if attrName == "" { - break - } - - valueKey := fmt.Sprintf("Attribute.%d.Value", i) - attrValue := values.Get(valueKey) - if attrValue == "" { - continue - } - switch attrName { - case "DelaySeconds": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.DelaySeconds = StringToInt(tmp) - case "MaximumMessageSize": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.MaximumMessageSize = StringToInt(tmp) - case "MessageRetentionPeriod": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.MessageRetentionPeriod = StringToInt(tmp) - case "Policy": - var tmp map[string]interface{} - err := json.Unmarshal([]byte(attrValue), &tmp) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.Policy = tmp - case "ReceiveMessageWaitTimeSeconds": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.ReceiveMessageWaitTimeSeconds = StringToInt(tmp) - case "VisibilityTimeout": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.VisibilityTimeout = StringToInt(tmp) - case "RedrivePolicy": - tmp := RedrivePolicy{} - var decodedPolicy struct { - MaxReceiveCount interface{} `json:"maxReceiveCount"` - DeadLetterTargetArn string `json:"deadLetterTargetArn"` - } - err := json.Unmarshal([]byte(attrValue), &decodedPolicy) - if err != nil || decodedPolicy.DeadLetterTargetArn == "" { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - // Support both int and string types (historic processing), set a default of 10 if not provided. - // Go will default into float64 for interface{} types when parsing numbers - receiveCount, ok := decodedPolicy.MaxReceiveCount.(float64) - if !ok { - receiveCount = 10 - t, ok := decodedPolicy.MaxReceiveCount.(string) - if ok { - r, err := strconv.ParseFloat(t, 64) - if err == nil { - receiveCount = r - } else { - log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) - } - } else { - log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) - } - } - tmp.MaxReceiveCount = StringToInt(receiveCount) - tmp.DeadLetterTargetArn = decodedPolicy.DeadLetterTargetArn - r.Attributes.RedrivePolicy = tmp - case "RedriveAllowPolicy": - var tmp map[string]interface{} - err := json.Unmarshal([]byte(attrValue), &tmp) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.RedriveAllowPolicy = tmp - } - } - return -} - -func NewListQueuesRequest() *ListQueueRequest { - return &ListQueueRequest{} -} - -type ListQueueRequest struct { - MaxResults int `json:"MaxResults" schema:"MaxResults"` - NextToken string `json:"NextToken" schema:"NextToken"` - QueueNamePrefix string `json:"QueueNamePrefix" schema:"QueueNamePrefix"` -} - -func (r *ListQueueRequest) SetAttributesFromForm(values url.Values) { - maxResults, err := strconv.Atoi(values.Get("MaxResults")) - if err == nil { - r.MaxResults = maxResults - } - r.NextToken = values.Get("NextToken") - r.QueueNamePrefix = values.Get("QueueNamePrefix") -} - -func NewGetQueueAttributesRequest() *GetQueueAttributesRequest { - return &GetQueueAttributesRequest{} -} - -type GetQueueAttributesRequest struct { - QueueUrl string `json:"QueueUrl"` - AttributeNames []string `json:"AttributeNames"` -} - -func (r *GetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { - r.QueueUrl = values.Get("QueueUrl") - // TODO - test me - for i := 1; true; i++ { - attrKey := fmt.Sprintf("AttributeName.%d", i) - attrValue := values.Get(attrKey) - if attrValue == "" { - break - } - r.AttributeNames = append(r.AttributeNames, attrValue) - } -} - -/*** Send Message Request */ -func NewSendMessageRequest() *SendMessageRequest { - return &SendMessageRequest{ - MessageAttributes: make(map[string]MessageAttributeValue), - MessageSystemAttributes: make(map[string]MessageAttributeValue), - } -} - -type SendMessageRequest struct { - DelaySeconds int `json:"DelaySeconds" schema:"DelaySeconds"` - // MessageAttributes is custom attributes that users can add on the message as they like. - // Please see: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageAttributes - MessageAttributes map[string]MessageAttributeValue `json:"MessageAttributes" schema:"MessageAttributes"` - MessageBody string `json:"MessageBody" schema:"MessageBody"` - MessageDeduplicationId string `json:"MessageDeduplicationId" schema:"MessageDeduplicationId"` - MessageGroupId string `json:"MessageGroupId" schema:"MessageGroupId"` - // MessageSystemAttributes is custom attributes for AWS services. - // Please see: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageSystemAttributes - // On AWS, the only supported attribute is "AWSTraceHeader" that is for AWS X-Ray. - // Goaws does not contains X-Ray emulation, so currently MessageSystemAttributes is unsupported. - // TODO: Replace with a struct with known attributes "AWSTraceHeader". - MessageSystemAttributes map[string]MessageAttributeValue `json:"MessageSystemAttributes" schema:"MessageSystemAttributes"` - QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` -} -type MessageAttributeValue struct { - BinaryListValues []string `json:"BinaryListValues"` // currently unsupported by AWS - BinaryValue string `json:"BinaryValue"` - DataType string `json:"DataType"` - StringListValues []string `json:"StringListValues"` // currently unsupported by AWS - StringValue string `json:"StringValue"` -} - -func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { - for i := 1; true; i++ { - nameKey := fmt.Sprintf("MessageAttribute.%d.Name", i) - name := values.Get(nameKey) - if name == "" { - break - } - - dataTypeKey := fmt.Sprintf("MessageAttribute.%d.Value.DataType", i) - dataType := values.Get(dataTypeKey) - if dataType == "" { - log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - continue - } - - stringValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.StringValue", i)) - binaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i)) - - r.MessageAttributes[name] = MessageAttributeValue{ - DataType: dataType, - StringValue: stringValue, - BinaryValue: binaryValue, - } - - if _, ok := r.MessageAttributes[name]; !ok { - log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - } - } -} - -// Get Queue Url Request -func NewGetQueueUrlRequest() *GetQueueUrlRequest { - return &GetQueueUrlRequest{} -} - -type GetQueueUrlRequest struct { - QueueName string `json:"QueueName"` - QueueOwnerAWSAccountId string `json:"QueueOwnerAWSAccountId"` // NOTE: not implemented -} - -func (r *GetQueueUrlRequest) SetAttributesFromForm(values url.Values) {} - -func NewSetQueueAttributesRequest() *SetQueueAttributesRequest { - return &SetQueueAttributesRequest{} -} - -type SetQueueAttributesRequest struct { - QueueUrl string `json:"QueueUrl"` - Attributes Attributes `json:"Attributes"` -} - -func (r *SetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { - r.QueueUrl = values.Get("QueueUrl") - // TODO - could we share with CreateQueueRequest? - for i := 1; true; i++ { - nameKey := fmt.Sprintf("Attribute.%d.Name", i) - attrName := values.Get(nameKey) - if attrName == "" { - break - } - - valueKey := fmt.Sprintf("Attribute.%d.Value", i) - attrValue := values.Get(valueKey) - if attrValue == "" { - continue - } - switch attrName { - case "DelaySeconds": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.DelaySeconds = StringToInt(tmp) - case "MaximumMessageSize": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.MaximumMessageSize = StringToInt(tmp) - case "MessageRetentionPeriod": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.MessageRetentionPeriod = StringToInt(tmp) - case "Policy": - var tmp map[string]interface{} - err := json.Unmarshal([]byte(attrValue), &tmp) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.Policy = tmp - case "ReceiveMessageWaitTimeSeconds": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.ReceiveMessageWaitTimeSeconds = StringToInt(tmp) - case "VisibilityTimeout": - tmp, err := strconv.Atoi(attrValue) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.VisibilityTimeout = StringToInt(tmp) - case "RedrivePolicy": - tmp := RedrivePolicy{} - var decodedPolicy struct { - MaxReceiveCount interface{} `json:"maxReceiveCount"` - DeadLetterTargetArn string `json:"deadLetterTargetArn"` - } - err := json.Unmarshal([]byte(attrValue), &decodedPolicy) - if err != nil || decodedPolicy.DeadLetterTargetArn == "" { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - // Support both int and string types (historic processing), set a default of 10 if not provided. - // Go will default into float64 for interface{} types when parsing numbers - receiveCount, ok := decodedPolicy.MaxReceiveCount.(float64) - if !ok { - receiveCount = 10 - t, ok := decodedPolicy.MaxReceiveCount.(string) - if ok { - r, err := strconv.ParseFloat(t, 64) - if err == nil { - receiveCount = r - } else { - log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) - } - } else { - log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) - } - } - tmp.MaxReceiveCount = StringToInt(receiveCount) - tmp.DeadLetterTargetArn = decodedPolicy.DeadLetterTargetArn - r.Attributes.RedrivePolicy = tmp - case "RedriveAllowPolicy": - var tmp map[string]interface{} - err := json.Unmarshal([]byte(attrValue), &tmp) - if err != nil { - log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) - continue - } - r.Attributes.RedriveAllowPolicy = tmp - } - } - return -} - -// TODO - copy Attributes for SNS - -// TODO - there are FIFO attributes and things too -// Attributes - SQS Attributes Available in create/set attributes requests. -// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html#SQS-CreateQueue-request-attributes -type Attributes struct { - DelaySeconds StringToInt `json:"DelaySeconds"` - MaximumMessageSize StringToInt `json:"MaximumMessageSize"` - MessageRetentionPeriod StringToInt `json:"MessageRetentionPeriod"` // NOTE: not implemented - Policy map[string]interface{} `json:"Policy"` // NOTE: not implemented - ReceiveMessageWaitTimeSeconds StringToInt `json:"ReceiveMessageWaitTimeSeconds"` - VisibilityTimeout StringToInt `json:"VisibilityTimeout"` - // Dead Letter Queues Only - RedrivePolicy RedrivePolicy `json:"RedrivePolicy"` - RedriveAllowPolicy map[string]interface{} `json:"RedriveAllowPolicy"` // NOTE: not implemented -} - -type RedrivePolicy struct { - MaxReceiveCount StringToInt `json:"maxReceiveCount"` - DeadLetterTargetArn string `json:"deadLetterTargetArn"` -} - -// UnmarshalJSON this will convert a JSON string of a Redrive Policy sub-doc (escaped characters and all) or -// a regular json document into the appropriate resulting struct. -func (r *RedrivePolicy) UnmarshalJSON(data []byte) error { - type basicRequest RedrivePolicy - - err := json.Unmarshal(data, (*basicRequest)(r)) - if err == nil { - return nil - } - - tmp, _ := strconv.Unquote(string(data)) - err = json.Unmarshal([]byte(tmp), (*basicRequest)(r)) - if err != nil { - return err - } - return nil -} - -func NewReceiveMessageRequest() *ReceiveMessageRequest { - return &ReceiveMessageRequest{} -} - -type ReceiveMessageRequest struct { - QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` - AttributeNames []string `json:"AttributeNames" schema:"AttributeNames"` - MessageSystemAttributeNames []string `json:"MessageSystemAttributeNames" schema:"MessageSystemAttributeNames"` - MessageAttributeNames []string `json:"MessageAttributeNames" schema:"MessageAttributeNames"` - MaxNumberOfMessages int `json:"MaxNumberOfMessages" schema:"MaxNumberOfMessages"` - VisibilityTimeout int `json:"VisibilityTimeout" schema:"VisibilityTimeout"` - WaitTimeSeconds int `json:"WaitTimeSeconds" schema:"WaitTimeSeconds"` - ReceiveRequestAttemptId string `json:"ReceiveRequestAttemptId" schema:"ReceiveRequestAttemptId"` -} - -func (r *ReceiveMessageRequest) SetAttributesFromForm(values url.Values) {} - -func NewCreateQueueRequest() *CreateQueueRequest { - return &CreateQueueRequest{ - Attributes: Attributes{ - DelaySeconds: 0, - MaximumMessageSize: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize), - MessageRetentionPeriod: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod), - ReceiveMessageWaitTimeSeconds: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds), - VisibilityTimeout: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout), - }, - } -} - -func NewChangeMessageVisibilityRequest() *ChangeMessageVisibilityRequest { - return &ChangeMessageVisibilityRequest{} -} - -type ChangeMessageVisibilityRequest struct { - QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` - ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` - VisibilityTimeout int `json:"VisibilityTimeout" schema:"VisibilityTimeout"` -} - -func (r *ChangeMessageVisibilityRequest) SetAttributesFromForm(values url.Values) {} - -func NewDeleteMessageRequest() *DeleteMessageRequest { - return &DeleteMessageRequest{} -} - -type DeleteMessageRequest struct { - QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` - ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` -} - -func (r *DeleteMessageRequest) SetAttributesFromForm(values url.Values) {} - -func NewPurgeQueueRequest() *PurgeQueueRequest { - return &PurgeQueueRequest{} -} - -type PurgeQueueRequest struct { - QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` -} - -func (r *PurgeQueueRequest) SetAttributesFromForm(values url.Values) {} - -func NewDeleteQueueRequest() *DeleteQueueRequest { - return &DeleteQueueRequest{} -} - -type DeleteQueueRequest struct { - QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` -} - -func (r *DeleteQueueRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/models/responses.go b/app/models/responses.go index f223941c..b25a5b39 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -62,7 +62,7 @@ type ResultMessage struct { // MarshalJSON first converts the ResultMessage to the shape which the SDKs // expect. When receiving a response from the JSON API, it apparently expects -// Attributes and MessageAttributes to be maps, rather than the former slice +// QueueAttributes and MessageAttributes to be maps, rather than the former slice // shape. func (r *ResultMessage) MarshalJSON() ([]byte, error) { m := &sqstypes.Message{ @@ -158,7 +158,7 @@ func (r ListQueuesResponse) GetRequestId() string { return r.Metadata.RequestId } -/*** Get Queue Attributes ***/ +/*** Get Queue QueueAttributes ***/ type Attribute struct { Name string `xml:"Name,omitempty"` Value string `xml:"Value,omitempty"` @@ -283,3 +283,29 @@ func (r DeleteQueueResponse) GetResult() interface{} { func (r DeleteQueueResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Create Subscription ***/ +type SubscribeResult struct { + SubscriptionArn string `xml:"SubscriptionArn"` +} + +type SubscribeResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result SubscribeResult `xml:"SubscribeResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r SubscribeResponse) GetResult() interface{} { + return r.Result +} + +func (r SubscribeResponse) GetRequestId() string { + return r.Metadata.RequestId +} + +/*** ConfirmSubscriptionResponse ***/ +type ConfirmSubscriptionResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result SubscribeResult `xml:"ConfirmSubscriptionResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} diff --git a/app/models/sns.go b/app/models/sns.go new file mode 100644 index 00000000..cdbe9594 --- /dev/null +++ b/app/models/sns.go @@ -0,0 +1,68 @@ +package models + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/Admiral-Piett/goaws/app" + + log "github.com/sirupsen/logrus" +) + +func NewSubscribeRequest() *SubscribeRequest { + return &SubscribeRequest{} +} + +type SubscribeRequest struct { + TopicArn string `json:"TopicArn" schema:"TopicArn"` + Endpoint string `json:"Endpoint" schema:"Endpoint"` + Protocol string `json:"Protocol" schema:"Protocol"` + Attributes SubscriptionAttributes `json:"Attributes"` +} + +func (r *SubscribeRequest) SetAttributesFromForm(values url.Values) { + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attributes.entry.%d.key", i) + attrName := values.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attributes.entry.%d.value", i) + attrValue := values.Get(valueKey) + if attrValue == "" { + continue + } + switch attrName { + case "RawMessageDelivery": + tmp, err := strconv.ParseBool(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.RawMessageDelivery = tmp + case "FilterPolicy": + var tmp map[string][]string + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.FilterPolicy = tmp + } + } + return +} + +type SubscriptionAttributes struct { + FilterPolicy app.FilterPolicy `json:"FilterPolicy" schema:"FilterPolicy"` + RawMessageDelivery bool `json:"RawMessageDelivery" schema:"RawMessageDelivery"` + //DeliveryPolicy map[string]interface{} `json:"DeliveryPolicy" schema:"DeliveryPolicy"` + //FilterPolicyScope string `json:"FilterPolicyScope" schema:"FilterPolicyScope"` + //RedrivePolicy RedrivePolicy `json:"RedrivePolicy" schema:"RawMessageDelivery"` + //SubscriptionRoleArn string `json:"SubscriptionRoleArn" schema:"SubscriptionRoleArn"` + //ReplayPolicy string `json:"ReplayPolicy" schema:"ReplayPolicy"` + //ReplayStatus string `json:"ReplayStatus" schema:"ReplayStatus"` +} diff --git a/app/models/sns_test.go b/app/models/sns_test.go new file mode 100644 index 00000000..e0c547cf --- /dev/null +++ b/app/models/sns_test.go @@ -0,0 +1,58 @@ +package models + +import ( + "net/url" + "testing" + + "github.com/Admiral-Piett/goaws/app" + + "github.com/stretchr/testify/assert" +) + +func TestSubscribeRequest_SetAttributesFromForm_success(t *testing.T) { + form := url.Values{} + form.Add("Attributes.entry.1.key", "RawMessageDelivery") + form.Add("Attributes.entry.1.value", "true") + form.Add("Attributes.entry.2.key", "FilterPolicy") + form.Add("Attributes.entry.2.value", "{\"filter\": [\"policy\"]}") + + cqr := &SubscribeRequest{ + Attributes: SubscriptionAttributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.True(t, cqr.Attributes.RawMessageDelivery) + assert.Equal(t, app.FilterPolicy{"filter": []string{"policy"}}, cqr.Attributes.FilterPolicy) +} + +func TestSubscribeRequest_SetAttributesFromForm_skips_invalid_values(t *testing.T) { + form := url.Values{} + form.Add("Attributes.entry.1.key", "RawMessageDelivery") + form.Add("Attributes.entry.1.value", "garbage") + form.Add("Attributes.entry.2.key", "FilterPolicy") + form.Add("Attributes.entry.2.value", "also-garbage") + + cqr := &SubscribeRequest{ + Attributes: SubscriptionAttributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.False(t, cqr.Attributes.RawMessageDelivery) + assert.Equal(t, app.FilterPolicy(nil), cqr.Attributes.FilterPolicy) +} + +func TestSubscribeRequest_SetAttributesFromForm_stops_if_attributes_not_numbered_sequentially(t *testing.T) { + form := url.Values{} + form.Add("Attributes.entry.2.key", "RawMessageDelivery") + form.Add("Attributes.entry.2.value", "garbage") + form.Add("Attributes.entry.3.key", "FilterPolicy") + form.Add("Attributes.entry.3.value", "also-garbage") + + cqr := &SubscribeRequest{ + Attributes: SubscriptionAttributes{}, + } + cqr.SetAttributesFromForm(form) + + assert.False(t, cqr.Attributes.RawMessageDelivery) + assert.Equal(t, app.FilterPolicy(nil), cqr.Attributes.FilterPolicy) +} diff --git a/app/models/sqs.go b/app/models/sqs.go new file mode 100644 index 00000000..9e5fbc79 --- /dev/null +++ b/app/models/sqs.go @@ -0,0 +1,457 @@ +package models + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/Admiral-Piett/goaws/app" + log "github.com/sirupsen/logrus" +) + +type CreateQueueRequest struct { + QueueName string `json:"QueueName" schema:"QueueName"` + Attributes QueueAttributes `json:"Attributes" schema:"Attribute"` + Tags map[string]string `json:"Tags" schema:"Tags"` + Version string `json:"Version" schema:"Version"` +} + +// TODO - is there an easier way to do this? Similar to the StringToInt type? +func (r *CreateQueueRequest) SetAttributesFromForm(values url.Values) { + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attribute.%d.Name", i) + attrName := values.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attribute.%d.Value", i) + attrValue := values.Get(valueKey) + if attrValue == "" { + continue + } + switch attrName { + case "DelaySeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.DelaySeconds = StringToInt(tmp) + case "MaximumMessageSize": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MaximumMessageSize = StringToInt(tmp) + case "MessageRetentionPeriod": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MessageRetentionPeriod = StringToInt(tmp) + case "Policy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.Policy = tmp + case "ReceiveMessageWaitTimeSeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.ReceiveMessageWaitTimeSeconds = StringToInt(tmp) + case "VisibilityTimeout": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.VisibilityTimeout = StringToInt(tmp) + case "RedrivePolicy": + tmp := RedrivePolicy{} + var decodedPolicy struct { + MaxReceiveCount interface{} `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + } + err := json.Unmarshal([]byte(attrValue), &decodedPolicy) + if err != nil || decodedPolicy.DeadLetterTargetArn == "" { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + // Support both int and string types (historic processing), set a default of 10 if not provided. + // Go will default into float64 for interface{} types when parsing numbers + receiveCount, ok := decodedPolicy.MaxReceiveCount.(float64) + if !ok { + receiveCount = 10 + t, ok := decodedPolicy.MaxReceiveCount.(string) + if ok { + r, err := strconv.ParseFloat(t, 64) + if err == nil { + receiveCount = r + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } + tmp.MaxReceiveCount = StringToInt(receiveCount) + tmp.DeadLetterTargetArn = decodedPolicy.DeadLetterTargetArn + r.Attributes.RedrivePolicy = tmp + case "RedriveAllowPolicy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.RedriveAllowPolicy = tmp + } + } + return +} + +func NewListQueuesRequest() *ListQueueRequest { + return &ListQueueRequest{} +} + +type ListQueueRequest struct { + MaxResults int `json:"MaxResults" schema:"MaxResults"` + NextToken string `json:"NextToken" schema:"NextToken"` + QueueNamePrefix string `json:"QueueNamePrefix" schema:"QueueNamePrefix"` +} + +func (r *ListQueueRequest) SetAttributesFromForm(values url.Values) { + maxResults, err := strconv.Atoi(values.Get("MaxResults")) + if err == nil { + r.MaxResults = maxResults + } + r.NextToken = values.Get("NextToken") + r.QueueNamePrefix = values.Get("QueueNamePrefix") +} + +func NewGetQueueAttributesRequest() *GetQueueAttributesRequest { + return &GetQueueAttributesRequest{} +} + +type GetQueueAttributesRequest struct { + QueueUrl string `json:"QueueUrl"` + AttributeNames []string `json:"AttributeNames"` +} + +func (r *GetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { + r.QueueUrl = values.Get("QueueUrl") + // TODO - test me + for i := 1; true; i++ { + attrKey := fmt.Sprintf("AttributeName.%d", i) + attrValue := values.Get(attrKey) + if attrValue == "" { + break + } + r.AttributeNames = append(r.AttributeNames, attrValue) + } +} + +/*** Send Message Request */ +func NewSendMessageRequest() *SendMessageRequest { + return &SendMessageRequest{ + MessageAttributes: make(map[string]MessageAttributeValue), + MessageSystemAttributes: make(map[string]MessageAttributeValue), + } +} + +type SendMessageRequest struct { + DelaySeconds int `json:"DelaySeconds" schema:"DelaySeconds"` + // MessageAttributes is custom attributes that users can add on the message as they like. + // Please see: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageAttributes + MessageAttributes map[string]MessageAttributeValue `json:"MessageAttributes" schema:"MessageAttributes"` + MessageBody string `json:"MessageBody" schema:"MessageBody"` + MessageDeduplicationId string `json:"MessageDeduplicationId" schema:"MessageDeduplicationId"` + MessageGroupId string `json:"MessageGroupId" schema:"MessageGroupId"` + // MessageSystemAttributes is custom attributes for AWS services. + // Please see: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageSystemAttributes + // On AWS, the only supported attribute is "AWSTraceHeader" that is for AWS X-Ray. + // Goaws does not contains X-Ray emulation, so currently MessageSystemAttributes is unsupported. + // TODO: Replace with a struct with known attributes "AWSTraceHeader". + MessageSystemAttributes map[string]MessageAttributeValue `json:"MessageSystemAttributes" schema:"MessageSystemAttributes"` + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} +type MessageAttributeValue struct { + BinaryListValues []string `json:"BinaryListValues"` // currently unsupported by AWS + BinaryValue string `json:"BinaryValue"` + DataType string `json:"DataType"` + StringListValues []string `json:"StringListValues"` // currently unsupported by AWS + StringValue string `json:"StringValue"` +} + +func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { + for i := 1; true; i++ { + nameKey := fmt.Sprintf("MessageAttribute.%d.Name", i) + name := values.Get(nameKey) + if name == "" { + break + } + + dataTypeKey := fmt.Sprintf("MessageAttribute.%d.Value.DataType", i) + dataType := values.Get(dataTypeKey) + if dataType == "" { + log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + continue + } + + stringValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.StringValue", i)) + binaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i)) + + r.MessageAttributes[name] = MessageAttributeValue{ + DataType: dataType, + StringValue: stringValue, + BinaryValue: binaryValue, + } + + if _, ok := r.MessageAttributes[name]; !ok { + log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + } + } +} + +// Get Queue Url Request +func NewGetQueueUrlRequest() *GetQueueUrlRequest { + return &GetQueueUrlRequest{} +} + +type GetQueueUrlRequest struct { + QueueName string `json:"QueueName"` + QueueOwnerAWSAccountId string `json:"QueueOwnerAWSAccountId"` // NOTE: not implemented +} + +func (r *GetQueueUrlRequest) SetAttributesFromForm(values url.Values) {} + +func NewSetQueueAttributesRequest() *SetQueueAttributesRequest { + return &SetQueueAttributesRequest{} +} + +type SetQueueAttributesRequest struct { + QueueUrl string `json:"QueueUrl"` + Attributes QueueAttributes `json:"Attributes"` +} + +func (r *SetQueueAttributesRequest) SetAttributesFromForm(values url.Values) { + r.QueueUrl = values.Get("QueueUrl") + // TODO - could we share with CreateQueueRequest? + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attribute.%d.Name", i) + attrName := values.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attribute.%d.Value", i) + attrValue := values.Get(valueKey) + if attrValue == "" { + continue + } + switch attrName { + case "DelaySeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.DelaySeconds = StringToInt(tmp) + case "MaximumMessageSize": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MaximumMessageSize = StringToInt(tmp) + case "MessageRetentionPeriod": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.MessageRetentionPeriod = StringToInt(tmp) + case "Policy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.Policy = tmp + case "ReceiveMessageWaitTimeSeconds": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.ReceiveMessageWaitTimeSeconds = StringToInt(tmp) + case "VisibilityTimeout": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.VisibilityTimeout = StringToInt(tmp) + case "RedrivePolicy": + tmp := RedrivePolicy{} + var decodedPolicy struct { + MaxReceiveCount interface{} `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` + } + err := json.Unmarshal([]byte(attrValue), &decodedPolicy) + if err != nil || decodedPolicy.DeadLetterTargetArn == "" { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + // Support both int and string types (historic processing), set a default of 10 if not provided. + // Go will default into float64 for interface{} types when parsing numbers + receiveCount, ok := decodedPolicy.MaxReceiveCount.(float64) + if !ok { + receiveCount = 10 + t, ok := decodedPolicy.MaxReceiveCount.(string) + if ok { + r, err := strconv.ParseFloat(t, 64) + if err == nil { + receiveCount = r + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } else { + log.Debugf("Failed to parse form attribute (maxReceiveCount) - %s: %s", attrName, attrValue) + } + } + tmp.MaxReceiveCount = StringToInt(receiveCount) + tmp.DeadLetterTargetArn = decodedPolicy.DeadLetterTargetArn + r.Attributes.RedrivePolicy = tmp + case "RedriveAllowPolicy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.RedriveAllowPolicy = tmp + } + } + return +} + +// TODO - there are FIFO attributes and things too +// QueueAttributes - SQS QueueAttributes Available in create/set attributes requests. +// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html#SQS-CreateQueue-request-attributes +type QueueAttributes struct { + DelaySeconds StringToInt `json:"DelaySeconds"` + MaximumMessageSize StringToInt `json:"MaximumMessageSize"` + MessageRetentionPeriod StringToInt `json:"MessageRetentionPeriod"` // NOTE: not implemented + Policy map[string]interface{} `json:"Policy"` // NOTE: not implemented + ReceiveMessageWaitTimeSeconds StringToInt `json:"ReceiveMessageWaitTimeSeconds"` + VisibilityTimeout StringToInt `json:"VisibilityTimeout"` + // Dead Letter Queues Only + RedrivePolicy RedrivePolicy `json:"RedrivePolicy"` + RedriveAllowPolicy map[string]interface{} `json:"RedriveAllowPolicy"` // NOTE: not implemented +} + +type RedrivePolicy struct { + MaxReceiveCount StringToInt `json:"maxReceiveCount"` + DeadLetterTargetArn string `json:"deadLetterTargetArn"` +} + +// UnmarshalJSON this will convert a JSON string of a Redrive Policy sub-doc (escaped characters and all) or +// a regular json document into the appropriate resulting struct. +func (r *RedrivePolicy) UnmarshalJSON(data []byte) error { + type basicRequest RedrivePolicy + + err := json.Unmarshal(data, (*basicRequest)(r)) + if err == nil { + return nil + } + + tmp, _ := strconv.Unquote(string(data)) + err = json.Unmarshal([]byte(tmp), (*basicRequest)(r)) + if err != nil { + return err + } + return nil +} + +func NewReceiveMessageRequest() *ReceiveMessageRequest { + return &ReceiveMessageRequest{} +} + +type ReceiveMessageRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` + AttributeNames []string `json:"AttributeNames" schema:"AttributeNames"` + MessageSystemAttributeNames []string `json:"MessageSystemAttributeNames" schema:"MessageSystemAttributeNames"` + MessageAttributeNames []string `json:"MessageAttributeNames" schema:"MessageAttributeNames"` + MaxNumberOfMessages int `json:"MaxNumberOfMessages" schema:"MaxNumberOfMessages"` + VisibilityTimeout int `json:"VisibilityTimeout" schema:"VisibilityTimeout"` + WaitTimeSeconds int `json:"WaitTimeSeconds" schema:"WaitTimeSeconds"` + ReceiveRequestAttemptId string `json:"ReceiveRequestAttemptId" schema:"ReceiveRequestAttemptId"` +} + +func (r *ReceiveMessageRequest) SetAttributesFromForm(values url.Values) {} + +func NewCreateQueueRequest() *CreateQueueRequest { + return &CreateQueueRequest{ + Attributes: QueueAttributes{ + DelaySeconds: 0, + MaximumMessageSize: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize), + MessageRetentionPeriod: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod), + ReceiveMessageWaitTimeSeconds: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds), + VisibilityTimeout: StringToInt(app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout), + }, + } +} + +func NewChangeMessageVisibilityRequest() *ChangeMessageVisibilityRequest { + return &ChangeMessageVisibilityRequest{} +} + +type ChangeMessageVisibilityRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` + ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` + VisibilityTimeout int `json:"VisibilityTimeout" schema:"VisibilityTimeout"` +} + +func (r *ChangeMessageVisibilityRequest) SetAttributesFromForm(values url.Values) {} + +func NewDeleteMessageRequest() *DeleteMessageRequest { + return &DeleteMessageRequest{} +} + +type DeleteMessageRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` + ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` +} + +func (r *DeleteMessageRequest) SetAttributesFromForm(values url.Values) {} + +func NewPurgeQueueRequest() *PurgeQueueRequest { + return &PurgeQueueRequest{} +} + +type PurgeQueueRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} + +func (r *PurgeQueueRequest) SetAttributesFromForm(values url.Values) {} + +func NewDeleteQueueRequest() *DeleteQueueRequest { + return &DeleteQueueRequest{} +} + +type DeleteQueueRequest struct { + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} + +func (r *DeleteQueueRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/models/models_test.go b/app/models/sqs_test.go similarity index 97% rename from app/models/models_test.go rename to app/models/sqs_test.go index 97eded66..a6b321e5 100644 --- a/app/models/models_test.go +++ b/app/models/sqs_test.go @@ -6,9 +6,9 @@ import ( "net/url" "testing" - "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/utils" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app" "github.com/stretchr/testify/assert" ) @@ -18,11 +18,11 @@ func TestNewCreateQueueRequest(t *testing.T) { app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds = 10 app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout = 30 defer func() { - utils.ResetApp() + test.ResetApp() }() expectedCreateQueueRequest := &CreateQueueRequest{ - Attributes: Attributes{ + Attributes: QueueAttributes{ DelaySeconds: 0, MaximumMessageSize: 262144, MessageRetentionPeriod: 345600, @@ -64,7 +64,7 @@ func TestCreateQueueRequest_SetAttributesFromForm_success(t *testing.T) { form.Add("Attribute.8.Value", "{\"i-am\":\"the-redrive-allow-policy\"}") cqr := &CreateQueueRequest{ - Attributes: Attributes{ + Attributes: QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 262144, MessageRetentionPeriod: 345600, @@ -95,7 +95,7 @@ func TestCreateQueueRequest_SetAttributesFromForm_success_handles_redrive_reciev form.Add("Attribute.1.Value", "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") cqr := &CreateQueueRequest{ - Attributes: Attributes{}, + Attributes: QueueAttributes{}, } cqr.SetAttributesFromForm(form) @@ -113,7 +113,7 @@ func TestCreateQueueRequest_SetAttributesFromForm_success_handles_redrive_reciev form.Add("Attribute.1.Value", "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") cqr := &CreateQueueRequest{ - Attributes: Attributes{}, + Attributes: QueueAttributes{}, } cqr.SetAttributesFromForm(form) @@ -131,7 +131,7 @@ func TestCreateQueueRequest_SetAttributesFromForm_success_default_unparsable_red form.Add("Attribute.1.Value", "{\"maxReceiveCount\": null, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") cqr := &CreateQueueRequest{ - Attributes: Attributes{}, + Attributes: QueueAttributes{}, } cqr.SetAttributesFromForm(form) @@ -158,7 +158,7 @@ func TestCreateQueueRequest_SetAttributesFromForm_success_skips_invalid_values(t form.Add("Attribute.8.Value", "garbage") cqr := &CreateQueueRequest{ - Attributes: Attributes{ + Attributes: QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 262144, MessageRetentionPeriod: 345600, @@ -348,7 +348,7 @@ func TestSetQueueAttributesRequest_SetAttributesFromForm_success(t *testing.T) { form.Add("Attribute.8.Value", "{\"i-am\":\"the-redrive-allow-policy\"}") cqr := &SetQueueAttributesRequest{ - Attributes: Attributes{ + Attributes: QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 262144, MessageRetentionPeriod: 345600, @@ -379,7 +379,7 @@ func TestSetQueueAttributesRequest_SetAttributesFromForm_success_handles_redrive form.Add("Attribute.1.Value", "{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") cqr := &SetQueueAttributesRequest{ - Attributes: Attributes{}, + Attributes: QueueAttributes{}, } cqr.SetAttributesFromForm(form) @@ -397,7 +397,7 @@ func TestSetQueueAttributesRequest_SetAttributesFromForm_success_handles_redrive form.Add("Attribute.1.Value", "{\"maxReceiveCount\": \"100\", \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") cqr := &SetQueueAttributesRequest{ - Attributes: Attributes{}, + Attributes: QueueAttributes{}, } cqr.SetAttributesFromForm(form) @@ -415,7 +415,7 @@ func TestSetQueueAttributesRequest_SetAttributesFromForm_success_default_unparsa form.Add("Attribute.1.Value", "{\"maxReceiveCount\": null, \"deadLetterTargetArn\":\"dead-letter-queue-arn\"}") cqr := &SetQueueAttributesRequest{ - Attributes: Attributes{}, + Attributes: QueueAttributes{}, } cqr.SetAttributesFromForm(form) @@ -442,7 +442,7 @@ func TestSetQueueAttributesRequest_SetAttributesFromForm_success_skips_invalid_v form.Add("Attribute.8.Value", "garbage") cqr := &SetQueueAttributesRequest{ - Attributes: Attributes{ + Attributes: QueueAttributes{ DelaySeconds: 1, MaximumMessageSize: 262144, MessageRetentionPeriod: 345600, diff --git a/app/router/router.go b/app/router/router.go index 70d4a6ca..3f4494f9 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -74,6 +74,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, + "Subscribe": sns.SubscribeV1, } var routingTable = map[string]http.HandlerFunc{ @@ -85,7 +86,6 @@ var routingTable = map[string]http.HandlerFunc{ "ListTopics": sns.ListTopics, "CreateTopic": sns.CreateTopic, "DeleteTopic": sns.DeleteTopic, - "Subscribe": sns.Subscribe, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, diff --git a/app/router/router_test.go b/app/router/router_test.go index 5c1c017b..e541e730 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -23,14 +23,9 @@ import ( "github.com/stretchr/testify/assert" - "github.com/Admiral-Piett/goaws/app/utils" + "github.com/Admiral-Piett/goaws/app/test" ) -func TestMain(m *testing.M) { - utils.InitializeDecoders() - m.Run() -} - func TestIndexServerhandler_POST_BadRequest(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. @@ -150,7 +145,7 @@ func TestIndexServerhandler_GET_GoodRequest_Pem_cert(t *testing.T) { } func TestEncodeResponse_success_xml(t *testing.T) { - w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + w, r := test.GenerateRequestInfo("POST", "/url", nil, false) encodeResponse(w, r, http.StatusOK, mocks.BaseResponse{Message: "test"}) @@ -162,7 +157,7 @@ func TestEncodeResponse_success_xml(t *testing.T) { } func TestEncodeResponse_success_skips_nil_body_xml(t *testing.T) { - w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + w, r := test.GenerateRequestInfo("POST", "/url", nil, false) encodeResponse(w, r, http.StatusOK, nil) @@ -171,7 +166,7 @@ func TestEncodeResponse_success_skips_nil_body_xml(t *testing.T) { } func TestEncodeResponse_success_json(t *testing.T) { - w, r := utils.GenerateRequestInfo("POST", "/url", nil, true) + w, r := test.GenerateRequestInfo("POST", "/url", nil, true) encodeResponse(w, r, http.StatusOK, mocks.BaseResponse{Message: "test"}) @@ -189,7 +184,7 @@ func TestEncodeResponse_success_skips_malformed_body_json(t *testing.T) { mock.MockGetResult = func() interface{} { return make(chan int) } - w, r := utils.GenerateRequestInfo("POST", "/url", nil, true) + w, r := test.GenerateRequestInfo("POST", "/url", nil, true) encodeResponse(w, r, http.StatusOK, mock) @@ -213,7 +208,7 @@ func TestActionHandler_v1_json(t *testing.T) { "CreateQueue": mockFunction, } - w, r := utils.GenerateRequestInfo("POST", "/url", nil, true) + w, r := test.GenerateRequestInfo("POST", "/url", nil, true) r.Header.Set("X-Amz-Target", "QueueService.CreateQueue") actionHandler(w, r) @@ -242,7 +237,7 @@ func TestActionHandler_v1_xml(t *testing.T) { "CreateQueue": mockFunction, } - w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + w, r := test.GenerateRequestInfo("POST", "/url", nil, false) form := url.Values{} form.Add("Action", "CreateQueue") r.PostForm = form @@ -260,6 +255,7 @@ func TestActionHandler_v1_xml(t *testing.T) { func TestActionHandler_v0_xml(t *testing.T) { defer func() { routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + // SQS "CreateQueue": sqs.CreateQueueV1, "ListQueues": sqs.ListQueuesV1, "GetQueueAttributes": sqs.GetQueueAttributesV1, @@ -271,6 +267,9 @@ func TestActionHandler_v0_xml(t *testing.T) { "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, + + // SNS + "Subscribe": sns.SubscribeV1, } routingTable = map[string]http.HandlerFunc{ // SQS @@ -281,7 +280,6 @@ func TestActionHandler_v0_xml(t *testing.T) { "ListTopics": sns.ListTopics, "CreateTopic": sns.CreateTopic, "DeleteTopic": sns.DeleteTopic, - "Subscribe": sns.Subscribe, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, @@ -304,7 +302,7 @@ func TestActionHandler_v0_xml(t *testing.T) { "CreateQueue": mockFunction, } - w, r := utils.GenerateRequestInfo("POST", "/url", nil, false) + w, r := test.GenerateRequestInfo("POST", "/url", nil, false) form := url.Values{} form.Add("Action", "CreateQueue") r.PostForm = form diff --git a/app/servertest/server_test.go b/app/servertest/server_test.go index 38d4d1b9..48be85a0 100644 --- a/app/servertest/server_test.go +++ b/app/servertest/server_test.go @@ -4,8 +4,6 @@ import ( "errors" "testing" - "github.com/Admiral-Piett/goaws/app/utils" - "encoding/json" "fmt" "io/ioutil" @@ -28,11 +26,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestMain(m *testing.M) { - utils.InitializeDecoders() - m.Run() -} - func TestNew(t *testing.T) { // Consume address srv, err := New("localhost:4100") diff --git a/app/sns.go b/app/sns.go index a4636e42..c43162f6 100644 --- a/app/sns.go +++ b/app/sns.go @@ -4,15 +4,6 @@ import ( "sync" ) -type SnsErrorType struct { - HttpError int - Type string - Code string - Message string -} - -var SnsErrors map[string]SnsErrorType - type MsgAttr struct { Type string Value string diff --git a/app/sns_messages.go b/app/sns_messages.go index 9d728636..188a3193 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -30,26 +30,7 @@ type CreateTopicResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Create Subscription ***/ -type SubscribeResult struct { - SubscriptionArn string `xml:"SubscriptionArn"` -} - -type SubscribeResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result SubscribeResult `xml:"SubscribeResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - -/*** ConfirmSubscriptionResponse ***/ -type ConfirmSubscriptionResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result SubscribeResult `xml:"ConfirmSubscriptionResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Set Subscription Response ***/ - type SetSubscriptionAttributesResponse struct { Xmlns string `xml:"xmlns,attr"` Metadata ResponseMetadata `xml:"ResponseMetadata"` diff --git a/app/sqs.go b/app/sqs.go index ff826aec..99dce4c1 100644 --- a/app/sqs.go +++ b/app/sqs.go @@ -12,19 +12,6 @@ import ( log "github.com/sirupsen/logrus" ) -type SqsErrorType struct { - HttpError int - Type string - Code string - Message string -} - -func (s *SqsErrorType) Error() string { - return s.Type -} - -var SqsErrors map[string]SqsErrorType - type Message struct { MessageBody []byte Uuid string diff --git a/app/utils/tests.go b/app/test/tests.go similarity index 98% rename from app/utils/tests.go rename to app/test/tests.go index 3be85742..49a5026a 100644 --- a/app/utils/tests.go +++ b/app/test/tests.go @@ -1,4 +1,4 @@ -package utils +package test import ( "bytes" diff --git a/app/utils/utils.go b/app/utils/utils.go index d2030b9e..0ed52dda 100644 --- a/app/utils/utils.go +++ b/app/utils/utils.go @@ -7,6 +7,8 @@ import ( "net/http" "net/url" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/interfaces" log "github.com/sirupsen/logrus" @@ -17,7 +19,7 @@ import ( var XmlDecoder *schema.Decoder var REQUEST_TRANSFORMER = TransformRequest -func InitializeDecoders() { +func init() { XmlDecoder = schema.NewDecoder() XmlDecoder.IgnoreUnknownKeys(true) } @@ -72,3 +74,18 @@ func ExtractQueueAttributes(u url.Values) map[string]string { } return attr } + +func CreateErrorResponseV1(errKey string, isSqs bool) (int, interfaces.AbstractResponseBody) { + var err interfaces.AbstractErrorResponse + if isSqs { + err = models.SqsErrors[errKey] + } else { + err = models.SnsErrors[errKey] + } + + respStruct := models.ErrorResponse{ + Result: err.Response(), + RequestId: "00000000-0000-0000-0000-000000000000", // TODO - fix + } + return err.StatusCode(), respStruct +} diff --git a/app/utils/utils_test.go b/app/utils/utils_test.go index 418761cd..2eda71b1 100644 --- a/app/utils/utils_test.go +++ b/app/utils/utils_test.go @@ -4,19 +4,16 @@ import ( "net/url" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/mocks" "github.com/stretchr/testify/assert" ) -func TestMain(m *testing.M) { - InitializeDecoders() - m.Run() -} - func TestTransformRequest_success_json(t *testing.T) { - _, r := GenerateRequestInfo("POST", "url", fixtures.JSONRequestBody, true) + _, r := test.GenerateRequestInfo("POST", "url", fixtures.JSONRequestBody, true) mock := &mocks.MockRequestBody{} @@ -28,7 +25,7 @@ func TestTransformRequest_success_json(t *testing.T) { } func TestTransformRequest_success_json_empty_request_accepted(t *testing.T) { - _, r := GenerateRequestInfo("POST", "url", nil, true) + _, r := test.GenerateRequestInfo("POST", "url", nil, true) mock := &mocks.MockRequestBody{} @@ -40,7 +37,7 @@ func TestTransformRequest_success_json_empty_request_accepted(t *testing.T) { } func TestTransformRequest_success_xml(t *testing.T) { - _, r := GenerateRequestInfo("POST", "url", nil, false) + _, r := test.GenerateRequestInfo("POST", "url", nil, false) form := url.Values{} form.Add("Action", "CreateQueue") form.Add("QueueName", "UnitTestQueue1") @@ -60,7 +57,7 @@ func TestTransformRequest_success_xml(t *testing.T) { } func TestTransformRequest_error_invalid_request_body_json(t *testing.T) { - _, r := GenerateRequestInfo("POST", "url", "\"I-am-garbage", true) + _, r := test.GenerateRequestInfo("POST", "url", "\"I-am-garbage", true) mock := &mocks.MockRequestBody{} @@ -72,7 +69,7 @@ func TestTransformRequest_error_invalid_request_body_json(t *testing.T) { } func TestTransformRequest_error_failure_to_parse_form_xml(t *testing.T) { - _, r := GenerateRequestInfo("POST", "url", nil, false) + _, r := test.GenerateRequestInfo("POST", "url", nil, false) mock := &mocks.MockRequestBody{} @@ -83,7 +80,7 @@ func TestTransformRequest_error_failure_to_parse_form_xml(t *testing.T) { } func TestTransformRequest_error_invalid_request_body_xml(t *testing.T) { - _, r := GenerateRequestInfo("POST", "url", nil, false) + _, r := test.GenerateRequestInfo("POST", "url", nil, false) form := url.Values{} form.Add("intField", "\"I-am-garbage") diff --git a/go.mod b/go.mod index 3feb252f..75d74af5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/aws/aws-sdk-go v1.47.3 - github.com/aws/aws-sdk-go-v2 v1.25.2 + github.com/aws/aws-sdk-go-v2 v1.30.0 github.com/aws/aws-sdk-go-v2/config v1.27.4 github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1 github.com/gavv/httpexpect/v2 v2.16.0 @@ -22,20 +22,22 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 // indirect - github.com/aws/smithy-go v1.20.1 // indirect + github.com/aws/smithy-go v1.20.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hpcloud/tail v1.0.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect diff --git a/go.sum b/go.sum index 1f37d935..bfb17cdb 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/aws/aws-sdk-go v1.47.3 h1:e0H6NFXiniCpR8Lu3lTphVdRaeRCDLAeRyTHd1tJSd8 github.com/aws/aws-sdk-go v1.47.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= +github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA= +github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= github.com/aws/aws-sdk-go-v2/config v1.27.4/go.mod h1:zq2FFXK3A416kiukwpsd+rD4ny6JC7QSkp4QdN1Mp2g= github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgpd9keKe2EAENgAuI= @@ -16,14 +18,20 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtF github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 h1:5ffmXjPtwRExp1zc7gENLgCPyHFbhEPwVTkTiH9niSk= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0= +github.com/aws/aws-sdk-go-v2/service/sns v1.30.1 h1:49R5Uh0Vi4Y21UHfLzmLmg7hwqQLyBmWqS0Vh+EpV2A= +github.com/aws/aws-sdk-go-v2/service/sns v1.30.1/go.mod h1:khPCTZaFImcuDtOLDqiveVdpQL53OXkK+/yoyao+kzk= github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1 h1:124rVNP6NbCfBZwiX1kfjMQrnsJtnpKeB0GalkuqSXo= github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1/go.mod h1:YijRvM1SAmuiIQ9pjfwahIEE3HMHUkx9P5oplL/Jnj4= github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ= @@ -34,6 +42,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1 github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA= github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -52,6 +62,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= diff --git a/smoke_tests/fixtures/responses.go b/smoke_tests/fixtures/responses.go index 0b4fbb5a..dfaf80f0 100644 --- a/smoke_tests/fixtures/responses.go +++ b/smoke_tests/fixtures/responses.go @@ -51,7 +51,7 @@ var BASE_GET_QUEUE_ATTRIBUTES_RESPONSE = models.GetQueueAttributesResponse{ }, { Name: "QueueArn", - Value: fmt.Sprintf("%s:new-queue-1", af.BASE_ARN), + Value: fmt.Sprintf("%s:new-queue-1", af.BASE_SQS_ARN), }, }}, Metadata: app.ResponseMetadata{RequestId: REQUEST_ID}, diff --git a/smoke_tests/main_test.go b/smoke_tests/main_test.go deleted file mode 100644 index 865a2c69..00000000 --- a/smoke_tests/main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package smoke_tests - -import ( - "testing" - - "github.com/Admiral-Piett/goaws/app/utils" -) - -func TestMain(m *testing.M) { - utils.InitializeDecoders() - m.Run() -} diff --git a/smoke_tests/sns_subscribe_test.go b/smoke_tests/sns_subscribe_test.go new file mode 100644 index 00000000..16fd8dec --- /dev/null +++ b/smoke_tests/sns_subscribe_test.go @@ -0,0 +1,151 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/aws/aws-sdk-go-v2/config" + + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/test" + + "github.com/gavv/httpexpect/v2" + + "github.com/Admiral-Piett/goaws/app" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sns" + "github.com/stretchr/testify/assert" + + af "github.com/Admiral-Piett/goaws/app/fixtures" +) + +func Test_Subscribe_json(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + response, err := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2")), + ReturnSubscriptionArn: true, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + app.SyncTopics.Lock() + defer app.SyncTopics.Unlock() + + subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions + assert.Len(t, subscriptions, 1) + + expectedFilterPolicy := app.FilterPolicy(nil) + assert.Equal(t, fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, &expectedFilterPolicy, subscriptions[0].FilterPolicy) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.False(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) +} + +func Test_Subscribe_json_with_additional_fields(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + response, err := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")), + Attributes: map[string]string{ + "FilterPolicy": "{\"filter\": [\"policy\"]}", + "RawMessageDelivery": "true", + }, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2")), + ReturnSubscriptionArn: true, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + app.SyncTopics.Lock() + defer app.SyncTopics.Unlock() + + subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions + assert.Len(t, subscriptions, 1) + + expectedFilterPolicy := app.FilterPolicy{"filter": []string{"policy"}} + assert.Equal(t, fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, &expectedFilterPolicy, subscriptions[0].FilterPolicy) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.True(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) +} + +func Test_Subscribe_xml(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + requestBody := struct { + Action string `schema:"Subscribe"` + TopicArn string `schema:"TopicArn"` + Endpoint string `schema:"Endpoint"` + Protocol string `schema:"Protocol"` + }{ + Action: "Subscribe", + TopicArn: fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2"), + Endpoint: fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2"), + Protocol: "sqs", + } + + e.POST("/"). + WithForm(requestBody). + WithFormField("Attributes.entry.1.key", "RawMessageDelivery"). + WithFormField("Attributes.entry.1.value", "true"). + WithFormField("Attributes.entry.2.key", "FilterPolicy"). + WithFormField("Attributes.entry.2.value", "{\"filter\": [\"policy\"]}"). + Expect(). + Status(http.StatusOK). + Body().Raw() + + subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions + assert.Len(t, subscriptions, 1) + + expectedFilterPolicy := app.FilterPolicy{"filter": []string{"policy"}} + assert.Equal(t, fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, &expectedFilterPolicy, subscriptions[0].FilterPolicy) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.True(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) +} diff --git a/smoke_tests/sqs_change_message_visibility_test.go b/smoke_tests/sqs_change_message_visibility_test.go index 6ac00694..f3ff0453 100644 --- a/smoke_tests/sqs_change_message_visibility_test.go +++ b/smoke_tests/sqs_change_message_visibility_test.go @@ -6,8 +6,9 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + af "github.com/Admiral-Piett/goaws/app/fixtures" - "github.com/Admiral-Piett/goaws/app/utils" sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -20,7 +21,7 @@ func Test_ChangeMessageVisibilityV1_json(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -60,7 +61,7 @@ func Test_ChangeMessageVisibilityV1_xml(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() ctx := context.Background() diff --git a/smoke_tests/sqs_create_queue_test.go b/smoke_tests/sqs_create_queue_test.go index b7c0c303..394020d6 100644 --- a/smoke_tests/sqs_create_queue_test.go +++ b/smoke_tests/sqs_create_queue_test.go @@ -8,12 +8,12 @@ import ( "net/http" "testing" + "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/sqs" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/Admiral-Piett/goaws/app/models" "github.com/mitchellh/copystructure" @@ -31,7 +31,7 @@ func Test_CreateQueueV1_json_no_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -75,7 +75,7 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -99,7 +99,7 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { //"Policy": "{\"this-is\": \"the-policy\"}", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", }, }) @@ -135,10 +135,10 @@ func Test_CreateQueueV1_json_with_attributes(t *testing.T) { exp3.Result.Attrs[2].Value = "3" exp3.Result.Attrs[3].Value = "4" exp3.Result.Attrs[4].Value = "5" - exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName) exp3.Result.Attrs = append(exp3.Result.Attrs, models.Attribute{ Name: "RedrivePolicy", - Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), }) r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) @@ -149,7 +149,7 @@ func Test_CreateQueueV1_json_with_attributes_as_ints(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -198,7 +198,7 @@ func Test_CreateQueueV1_json_with_attributes_as_ints(t *testing.T) { exp3.Result.Attrs[2].Value = "3" exp3.Result.Attrs[3].Value = "4" exp3.Result.Attrs[4].Value = "5" - exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName) r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) @@ -209,7 +209,7 @@ func Test_CreateQueueV1_json_with_attributes_ints_as_strings(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -263,7 +263,7 @@ func Test_CreateQueueV1_json_with_attributes_ints_as_strings(t *testing.T) { DeadLetterTargetArn string `json:"deadLetterTargetArn"` }{ MaxReceiveCount: "100", - DeadLetterTargetArn: fmt.Sprintf("%s:new-queue-1", af.BASE_ARN), + DeadLetterTargetArn: fmt.Sprintf("%s:new-queue-1", af.BASE_SQS_ARN), }, VisibilityTimeout: "30"}, } @@ -305,10 +305,10 @@ func Test_CreateQueueV1_json_with_attributes_ints_as_strings(t *testing.T) { exp3.Result.Attrs[2].Value = "3" exp3.Result.Attrs[3].Value = "0" exp3.Result.Attrs[4].Value = "30" - exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:new-string-queue", af.BASE_ARN) + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:new-string-queue", af.BASE_SQS_ARN) exp3.Result.Attrs = append(exp3.Result.Attrs, models.Attribute{ Name: "RedrivePolicy", - Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, af.QueueName), + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, af.QueueName), }) r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) @@ -319,7 +319,7 @@ func Test_CreateQueueV1_xml_no_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -370,7 +370,7 @@ func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -408,7 +408,7 @@ func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { WithFormField("Attribute.6.Name", "ReceiveMessageWaitTimeSeconds"). WithFormField("Attribute.6.Value", "4"). WithFormField("Attribute.7.Name", "RedrivePolicy"). - WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:new-queue-1\"}", af.BASE_ARN)). + WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:new-queue-1\"}", af.BASE_SQS_ARN)). WithFormField("Attribute.8.Name", "RedriveAllowPolicy"). WithFormField("Attribute.8.Value", "{\"this-is\": \"the-redrive-allow-policy\"}"). Expect(). @@ -447,10 +447,10 @@ func Test_CreateQueueV1_xml_with_attributes(t *testing.T) { exp3.Result.Attrs[2].Value = "3" exp3.Result.Attrs[3].Value = "4" exp3.Result.Attrs[4].Value = "5" - exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:new-queue-2", af.BASE_ARN) + exp3.Result.Attrs[9].Value = fmt.Sprintf("%s:new-queue-2", af.BASE_SQS_ARN) exp3.Result.Attrs = append(exp3.Result.Attrs, models.Attribute{ Name: "RedrivePolicy", - Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, af.QueueName), + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, af.QueueName), }) r3 := models.GetQueueAttributesResponse{} xml.Unmarshal([]byte(r), &r3) diff --git a/smoke_tests/sqs_delete_message_test.go b/smoke_tests/sqs_delete_message_test.go index 89c5a4f8..fd3c9bf1 100644 --- a/smoke_tests/sqs_delete_message_test.go +++ b/smoke_tests/sqs_delete_message_test.go @@ -6,8 +6,9 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + af "github.com/Admiral-Piett/goaws/app/fixtures" - "github.com/Admiral-Piett/goaws/app/utils" sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -20,7 +21,7 @@ func Test_DeleteMessageV1_json(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -59,7 +60,7 @@ func Test_DeleteMessageV1_xml(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) diff --git a/smoke_tests/sqs_delete_queue_test.go b/smoke_tests/sqs_delete_queue_test.go index 811d7b1e..251c2eba 100644 --- a/smoke_tests/sqs_delete_queue_test.go +++ b/smoke_tests/sqs_delete_queue_test.go @@ -6,11 +6,12 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/gavv/httpexpect/v2" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -24,7 +25,7 @@ func Test_DeleteQueueV1_json(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -55,7 +56,7 @@ func Test_DeleteQueueV1_xml(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) diff --git a/smoke_tests/sqs_get_queue_attributes_test.go b/smoke_tests/sqs_get_queue_attributes_test.go index 3a1a8576..d80b33c3 100644 --- a/smoke_tests/sqs_get_queue_attributes_test.go +++ b/smoke_tests/sqs_get_queue_attributes_test.go @@ -7,6 +7,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/models" sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" "github.com/gavv/httpexpect/v2" @@ -20,8 +22,6 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/stretchr/testify/assert" ) @@ -29,7 +29,7 @@ func Test_GetQueueAttributes_json_all(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -47,7 +47,7 @@ func Test_GetQueueAttributes_json_all(t *testing.T) { //"Policy": "{\"this-is\": \"the-policy\"}", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", } sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ @@ -62,12 +62,12 @@ func Test_GetQueueAttributes_json_all(t *testing.T) { dupe, _ := copystructure.Copy(attributes) expectedAttributes, _ := dupe.(map[string]string) - expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue) + expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue) expectedAttributes["ApproximateNumberOfMessages"] = "0" expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" expectedAttributes["CreatedTimestamp"] = "0000000000" expectedAttributes["LastModifiedTimestamp"] = "0000000000" - expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName) assert.Nil(t, err) assert.Equal(t, expectedAttributes, sdkResponse.Attributes) } @@ -76,7 +76,7 @@ func Test_GetQueueAttributes_json_specific_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -109,7 +109,7 @@ func Test_GetQueueAttributes_json_missing_attribute_name_returns_all(t *testing. server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -127,7 +127,7 @@ func Test_GetQueueAttributes_json_missing_attribute_name_returns_all(t *testing. //"Policy": "{\"this-is\": \"the-policy\"}", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", } sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ @@ -141,12 +141,12 @@ func Test_GetQueueAttributes_json_missing_attribute_name_returns_all(t *testing. dupe, _ := copystructure.Copy(attributes) expectedAttributes, _ := dupe.(map[string]string) - expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue) + expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue) expectedAttributes["ApproximateNumberOfMessages"] = "0" expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" expectedAttributes["CreatedTimestamp"] = "0000000000" expectedAttributes["LastModifiedTimestamp"] = "0000000000" - expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName) + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName) assert.Nil(t, err) assert.Equal(t, expectedAttributes, sdkResponse.Attributes) } @@ -155,7 +155,7 @@ func Test_GetQueueAttributes_xml_all(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -174,7 +174,7 @@ func Test_GetQueueAttributes_xml_all(t *testing.T) { "MessageRetentionPeriod": "3", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), } sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ QueueName: &af.QueueName, @@ -197,7 +197,7 @@ func Test_GetQueueAttributes_xml_all(t *testing.T) { expectedResponse.Result.Attrs[4].Value = "5" expectedResponse.Result.Attrs = append(expectedResponse.Result.Attrs, models.Attribute{ Name: "RedrivePolicy", - Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), }) r1 := models.GetQueueAttributesResponse{} @@ -209,7 +209,7 @@ func Test_GetQueueAttributes_xml_select_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -271,7 +271,7 @@ func Test_GetQueueAttributes_xml_missing_attribute_name_returns_all(t *testing.T server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -291,7 +291,7 @@ func Test_GetQueueAttributes_xml_missing_attribute_name_returns_all(t *testing.T //"Policy": "{\"this-is\": \"the-policy\"}", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), } sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ QueueName: &af.QueueName, @@ -323,7 +323,7 @@ func Test_GetQueueAttributes_xml_missing_attribute_name_returns_all(t *testing.T expectedResponse.Result.Attrs[4].Value = "5" expectedResponse.Result.Attrs = append(expectedResponse.Result.Attrs, models.Attribute{ Name: "RedrivePolicy", - Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + Value: fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), }) r1 := models.GetQueueAttributesResponse{} diff --git a/smoke_tests/sqs_get_queue_url_test.go b/smoke_tests/sqs_get_queue_url_test.go index 1d075729..418f5090 100644 --- a/smoke_tests/sqs_get_queue_url_test.go +++ b/smoke_tests/sqs_get_queue_url_test.go @@ -7,9 +7,10 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + af "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" @@ -22,7 +23,7 @@ func Test_GetQueueUrlV1_json_success_retrieve_queue_url(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -43,7 +44,7 @@ func Test_GetQueueUrlV1_json_error_not_found_queue(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -63,7 +64,7 @@ func Test_GetQueueUrlV1_xml_success_retrieve_queue_url(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -103,7 +104,7 @@ func Test_GetQueueUrlV1_xml_error_not_found_queue(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) diff --git a/smoke_tests/sqs_list_queues_test.go b/smoke_tests/sqs_list_queues_test.go index a33c56db..7fed9a24 100644 --- a/smoke_tests/sqs_list_queues_test.go +++ b/smoke_tests/sqs_list_queues_test.go @@ -7,14 +7,14 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/models" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/Admiral-Piett/goaws/app" "github.com/stretchr/testify/assert" @@ -27,7 +27,7 @@ func Test_ListQueues_json_no_queues(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -44,7 +44,7 @@ func Test_ListQueues_json_multiple_queues(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -76,7 +76,7 @@ func Test_ListQueues_json_multiple_queues_with_queue_name_prefix(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -108,7 +108,7 @@ func Test_ListQueues_xml_no_queues(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -135,7 +135,7 @@ func Test_ListQueues_xml_multiple_queues(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -173,7 +173,7 @@ func Test_ListQueues_xml_multiple_queues_with_queue_name_prefix(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) diff --git a/smoke_tests/sqs_purge_queue_test.go b/smoke_tests/sqs_purge_queue_test.go index d4d6db5e..2c5bf0f2 100644 --- a/smoke_tests/sqs_purge_queue_test.go +++ b/smoke_tests/sqs_purge_queue_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app/test" + "github.com/Admiral-Piett/goaws/app/models" "github.com/gavv/httpexpect/v2" @@ -15,8 +17,6 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/Admiral-Piett/goaws/app" "github.com/stretchr/testify/assert" @@ -31,7 +31,7 @@ func Test_PurgeQueueV1_json(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetApp() + test.ResetApp() app.CurrentEnvironment = defaultEnvironment }() @@ -74,7 +74,7 @@ func Test_PurgeQueueV1_xml(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetApp() + test.ResetApp() app.CurrentEnvironment = defaultEnvironment }() diff --git a/smoke_tests/sqs_receive_message_test.go b/smoke_tests/sqs_receive_message_test.go index 7c798647..fd5e36d0 100644 --- a/smoke_tests/sqs_receive_message_test.go +++ b/smoke_tests/sqs_receive_message_test.go @@ -8,9 +8,10 @@ import ( "sync" "testing" + "github.com/Admiral-Piett/goaws/app/test" + af "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -23,7 +24,7 @@ func Test_ReceiveMessageV1_json(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -58,7 +59,7 @@ func Test_ReceiveMessageV1_json_while_concurrent_delete(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -95,7 +96,7 @@ func Test_ReceiveMessageV1_xml(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) diff --git a/smoke_tests/sqs_send_message_test.go b/smoke_tests/sqs_send_message_test.go index 0bf4fc88..769711fd 100644 --- a/smoke_tests/sqs_send_message_test.go +++ b/smoke_tests/sqs_send_message_test.go @@ -7,9 +7,10 @@ import ( "testing" "time" + "github.com/Admiral-Piett/goaws/app/test" + af "github.com/Admiral-Piett/goaws/app/fixtures" "github.com/Admiral-Piett/goaws/app/models" - "github.com/Admiral-Piett/goaws/app/utils" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" @@ -22,7 +23,7 @@ func Test_SendMessageV1_json_no_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -66,7 +67,7 @@ func Test_SendMessageV1_json_with_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -162,7 +163,7 @@ func Test_SendMessageV1_json_MaximumMessageSize_TooBig(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -191,7 +192,7 @@ func Test_SendMessageV1_json_QueueNotExistant(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -214,7 +215,7 @@ func Test_SendMessageV1_xml_no_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -264,7 +265,7 @@ func Test_SendMessageV1_xml_with_attributes(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) diff --git a/smoke_tests/sqs_set_queue_attributes_test.go b/smoke_tests/sqs_set_queue_attributes_test.go index 513a1dfd..3b2e1164 100644 --- a/smoke_tests/sqs_set_queue_attributes_test.go +++ b/smoke_tests/sqs_set_queue_attributes_test.go @@ -6,6 +6,8 @@ import ( "net/http" "testing" + "github.com/Admiral-Piett/goaws/app/test" + sf "github.com/Admiral-Piett/goaws/smoke_tests/fixtures" "github.com/gavv/httpexpect/v2" @@ -18,8 +20,6 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/Admiral-Piett/goaws/app/utils" - "github.com/stretchr/testify/assert" ) @@ -27,7 +27,7 @@ func Test_SetQueueAttributes_json_multiple(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -49,7 +49,7 @@ func Test_SetQueueAttributes_json_multiple(t *testing.T) { //"Policy": "{\"this-is\": \"the-policy\"}", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100","deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", } @@ -68,12 +68,12 @@ func Test_SetQueueAttributes_json_multiple(t *testing.T) { dupe, _ := copystructure.Copy(attributes) expectedAttributes, _ := dupe.(map[string]string) - expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue) + expectedAttributes["RedrivePolicy"] = fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue) expectedAttributes["ApproximateNumberOfMessages"] = "0" expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" expectedAttributes["CreatedTimestamp"] = "0000000000" expectedAttributes["LastModifiedTimestamp"] = "0000000000" - expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, queueName) + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, queueName) assert.Nil(t, err) assert.Equal(t, expectedAttributes, sdkResponse.Attributes) } @@ -82,7 +82,7 @@ func Test_SetQueueAttributes_json_single(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) @@ -120,7 +120,7 @@ func Test_SetQueueAttributes_json_single(t *testing.T) { expectedAttributes["ApproximateNumberOfMessagesNotVisible"] = "0" expectedAttributes["CreatedTimestamp"] = "0000000000" expectedAttributes["LastModifiedTimestamp"] = "0000000000" - expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_ARN, queueName) + expectedAttributes["QueueArn"] = fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, queueName) assert.Nil(t, err) assert.Equal(t, expectedAttributes, sdkResponse.Attributes) } @@ -129,7 +129,7 @@ func Test_SetQueueAttributes_xml_all(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -161,7 +161,7 @@ func Test_SetQueueAttributes_xml_all(t *testing.T) { WithFormField("Attribute.6.Name", "ReceiveMessageWaitTimeSeconds"). WithFormField("Attribute.6.Value", "4"). WithFormField("Attribute.7.Name", "RedrivePolicy"). - WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:%s\"}", af.BASE_ARN, redriveQueue)). + WithFormField("Attribute.7.Value", fmt.Sprintf("{\"maxReceiveCount\": 100, \"deadLetterTargetArn\":\"%s:%s\"}", af.BASE_SQS_ARN, redriveQueue)). WithFormField("Attribute.8.Name", "RedriveAllowPolicy"). WithFormField("Attribute.8.Value", "{\"this-is\": \"the-redrive-allow-policy\"}"). Expect(). @@ -180,13 +180,13 @@ func Test_SetQueueAttributes_xml_all(t *testing.T) { //"Policy": "{\"this-is\": \"the-policy\"}", "ReceiveMessageWaitTimeSeconds": "4", "VisibilityTimeout": "5", - "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_ARN, redriveQueue), + "RedrivePolicy": fmt.Sprintf(`{"maxReceiveCount":"100", "deadLetterTargetArn":"%s:%s"}`, af.BASE_SQS_ARN, redriveQueue), //"RedriveAllowPolicy": "{\"this-is\": \"the-redrive-allow-policy\"}", "ApproximateNumberOfMessages": "0", "ApproximateNumberOfMessagesNotVisible": "0", "CreatedTimestamp": "0000000000", "LastModifiedTimestamp": "0000000000", - "QueueArn": fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName), + "QueueArn": fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName), } assert.Nil(t, err) assert.Equal(t, expectedAttributes, sdkResponse.Attributes) @@ -196,7 +196,7 @@ func Test_SetQueueAttributes_xml_single(t *testing.T) { server := generateServer() defer func() { server.Close() - utils.ResetResources() + test.ResetResources() }() e := httpexpect.Default(t, server.URL) @@ -236,7 +236,7 @@ func Test_SetQueueAttributes_xml_single(t *testing.T) { "ApproximateNumberOfMessagesNotVisible": "0", "CreatedTimestamp": "0000000000", "LastModifiedTimestamp": "0000000000", - "QueueArn": fmt.Sprintf("%s:%s", af.BASE_ARN, af.QueueName), + "QueueArn": fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName), } assert.Nil(t, err) assert.Equal(t, expectedAttributes, sdkResponse.Attributes) From 00ecbdad0778f50d8d61c7b821b8332b8f477bcd Mon Sep 17 00:00:00 2001 From: ksaiki Date: Thu, 4 Jul 2024 20:37:05 +0900 Subject: [PATCH 29/41] Add CreateTopicV1 for JSON support --- app/gosns/create_topic.go | 48 +++++ app/gosns/create_topic_test.go | 106 +++++++++++ app/gosns/gosns.go | 21 --- app/gosns/gosns_test.go | 103 +++++++---- app/models/responses.go | 19 ++ app/models/sns.go | 107 +++++++++++ app/models/sns_test.go | 61 +++++++ app/router/router.go | 7 +- app/router/router_test.go | 4 +- app/sns_messages.go | 11 -- go.mod | 4 +- go.sum | 8 - smoke_tests/sns_create_topic_test.go | 256 +++++++++++++++++++++++++++ 13 files changed, 674 insertions(+), 81 deletions(-) create mode 100644 app/gosns/create_topic.go create mode 100644 app/gosns/create_topic_test.go create mode 100644 smoke_tests/sns_create_topic_test.go diff --git a/app/gosns/create_topic.go b/app/gosns/create_topic.go new file mode 100644 index 00000000..7294a5fd --- /dev/null +++ b/app/gosns/create_topic.go @@ -0,0 +1,48 @@ +package gosns + +import ( + "fmt" + "net/http" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + log "github.com/sirupsen/logrus" +) + +func CreateTopicV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewCreateTopicRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - CreateTopicV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + topicName := requestBody.Name + topicArn := "" + if _, ok := app.SyncTopics.Topics[topicName]; ok { + topicArn = app.SyncTopics.Topics[topicName].Arn + } else { + topicArn = fmt.Sprintf("arn:aws:sns:%s:%s:%s", app.CurrentEnvironment.Region, app.CurrentEnvironment.AccountID, topicName) + + log.Info("Creating Topic:", topicName) + topic := &app.Topic{Name: topicName, Arn: topicArn} + topic.Subscriptions = make([]*app.Subscription, 0) + app.SyncTopics.Lock() + app.SyncTopics.Topics[topicName] = topic + app.SyncTopics.Unlock() + } + + uuid, _ := common.NewUUID() + respStruct := models.CreateTopicResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.CreateTopicResult{ + TopicArn: topicArn, + }, + Metadata: app.ResponseMetadata{RequestId: uuid}, + } + + return http.StatusOK, respStruct +} diff --git a/app/gosns/create_topic_test.go b/app/gosns/create_topic_test.go new file mode 100644 index 00000000..758299ec --- /dev/null +++ b/app/gosns/create_topic_test.go @@ -0,0 +1,106 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/fixtures" + "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 TestCreateTopicV1_success(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + targetTopicName := "new-topic-1" + request_success := models.CreateTopicRequest{ + Name: targetTopicName, + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.CreateTopicRequest) + *v = request_success + return true + } + + // No topic yet + assert.Equal(t, 0, len(app.SyncTopics.Topics)) + + // Request + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := CreateTopicV1(r) + + // Result + assert.Equal(t, http.StatusOK, status) + createTopicResponse, ok := response.(models.CreateTopicResponse) + assert.True(t, ok) + assert.Contains(t, createTopicResponse.Result.TopicArn, "arn:aws:sns:") + assert.Contains(t, createTopicResponse.Result.TopicArn, targetTopicName) + // 1 topic there + assert.Equal(t, 1, len(app.SyncTopics.Topics)) +} + +func TestCreateTopicV1_existant_topic(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + targetTopicName := "new-topic-1" + + // Same topic name with existant topic + request_success := models.CreateTopicRequest{ + Name: targetTopicName, + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.CreateTopicRequest) + *v = request_success + return true + } + + // Prepare existant topic + targetTopicArn := "arn:aws:sns:us-east-1:123456789012:" + targetTopicName + topic := &app.Topic{ + Name: targetTopicName, + Arn: targetTopicArn, + } + app.SyncTopics.Topics[targetTopicName] = topic + assert.Equal(t, 1, len(app.SyncTopics.Topics)) + + // Reques + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := CreateTopicV1(r) + + // Result + assert.Equal(t, http.StatusOK, status) + createTopicResponse, ok := response.(models.CreateTopicResponse) + assert.True(t, ok) + assert.Equal(t, targetTopicArn, createTopicResponse.Result.TopicArn) // Same with existant topic + // No additional topic + assert.Equal(t, 1, len(app.SyncTopics.Topics)) +} + +func TestCreateTopicV1_request_transformer_error(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + 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, _ := CreateTopicV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index b448e71f..093228b8 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -103,27 +103,6 @@ func ListTopics(w http.ResponseWriter, req *http.Request) { SendResponseBack(w, req, respStruct, content) } -func CreateTopic(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - topicName := req.FormValue("Name") - topicArn := "" - if _, ok := app.SyncTopics.Topics[topicName]; ok { - topicArn = app.SyncTopics.Topics[topicName].Arn - } else { - topicArn = "arn:aws:sns:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + topicName - - log.Println("Creating Topic:", topicName) - topic := &app.Topic{Name: topicName, Arn: topicArn} - topic.Subscriptions = make([]*app.Subscription, 0, 0) - app.SyncTopics.Lock() - app.SyncTopics.Topics[topicName] = topic - app.SyncTopics.Unlock() - } - uuid, _ := common.NewUUID() - respStruct := app.CreateTopicResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.CreateTopicResult{TopicArn: topicArn}, app.ResponseMetadata{RequestId: uuid}} - 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 fd1b99fe..2e64c2e2 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -9,6 +9,7 @@ import ( "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app/test" + "github.com/stretchr/testify/assert" "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/common" @@ -44,42 +45,11 @@ func TestListTopicshandler_POST_NoTopics(t *testing.T) { } } -func TestCreateTopicshandler_POST_CreateTopics(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) - } - - form := url.Values{} - form.Add("Action", "CreateTopic") - form.Add("Name", "UnitTestTopic1") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(CreateTopic) - - // 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 := "UnitTestTopic1" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - func TestPublishhandler_POST_SendMessage(t *testing.T) { + defer func() { + test.ResetApp() + }() + // 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) @@ -92,6 +62,13 @@ func TestPublishhandler_POST_SendMessage(t *testing.T) { form.Add("Message", "TestMessage1") req.PostForm = form + // Prepare existant topic + topic := &app.Topic{ + Name: "UnitTestTopic1", + Arn: "arn:aws:sns:local:000000000000:UnitTestTopic1", + } + app.SyncTopics.Topics["UnitTestTopic1"] = topic + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr := httptest.NewRecorder() handler := http.HandlerFunc(Publish) @@ -258,6 +235,10 @@ func TestPublishHandler_POST_FilterPolicyPassesTheMessage(t *testing.T) { } func TestPublish_No_Queue_Error_handler_POST_Success(t *testing.T) { + defer func() { + test.ResetApp() + }() + // 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) @@ -270,6 +251,13 @@ func TestPublish_No_Queue_Error_handler_POST_Success(t *testing.T) { form.Add("Message", "TestMessage1") req.PostForm = form + // Prepare existant topic + topic := &app.Topic{ + Name: "UnitTestTopic1", + Arn: "arn:aws:sns:local:000000000000:UnitTestTopic1", + } + app.SyncTopics.Topics["UnitTestTopic1"] = topic + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr := httptest.NewRecorder() handler := http.HandlerFunc(Publish) @@ -313,6 +301,23 @@ func TestListSubscriptionByTopicResponse_No_Owner(t *testing.T) { 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(ListSubscriptionsByTopic) @@ -355,6 +360,23 @@ func TestListSubscriptionsResponse_No_Owner(t *testing.T) { 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) @@ -395,6 +417,13 @@ func TestDeleteTopichandler_POST_Success(t *testing.T) { form.Add("Message", "TestMessage1") req.PostForm = form + // Prepare existant topic + topic := &app.Topic{ + Name: "local-topic1", + Arn: "arn:aws:sns:local:000000000000:local-topic1", + } + app.SyncTopics.Topics["local-topic1"] = topic + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr := httptest.NewRecorder() handler := http.HandlerFunc(DeleteTopic) @@ -421,6 +450,10 @@ func TestDeleteTopichandler_POST_Success(t *testing.T) { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } + + // Target topic should be disappeared + _, ok := app.SyncTopics.Topics["local-topic1"] + assert.False(t, ok) } func TestGetSubscriptionAttributesHandler_POST_Success(t *testing.T) { diff --git a/app/models/responses.go b/app/models/responses.go index b25a5b39..b4ce42aa 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -284,6 +284,25 @@ func (r DeleteQueueResponse) GetRequestId() string { return r.Metadata.RequestId } +/*** Create Topic Response */ +type CreateTopicResult struct { + TopicArn string `xml:"TopicArn"` +} + +type CreateTopicResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result CreateTopicResult `xml:"CreateTopicResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r CreateTopicResponse) GetResult() interface{} { + return r.Result +} + +func (r CreateTopicResponse) GetRequestId() string { + return r.Metadata.RequestId +} + /*** Create Subscription ***/ type SubscribeResult struct { SubscriptionArn string `xml:"SubscriptionArn"` diff --git a/app/models/sns.go b/app/models/sns.go index cdbe9594..cbbb70b7 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -11,6 +11,113 @@ import ( log "github.com/sirupsen/logrus" ) +func NewCreateTopicRequest() *CreateTopicRequest { + return &CreateTopicRequest{ + Attributes: TopicAttributes{ + FifoTopic: false, + SignatureVersion: 1, + TracingConfig: "Active", + ContentBasedDeduplication: false, + }, + } +} + +type CreateTopicRequest struct { + Name string `json:"Name" schema:"Name"` + + // Goaws unsupports below properties currently. + DataProtectionPolicy string `json:"DataProtectionPolicy" schema:"DataProtectionPolicy"` + Attributes TopicAttributes `json:"Attributes" schema:"Attributes"` + Tags map[string]string `json:"Tags" schema:"Tags"` +} + +// Ref: https://docs.aws.amazon.com/sns/latest/api/API_CreateTopic.html +type TopicAttributes struct { + DeliveryPolicy map[string]interface{} `json:"DeliveryPolicy"` // NOTE: not implemented + DisplayName string `json:"DisplayName"` // NOTE: not implemented + FifoTopic bool `json:"FifoTopic"` // NOTE: not implemented + Policy map[string]interface{} `json:"Policy"` // NOTE: not implemented + SignatureVersion StringToInt `json:"SignatureVersion"` // NOTE: not implemented + TracingConfig string `json:"TracingConfig"` // NOTE: not implemented + KmsMasterKeyId string `json:"KmsMasterKeyId"` // NOTE: not implemented + ArchivePolicy map[string]interface{} `json:"ArchivePolicy"` // NOTE: not implemented + BeginningArchiveTime string `json:"BeginningArchiveTime"` // NOTE: not implemented + ContentBasedDeduplication bool `json:"ContentBasedDeduplication"` // NOTE: not implemented +} + +func (r *CreateTopicRequest) SetAttributesFromForm(values url.Values) { + + for i := 1; true; i++ { + nameKey := fmt.Sprintf("Attribute.%d.Name", i) + attrName := values.Get(nameKey) + if attrName == "" { + break + } + + valueKey := fmt.Sprintf("Attribute.%d.Value", i) + attrValue := values.Get(valueKey) + if attrValue == "" { + continue + } + + switch attrName { + case "DeliveryPolicy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.DeliveryPolicy = tmp + case "DisplayName": + r.Attributes.DisplayName = attrValue + case "FifoTopic": + tmp, err := strconv.ParseBool(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.FifoTopic = tmp + case "Policy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.Policy = tmp + case "SignatureVersion": + tmp, err := strconv.Atoi(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.SignatureVersion = StringToInt(tmp) + case "TracingConfig": + r.Attributes.TracingConfig = attrValue + case "KmsMasterKeyId": + r.Attributes.KmsMasterKeyId = attrValue + case "ArchivePolicy": + var tmp map[string]interface{} + err := json.Unmarshal([]byte(attrValue), &tmp) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.ArchivePolicy = tmp + case "BeginningArchiveTime": + r.Attributes.BeginningArchiveTime = attrValue + case "ContentBasedDeduplication": + tmp, err := strconv.ParseBool(attrValue) + if err != nil { + log.Debugf("Failed to parse form attribute - %s: %s", attrName, attrValue) + continue + } + r.Attributes.ContentBasedDeduplication = tmp + } + } +} + func NewSubscribeRequest() *SubscribeRequest { return &SubscribeRequest{} } diff --git a/app/models/sns_test.go b/app/models/sns_test.go index e0c547cf..6d18e9c8 100644 --- a/app/models/sns_test.go +++ b/app/models/sns_test.go @@ -5,10 +5,71 @@ import ( "testing" "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/test" "github.com/stretchr/testify/assert" ) +func TestNewCreateTopicRequest(t *testing.T) { + defer func() { + test.ResetApp() + }() + + result := NewCreateTopicRequest() + + assert.Equal(t, false, result.Attributes.FifoTopic) + assert.Equal(t, StringToInt(1), result.Attributes.SignatureVersion) + assert.Equal(t, "Active", result.Attributes.TracingConfig) + assert.Equal(t, false, result.Attributes.ContentBasedDeduplication) +} + +func TestCreateTopicRequest_SetAttributesFromForm_success(t *testing.T) { + form := url.Values{} + form.Add("Action", "CreateQueue") + form.Add("QueueName", "new-queue") + form.Add("Version", "2012-11-05") + form.Add("Attribute.1.Name", "DeliveryPolicy") + form.Add("Attribute.1.Value", "{\"i-am\":\"the-policy\", \"name\":\"delivery-policy\"}") + form.Add("Attribute.2.Name", "DisplayName") + form.Add("Attribute.2.Value", "Foo") + form.Add("Attribute.3.Name", "FifoTopic") + form.Add("Attribute.3.Value", "true") + form.Add("Attribute.4.Name", "Policy") + form.Add("Attribute.4.Value", "{\"i-am\":\"the-policy\", \"name\":\"policy\"}") + form.Add("Attribute.5.Name", "SignatureVersion") + form.Add("Attribute.5.Value", "99") + form.Add("Attribute.6.Name", "TracingConfig") + form.Add("Attribute.6.Value", "PassThrough") + form.Add("Attribute.7.Name", "KmsMasterKeyId") + form.Add("Attribute.7.Value", "1234abcd-12ab-34cd-56ef-1234567890ab") + form.Add("Attribute.8.Name", "ArchivePolicy") + form.Add("Attribute.8.Value", "{\"i-am\":\"the-policy\", \"name\":\"archive-policy\"}") + form.Add("Attribute.9.Name", "BeginningArchiveTime") + form.Add("Attribute.9.Value", "2024-07-01T23:59:59+09:00") + form.Add("Attribute.10.Name", "ContentBasedDeduplication") + form.Add("Attribute.10.Value", "true") + + ctr := &CreateTopicRequest{} + ctr.SetAttributesFromForm(form) + + assert.Equal(t, 2, len(ctr.Attributes.DeliveryPolicy)) + assert.Equal(t, "the-policy", ctr.Attributes.DeliveryPolicy["i-am"]) + assert.Equal(t, "delivery-policy", ctr.Attributes.DeliveryPolicy["name"]) + assert.Equal(t, "Foo", ctr.Attributes.DisplayName) + assert.Equal(t, true, ctr.Attributes.FifoTopic) + assert.Equal(t, 2, len(ctr.Attributes.Policy)) + assert.Equal(t, "the-policy", ctr.Attributes.Policy["i-am"]) + assert.Equal(t, "policy", ctr.Attributes.Policy["name"]) + assert.Equal(t, StringToInt(99), ctr.Attributes.SignatureVersion) + assert.Equal(t, "PassThrough", ctr.Attributes.TracingConfig) + assert.Equal(t, "1234abcd-12ab-34cd-56ef-1234567890ab", ctr.Attributes.KmsMasterKeyId) + assert.Equal(t, 2, len(ctr.Attributes.ArchivePolicy)) + assert.Equal(t, "the-policy", ctr.Attributes.ArchivePolicy["i-am"]) + assert.Equal(t, "archive-policy", ctr.Attributes.ArchivePolicy["name"]) + assert.Equal(t, "2024-07-01T23:59:59+09:00", ctr.Attributes.BeginningArchiveTime) + assert.Equal(t, true, ctr.Attributes.ContentBasedDeduplication) +} + func TestSubscribeRequest_SetAttributesFromForm_success(t *testing.T) { form := url.Values{} form.Add("Attributes.entry.1.key", "RawMessageDelivery") diff --git a/app/router/router.go b/app/router/router.go index 3f4494f9..3ce2037f 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -63,6 +63,7 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // V1 - includes JSON Support (and of course the old XML). var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ + // SQS "CreateQueue": sqs.CreateQueueV1, "ListQueues": sqs.ListQueuesV1, "GetQueueAttributes": sqs.GetQueueAttributesV1, @@ -74,7 +75,10 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, - "Subscribe": sns.SubscribeV1, + + // SNS + "CreateTopic": sns.CreateTopicV1, + "Subscribe": sns.SubscribeV1, } var routingTable = map[string]http.HandlerFunc{ @@ -84,7 +88,6 @@ var routingTable = map[string]http.HandlerFunc{ // SNS "ListTopics": sns.ListTopics, - "CreateTopic": sns.CreateTopic, "DeleteTopic": sns.DeleteTopic, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, diff --git a/app/router/router_test.go b/app/router/router_test.go index e541e730..9e8343b4 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -269,7 +269,8 @@ func TestActionHandler_v0_xml(t *testing.T) { "DeleteQueue": sqs.DeleteQueueV1, // SNS - "Subscribe": sns.SubscribeV1, + "CreateTopic": sns.CreateTopicV1, + "Subscribe": sns.SubscribeV1, } routingTable = map[string]http.HandlerFunc{ // SQS @@ -278,7 +279,6 @@ func TestActionHandler_v0_xml(t *testing.T) { // SNS "ListTopics": sns.ListTopics, - "CreateTopic": sns.CreateTopic, "DeleteTopic": sns.DeleteTopic, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, diff --git a/app/sns_messages.go b/app/sns_messages.go index 188a3193..10d373ae 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -19,17 +19,6 @@ type ListTopicsResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Create Topic Response */ -type CreateTopicResult struct { - TopicArn string `xml:"TopicArn"` -} - -type CreateTopicResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result CreateTopicResult `xml:"CreateTopicResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Set Subscription Response ***/ type SetSubscriptionAttributesResponse struct { Xmlns string `xml:"xmlns,attr"` diff --git a/go.mod b/go.mod index 75d74af5..25bd08b0 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ require ( github.com/aws/aws-sdk-go v1.47.3 github.com/aws/aws-sdk-go-v2 v1.30.0 github.com/aws/aws-sdk-go-v2/config v1.27.4 + github.com/aws/aws-sdk-go-v2/service/sns v1.30.1 github.com/aws/aws-sdk-go-v2/service/sqs v1.31.1 github.com/gavv/httpexpect/v2 v2.16.0 github.com/ghodss/yaml v1.0.0 + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.1 github.com/mitchellh/copystructure v1.2.0 @@ -27,7 +29,6 @@ require ( github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sns v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 // indirect @@ -37,7 +38,6 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hpcloud/tail v1.0.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect diff --git a/go.sum b/go.sum index bfb17cdb..1f5058fc 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/aws/aws-sdk-go v1.47.3 h1:e0H6NFXiniCpR8Lu3lTphVdRaeRCDLAeRyTHd1tJSd8= github.com/aws/aws-sdk-go v1.47.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= -github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA= github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= @@ -16,12 +14,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgp github.com/aws/aws-sdk-go-v2/credentials v1.17.4/go.mod h1:+30tpwrkOgvkJL1rUZuRLoxcJwtI/OkeBLYnHxJtVe0= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= @@ -40,8 +34,6 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y= github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1LHPqXYk/QGlu2ew= github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA= -github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= -github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/smoke_tests/sns_create_topic_test.go b/smoke_tests/sns_create_topic_test.go new file mode 100644 index 00000000..2224e3c9 --- /dev/null +++ b/smoke_tests/sns_create_topic_test.go @@ -0,0 +1,256 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "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" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sns" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_CreateTopicV1_json_success(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + // Target test + topicName := "new-topic-1" + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + sdkResponse, err := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + // Should success + assert.Contains(t, *sdkResponse.TopicArn, topicName) + assert.Nil(t, err) + + // Get created topic + listTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListTopics", + Version: "2012-11-05", + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(listTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r2 := app.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) +} + +func Test_CreateTopicV1_json_existant_topic(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + // Prepare existant topic + topicName := "new-topic-1" + sdkConfig, err := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + sdkResponse, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + assert.Contains(t, *sdkResponse.TopicArn, topicName) + assert.Nil(t, err) + + // Target test: create topic with same name + sdkResponse, err = snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + // Should success + assert.Contains(t, *sdkResponse.TopicArn, topicName) + assert.Nil(t, err) + + // Topic should not be duplicated + listTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListTopics", + Version: "2012-11-05", + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(listTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r2 := app.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) +} + +func Test_CreateTopicV1_json_add_multiple_topics(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + // Prepare existant topic + topicName := "new-topic-1" + sdkConfig, err := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + sdkResponse, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + assert.Contains(t, *sdkResponse.TopicArn, topicName) + assert.Nil(t, err) + + // Target test: create topic with different name + topicName2 := "new-topic-2" + sdkResponse, err = snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName2, + }) + + // Should success + assert.Contains(t, *sdkResponse.TopicArn, topicName2) + assert.Nil(t, err) + + // Number of topic should be 2 + listTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListTopics", + Version: "2012-11-05", + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(listTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r2 := app.ListTopicsResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Equal(t, 2, len(r2.Result.Topics.Member)) +} + +func Test_CreateTopicV1_xml_success(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + // Target test + topicName := "new-topic-1" + createTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + Name string `xml:"Name"` + }{ + Action: "CreateTopic", + Version: "2012-11-05", + Name: topicName, + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(createTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r2 := models.CreateTopicResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Contains(t, r2.Result.TopicArn, topicName) + + // Get created topic + listTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListTopics", + Version: "2012-11-05", + } + r = e.POST("/"). + WithForm(listTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r3 := app.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) +} + +func Test_CreateTopicV1_xml_existant_topic(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + topicName := "new-topic-1" + + // Prepare existant topic + createTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + Name string `xml:"Name"` + }{ + Action: "CreateTopic", + Version: "2012-11-05", + Name: topicName, + } + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(createTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r2 := models.CreateTopicResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Contains(t, r2.Result.TopicArn, topicName) + + // Target test: create topic with same name + r = e.POST("/"). + WithForm(createTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r2 = models.CreateTopicResponse{} + xml.Unmarshal([]byte(r), &r2) + assert.Contains(t, r2.Result.TopicArn, topicName) + + // Topic should not be duplicated + listTopicsXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + }{ + Action: "ListTopics", + Version: "2012-11-05", + } + r = e.POST("/"). + WithForm(listTopicsXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + r3 := app.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) +} From 2374d82476c4601994200358113ddbd2c0b2eaaa Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Wed, 10 Jul 2024 15:34:46 -0400 Subject: [PATCH 30/41] Add UnsubscribeV1 for JSON support --- app/gosns/gosns.go | 26 --------- app/gosns/unsubscribe.go | 45 ++++++++++++++++ app/gosns/unsubscribe_test.go | 83 +++++++++++++++++++++++++++++ app/models/responses.go | 14 +++++ app/models/sns.go | 10 ++++ app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sns_messages.go | 6 --- smoke_tests/sns_unsubscribe_test.go | 79 +++++++++++++++++++++++++++ 9 files changed, 233 insertions(+), 34 deletions(-) create mode 100644 app/gosns/unsubscribe.go create mode 100644 app/gosns/unsubscribe_test.go create mode 100644 smoke_tests/sns_unsubscribe_test.go diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 093228b8..8f3ef1b5 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -306,32 +306,6 @@ func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { createErrorResponse(w, req, "SubscriptionNotFound") } -func Unsubscribe(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - subArn := req.FormValue("SubscriptionArn") - - log.Println("Unsubscribe:", subArn) - for _, topic := range app.SyncTopics.Topics { - for i, sub := range topic.Subscriptions { - if sub.SubscriptionArn == subArn { - app.SyncTopics.Lock() - - copy(topic.Subscriptions[i:], topic.Subscriptions[i+1:]) - topic.Subscriptions[len(topic.Subscriptions)-1] = nil - topic.Subscriptions = topic.Subscriptions[:len(topic.Subscriptions)-1] - - app.SyncTopics.Unlock() - - uuid, _ := common.NewUUID() - respStruct := app.UnsubscribeResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} - SendResponseBack(w, req, respStruct, content) - return - } - } - } - createErrorResponse(w, req, "SubscriptionNotFound") -} - func DeleteTopic(w http.ResponseWriter, req *http.Request) { content := req.FormValue("ContentType") topicArn := req.FormValue("TopicArn") diff --git a/app/gosns/unsubscribe.go b/app/gosns/unsubscribe.go new file mode 100644 index 00000000..e0d21ef7 --- /dev/null +++ b/app/gosns/unsubscribe.go @@ -0,0 +1,45 @@ +package gosns + +import ( + "net/http" + + "github.com/google/uuid" + + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + log "github.com/sirupsen/logrus" +) + +func UnsubscribeV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewUnsubscribeRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - UnsubscribeV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + log.Infof("Unsubscribe: %s", requestBody.SubscriptionArn) + for _, topic := range app.SyncTopics.Topics { + for i, sub := range topic.Subscriptions { + if sub.SubscriptionArn == requestBody.SubscriptionArn { + app.SyncTopics.Lock() + + copy(topic.Subscriptions[i:], topic.Subscriptions[i+1:]) + topic.Subscriptions[len(topic.Subscriptions)-1] = nil + topic.Subscriptions = topic.Subscriptions[:len(topic.Subscriptions)-1] + + app.SyncTopics.Unlock() + + respStruct := models.UnsubscribeResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: app.ResponseMetadata{RequestId: uuid.NewString()}, + } + return http.StatusOK, respStruct + } + } + } + return utils.CreateErrorResponseV1("SubscriptionNotFound", false) +} diff --git a/app/gosns/unsubscribe_test.go b/app/gosns/unsubscribe_test.go new file mode 100644 index 00000000..49bf6cfd --- /dev/null +++ b/app/gosns/unsubscribe_test.go @@ -0,0 +1,83 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app/fixtures" + + "github.com/Admiral-Piett/goaws/app/conf" + + "github.com/Admiral-Piett/goaws/app" + "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 TestUnsubscribeV1_success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + subArn := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0].SubscriptionArn + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.UnsubscribeRequest) + *v = models.UnsubscribeRequest{ + SubscriptionArn: subArn, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := UnsubscribeV1(r) + + assert.Equal(t, http.StatusOK, status) + _, ok := response.(models.UnsubscribeResponse) + + subs := app.SyncTopics.Topics["unit-topic1"].Subscriptions + assert.Len(t, subs, 0) + assert.True(t, ok) +} + +func TestUnsubscribeV1_invalid_request_body(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + 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) + status, _ := UnsubscribeV1(r) + + assert.Equal(t, http.StatusBadRequest, status) +} + +func TestUnsubscribeV1_invalid_subscription_arn(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + 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.UnsubscribeRequest) + *v = models.UnsubscribeRequest{ + SubscriptionArn: "garbage", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, _ := UnsubscribeV1(r) + + assert.Equal(t, http.StatusBadRequest, status) +} diff --git a/app/models/responses.go b/app/models/responses.go index b4ce42aa..07fa1e07 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -328,3 +328,17 @@ type ConfirmSubscriptionResponse struct { Result SubscribeResult `xml:"ConfirmSubscriptionResult"` Metadata app.ResponseMetadata `xml:"ResponseMetadata"` } + +/*** Delete Subscription ***/ +type UnsubscribeResponse struct { + Xmlns string `xml:"xmlns,attr"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r UnsubscribeResponse) GetResult() interface{} { + return nil +} + +func (r UnsubscribeResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index cbbb70b7..eab5352b 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -173,3 +173,13 @@ type SubscriptionAttributes struct { //ReplayPolicy string `json:"ReplayPolicy" schema:"ReplayPolicy"` //ReplayStatus string `json:"ReplayStatus" schema:"ReplayStatus"` } + +func NewUnsubscribeRequest() *UnsubscribeRequest { + return &UnsubscribeRequest{} +} + +type UnsubscribeRequest struct { + SubscriptionArn string `json:"SubscriptionArn" schema:"SubscriptionArn"` +} + +func (r *UnsubscribeRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index 3ce2037f..6b7293fb 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -79,6 +79,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR // SNS "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, + "Unsubscribe": sns.UnsubscribeV1, } var routingTable = map[string]http.HandlerFunc{ @@ -93,7 +94,6 @@ var routingTable = map[string]http.HandlerFunc{ "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, "ListSubscriptions": sns.ListSubscriptions, - "Unsubscribe": sns.Unsubscribe, "Publish": sns.Publish, // SNS Internal diff --git a/app/router/router_test.go b/app/router/router_test.go index 9e8343b4..529ff9b5 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -271,6 +271,7 @@ func TestActionHandler_v0_xml(t *testing.T) { // SNS "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, + "Unsubscribe": sns.UnsubscribeV1, } routingTable = map[string]http.HandlerFunc{ // SQS @@ -284,7 +285,6 @@ func TestActionHandler_v0_xml(t *testing.T) { "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, "ListSubscriptions": sns.ListSubscriptions, - "Unsubscribe": sns.Unsubscribe, "Publish": sns.Publish, // SNS Internal diff --git a/app/sns_messages.go b/app/sns_messages.go index 10d373ae..3e4fff4a 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -93,12 +93,6 @@ type PublishResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Unsubscribe ***/ -type UnsubscribeResponse struct { - Xmlns string `xml:"xmlns,attr"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Delete Topic ***/ type DeleteTopicResponse struct { Xmlns string `xml:"xmlns,attr"` diff --git a/smoke_tests/sns_unsubscribe_test.go b/smoke_tests/sns_unsubscribe_test.go new file mode 100644 index 00000000..92c2bc7f --- /dev/null +++ b/smoke_tests/sns_unsubscribe_test.go @@ -0,0 +1,79 @@ +package smoke_tests + +import ( + "context" + "net/http" + "testing" + + "github.com/aws/aws-sdk-go-v2/config" + + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/test" + + "github.com/gavv/httpexpect/v2" + + "github.com/Admiral-Piett/goaws/app" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sns" + "github.com/stretchr/testify/assert" +) + +func Test_Unsubscribe_json(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + subArn := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0].SubscriptionArn + response, err := snsClient.Unsubscribe(context.TODO(), &sns.UnsubscribeInput{ + SubscriptionArn: &subArn, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + app.SyncTopics.Lock() + defer app.SyncTopics.Unlock() + + subscriptions := app.SyncTopics.Topics["unit-topic1"].Subscriptions + assert.Len(t, subscriptions, 0) +} + +func Test_Unsubscribe_xml(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + subArn := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0].SubscriptionArn + requestBody := struct { + Action string `xml:"Action"` + SubscriptionArn string `schema:"SubscriptionArn"` + }{ + Action: "Unsubscribe", + SubscriptionArn: subArn, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + subscriptions := app.SyncTopics.Topics["unit-topic1"].Subscriptions + assert.Len(t, subscriptions, 0) +} From e5f9bdb18c4d0be81cb021ddbf715b1b142d5c31 Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Thu, 11 Jul 2024 12:28:07 +0900 Subject: [PATCH 31/41] Add SendMessageBatchV1 for JSON support fixes comment --- app/gosqs/gosqs.go | 136 ------ app/gosqs/gosqs_test.go | 282 ------------- app/gosqs/send_message_batch.go | 108 +++++ app/gosqs/send_message_batch_test.go | 350 ++++++++++++++++ app/models/responses.go | 28 ++ app/models/sqs.go | 71 ++++ app/router/router.go | 2 +- app/router/router_test.go | 3 +- app/sqs_messages.go | 20 - smoke_tests/sqs_send_message_batch_test.go | 466 +++++++++++++++++++++ 10 files changed, 1026 insertions(+), 440 deletions(-) create mode 100644 app/gosqs/send_message_batch.go create mode 100644 app/gosqs/send_message_batch_test.go create mode 100644 smoke_tests/sqs_send_message_batch_test.go diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 146e81c7..76a3655b 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -13,7 +13,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/common" "github.com/gorilla/mux" ) @@ -68,141 +67,6 @@ func PeriodicTasks(d time.Duration, quit <-chan struct{}) { } } -type SendEntry struct { - Id string - MessageBody string - MessageAttributes map[string]app.MessageAttributeValue - MessageGroupId string - MessageDeduplicationId string -} - -func SendMessageBatch(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - req.ParseForm() - - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - if _, ok := app.SyncQueues.Queues[queueName]; !ok { - createErrorResponse(w, req, "QueueNotFound") - return - } - - sendEntries := []SendEntry{} - - for k, v := range req.Form { - keySegments := strings.Split(k, ".") - if keySegments[0] == "SendMessageBatchRequestEntry" { - if len(keySegments) < 3 { - createErrorResponse(w, req, "EmptyBatchRequest") - return - } - keyIndex, err := strconv.Atoi(keySegments[1]) - if err != nil { - createErrorResponse(w, req, "Error") - return - } - - if len(sendEntries) < keyIndex { - newSendEntries := make([]SendEntry, keyIndex) - copy(newSendEntries, sendEntries) - sendEntries = newSendEntries - } - - if keySegments[2] == "Id" { - sendEntries[keyIndex-1].Id = v[0] - } - - if keySegments[2] == "MessageBody" { - sendEntries[keyIndex-1].MessageBody = v[0] - } - - if keySegments[2] == "MessageGroupId" { - sendEntries[keyIndex-1].MessageGroupId = v[0] - } - - if keySegments[2] == "MessageDeduplicationId" { - sendEntries[keyIndex-1].MessageDeduplicationId = v[0] - } - } - } - - if len(sendEntries) == 0 { - createErrorResponse(w, req, "EmptyBatchRequest") - return - } - - if len(sendEntries) > 10 { - createErrorResponse(w, req, "TooManyEntriesInBatchRequest") - return - } - ids := map[string]struct{}{} - for _, v := range sendEntries { - if _, ok := ids[v.Id]; ok { - createErrorResponse(w, req, "BatchEntryIdsNotDistinct") - return - } - ids[v.Id] = struct{}{} - } - - sentEntries := make([]app.SendMessageBatchResultEntry, 0) - log.Println("Putting Message in Queue:", queueName) - for _, sendEntry := range sendEntries { - msg := app.Message{MessageBody: []byte(sendEntry.MessageBody)} - if len(sendEntry.MessageAttributes) > 0 { - msg.MessageAttributes = sendEntry.MessageAttributes - msg.MD5OfMessageAttributes = common.HashAttributes(sendEntry.MessageAttributes) - } - msg.MD5OfMessageBody = common.GetMD5Hash(sendEntry.MessageBody) - msg.GroupID = sendEntry.MessageGroupId - msg.DeduplicationID = sendEntry.MessageDeduplicationId - msg.Uuid, _ = common.NewUUID() - msg.SentTime = time.Now() - app.SyncQueues.Lock() - fifoSeqNumber := "" - if app.SyncQueues.Queues[queueName].IsFIFO { - fifoSeqNumber = app.SyncQueues.Queues[queueName].NextSequenceNumber(sendEntry.MessageGroupId) - } - - if !app.SyncQueues.Queues[queueName].IsDuplicate(sendEntry.MessageDeduplicationId) { - app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) - } else { - log.Debugf("Message with deduplicationId [%s] in queue [%s] is duplicate ", sendEntry.MessageDeduplicationId, queueName) - } - - app.SyncQueues.Queues[queueName].InitDuplicatation(sendEntry.MessageDeduplicationId) - - app.SyncQueues.Unlock() - se := app.SendMessageBatchResultEntry{ - Id: sendEntry.Id, - MessageId: msg.Uuid, - MD5OfMessageBody: msg.MD5OfMessageBody, - MD5OfMessageAttributes: msg.MD5OfMessageAttributes, - SequenceNumber: fifoSeqNumber, - } - sentEntries = append(sentEntries, se) - log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) - } - - respStruct := app.SendMessageBatchResponse{ - "http://queue.amazonaws.com/doc/2012-11-05/", - app.SendMessageBatchResult{Entry: sentEntries}, - app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} - - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } -} - func numberOfHiddenMessagesInQueue(queue app.Queue) int { num := 0 for _, m := range queue.Messages { diff --git a/app/gosqs/gosqs_test.go b/app/gosqs/gosqs_test.go index 0fc5df2e..ae4e4a97 100644 --- a/app/gosqs/gosqs_test.go +++ b/app/gosqs/gosqs_test.go @@ -4,7 +4,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "strings" "sync" "testing" "time" @@ -15,287 +14,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSendMessageBatch_POST_QueueNotFound(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - form := url.Values{} - form.Add("Action", "SendMessageBatch") - form.Add("QueueUrl", "http://localhost:4100/queue/testing") - form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") - form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") - form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") - form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessageBatch) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "NonExistentQueue" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendMessageBatch_POST_NoEntry(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} - - form := url.Values{} - form.Add("Action", "SendMessageBatch") - form.Add("QueueUrl", "http://localhost:4100/queue/testing") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessageBatch) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "EmptyBatchRequest" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } - - req, _ = http.NewRequest("POST", "/", nil) - form.Add("SendMessageBatchRequestEntry", "") - req.PostForm = form - - rr = httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendMessageBatch_POST_IdNotDistinct(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} - - form := url.Values{} - form.Add("Action", "SendMessageBatch") - form.Add("QueueUrl", "http://localhost:4100/queue/testing") - form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") - form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") - form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_001") - form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessageBatch) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "BatchEntryIdsNotDistinct" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendMessageBatch_POST_TooManyEntries(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} - - form := url.Values{} - form.Add("Action", "SendMessageBatch") - form.Add("QueueUrl", "http://localhost:4100/queue/testing") - form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") - form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") - form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") - form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.3.Id", "test_msg_003") - form.Add("SendMessageBatchRequestEntry.3.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.4.Id", "test_msg_004") - form.Add("SendMessageBatchRequestEntry.4.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.5.Id", "test_msg_005") - form.Add("SendMessageBatchRequestEntry.5.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.6.Id", "test_msg_006") - form.Add("SendMessageBatchRequestEntry.6.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.7.Id", "test_msg_007") - form.Add("SendMessageBatchRequestEntry.7.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.8.Id", "test_msg_008") - form.Add("SendMessageBatchRequestEntry.8.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.9.Id", "test_msg_009") - form.Add("SendMessageBatchRequestEntry.9.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.10.Id", "test_msg_010") - form.Add("SendMessageBatchRequestEntry.10.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.11.Id", "test_msg_011") - form.Add("SendMessageBatchRequestEntry.11.MessageBody", "test%20message%20body%202") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessageBatch) - - // 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.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } - - // Check the response body is what we expect. - expected := "TooManyEntriesInBatchRequest" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendMessageBatch_POST_Success(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} - - form := url.Values{} - form.Add("Action", "SendMessageBatch") - form.Add("QueueUrl", "http://localhost:4100/queue/testing") - form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") - form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") - form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") - form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Name", "test_attribute_name_1") - form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.StringValue", "test_attribute_value_1") - form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.DataType", "String") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessageBatch) - - // 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 \n%v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect. - expected := "1c538b76fce1a234bce865025c02b042" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - -func TestSendMessageBatchToFIFOQueue_POST_Success(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - app.SyncQueues.Queues["testing.fifo"] = &app.Queue{ - Name: "testing.fifo", - IsFIFO: true, - } - - form := url.Values{} - form.Add("Action", "SendMessageBatch") - form.Add("QueueUrl", "http://localhost:4100/queue/testing.fifo") - form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") - form.Add("SendMessageBatchRequestEntry.1.MessageGroupId", "GROUP-X") - form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") - form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") - form.Add("SendMessageBatchRequestEntry.2.MessageGroupId", "GROUP-X") - form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") - form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Name", "test_attribute_name_1") - form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.StringValue", "test_attribute_value_1") - form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.DataType", "String") - form.Add("Version", "2012-11-05") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SendMessageBatch) - - // 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 \n%v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect. - expected := "1c538b76fce1a234bce865025c02b042" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { done := make(chan struct{}, 0) go PeriodicTasks(1*time.Second, done) diff --git a/app/gosqs/send_message_batch.go b/app/gosqs/send_message_batch.go new file mode 100644 index 00000000..cce6d8fe --- /dev/null +++ b/app/gosqs/send_message_batch.go @@ -0,0 +1,108 @@ +package gosqs + +import ( + "net/http" + "strings" + "time" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + + requestBody := models.NewSendMessageBatchRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + + if !ok { + log.Error("Invalid Request - SendMessageBatchV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", true) + } + + queueUrl := requestBody.QueueUrl + + // TODO: Remove this query param logic if it's not still valid or something + queueName := "" + if queueUrl == "" { + vars := mux.Vars(req) + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(queueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } + + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + return utils.CreateErrorResponseV1("QueueNotFound", true) + } + + sendEntries := requestBody.Entries + + if len(sendEntries) == 0 { + return utils.CreateErrorResponseV1("EmptyBatchRequest", true) + } + + if len(sendEntries) > 10 { + return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", true) + } + ids := map[string]struct{}{} + for _, v := range sendEntries { + if _, ok := ids[v.Id]; ok { + return utils.CreateErrorResponseV1("BatchEntryIdsNotDistinct", true) + } + ids[v.Id] = struct{}{} + } + + sentEntries := make([]models.SendMessageBatchResultEntry, 0) + log.Debug("Putting Message in Queue:", queueName) + for _, sendEntry := range sendEntries { + msg := app.Message{MessageBody: []byte(sendEntry.MessageBody)} + if len(sendEntry.MessageAttributes) > 0 { + oldStyleMessageAttributes := convertToOldMessageAttributeValueStructure(sendEntry.MessageAttributes) + msg.MessageAttributes = oldStyleMessageAttributes + msg.MD5OfMessageAttributes = common.HashAttributes(oldStyleMessageAttributes) + } + msg.MD5OfMessageBody = common.GetMD5Hash(sendEntry.MessageBody) + msg.GroupID = sendEntry.MessageGroupId + msg.DeduplicationID = sendEntry.MessageDeduplicationId + msg.Uuid, _ = common.NewUUID() + msg.SentTime = time.Now() + app.SyncQueues.Lock() + fifoSeqNumber := "" + if app.SyncQueues.Queues[queueName].IsFIFO { + fifoSeqNumber = app.SyncQueues.Queues[queueName].NextSequenceNumber(sendEntry.MessageGroupId) + } + + if !app.SyncQueues.Queues[queueName].IsDuplicate(sendEntry.MessageDeduplicationId) { + app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) + } else { + log.Debugf("Message with deduplicationId [%s] in queue [%s] is duplicate ", sendEntry.MessageDeduplicationId, queueName) + } + + app.SyncQueues.Queues[queueName].InitDuplicatation(sendEntry.MessageDeduplicationId) + + app.SyncQueues.Unlock() + se := models.SendMessageBatchResultEntry{ + Id: sendEntry.Id, + MessageId: msg.Uuid, + MD5OfMessageBody: msg.MD5OfMessageBody, + MD5OfMessageAttributes: msg.MD5OfMessageAttributes, + SequenceNumber: fifoSeqNumber, + } + sentEntries = append(sentEntries, se) + log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) + } + + respStruct := models.SendMessageBatchResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.SendMessageBatchResult{Entry: sentEntries}, + Metadata: models.BASE_RESPONSE_METADATA, + } + + return http.StatusOK, respStruct + +} diff --git a/app/gosqs/send_message_batch_test.go b/app/gosqs/send_message_batch_test.go new file mode 100644 index 00000000..a7249880 --- /dev/null +++ b/app/gosqs/send_message_batch_test.go @@ -0,0 +1,350 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/fixtures" + "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 TestSendMessageBatchV1_Success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageBatchRequest{ + Entries: []models.SendMessageBatchRequestEntry{ + { + Id: "test-msg-with-non-attribute", + MessageBody: "test%20message%20body%201", + }, + { + Id: "test-msg-with-single-attirbute", + MessageBody: "test%20message%20body%202", + MessageAttributes: map[string]models.MessageAttributeValue{ + "my-attribute-name": { + BinaryValue: "base64-encoded-value", + DataType: "hogehoge", + StringValue: "my-attribute-string-value", + }, + }, + }, + { + Id: "test-msg-with-multi-attirbute", + MessageBody: "test%20message%20body%203", + MessageAttributes: map[string]models.MessageAttributeValue{ + "my-attribute-name-1": { + BinaryValue: "base64-encoded-value-1", + DataType: "hogehoge", + StringValue: "my-attribute-string-value-1", + }, + "my-attribute-name-2": { + BinaryValue: "base64-encoded-value-2", + DataType: "hogehoge", + StringValue: "my-attribute-string-value-2", + }, + }, + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SendMessageBatchRequest) + *v = sendMessageRequest_success + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageBatchV1(r) + sendMessageBatchResponse, ok := response.(models.SendMessageBatchResponse) + + assert.Equal(t, http.StatusOK, status) + assert.True(t, ok) + + resultEntry := sendMessageBatchResponse.Result.Entry + assert.Equal(t, 3, len(resultEntry)) + assert.Contains(t, resultEntry[0].Id, "test-msg-with-non-attribute") + assert.Contains(t, resultEntry[1].Id, "test-msg-with-single-attirbute") + assert.Contains(t, resultEntry[2].Id, "test-msg-with-multi-attirbute") + assert.Empty(t, resultEntry[0].SequenceNumber) + assert.Empty(t, resultEntry[1].SequenceNumber) + assert.Empty(t, resultEntry[2].SequenceNumber) + +} + +func TestSendMessageBatchV1_Success_Fifo_Queue(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageBatchRequest{ + Entries: []models.SendMessageBatchRequestEntry{ + { + Id: "test_msg_001", + MessageBody: "test%20message%20body%201", + }, + { + Id: "test_msg_002", + MessageBody: "test%20message%20body%202", + }, + { + Id: "test_msg_003", + MessageBody: "test%20message%20body%203", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "fifo-queue-1"), + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SendMessageBatchRequest) + *v = sendMessageRequest_success + return true + } + + q := &app.Queue{ + Name: "fifo-queue-1", + MaximumMessageSize: 1024, + IsFIFO: true, + } + app.SyncQueues.Queues["fifo-queue-1"] = q + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageBatchV1(r) + sendMessageBatchResponse, ok := response.(models.SendMessageBatchResponse) + + assert.Equal(t, http.StatusOK, status) + assert.True(t, ok) + + resultEntry := sendMessageBatchResponse.Result.Entry + assert.Equal(t, 3, len(resultEntry)) + assert.Contains(t, resultEntry[0].Id, "test_msg_001") + assert.NotEmpty(t, resultEntry[0].SequenceNumber) + assert.Contains(t, resultEntry[1].Id, "test_msg_002") + assert.NotEmpty(t, resultEntry[1].SequenceNumber) + assert.Contains(t, resultEntry[2].Id, "test_msg_003") + assert.NotEmpty(t, resultEntry[2].SequenceNumber) +} + +func TestSendMessageBatchV1_Error_QueueNotFound(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_not_found_queue := models.SendMessageBatchRequest{ + Entries: []models.SendMessageBatchRequestEntry{ + { + Id: "test_msg_001", + MessageBody: "test%20message%20body%201", + }, + { + Id: "test_msg_002", + MessageBody: "test%20message%20body%202", + }, + { + Id: "test_msg_003", + MessageBody: "test%20message%20body%203", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "not-exist-queue1"), + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SendMessageBatchRequest) + *v = sendMessageRequest_not_found_queue + return true + } + + expected := models.ErrorResult{ + Type: "Not Found", + Code: "AWS.SimpleQueueService.NonExistentQueue", + Message: "The specified queue does not exist for this wsdl version.", + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageBatchV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + assert.Equal(t, http.StatusBadRequest, status) + assert.Equal(t, expected, errorResult) +} + +func TestSendMessageBatchV1_Error_NoEntry(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_no_entry := models.SendMessageBatchRequest{ + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SendMessageBatchRequest) + *v = sendMessageRequest_no_entry + return true + } + + expected := models.ErrorResult{ + Type: "EmptyBatchRequest", + Code: "AWS.SimpleQueueService.EmptyBatchRequest", + Message: "The batch request doesn't contain any entries.", + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageBatchV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + assert.Equal(t, http.StatusBadRequest, status) + assert.Equal(t, expected, errorResult) +} + +func TestSendMessageBatchV1_Error_IdNotDistinct(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_no_entry := models.SendMessageBatchRequest{ + Entries: []models.SendMessageBatchRequestEntry{ + { + Id: "test_msg_001", + MessageBody: "test%20message%20body%201", + }, + { + Id: "test_msg_001", + MessageBody: "test%20message%20body%202", + }, + { + Id: "test_msg_001", + MessageBody: "test%20message%20body%203", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SendMessageBatchRequest) + *v = sendMessageRequest_no_entry + return true + } + + expected := models.ErrorResult{ + Type: "BatchEntryIdsNotDistinct", + Code: "AWS.SimpleQueueService.BatchEntryIdsNotDistinct", + Message: "Two or more batch entries in the request have the same Id.", + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageBatchV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + assert.Equal(t, http.StatusBadRequest, status) + assert.Equal(t, expected, errorResult) +} + +func TestSendMessageBatchV1_Error_TooManyEntries(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + sendMessageRequest_success := models.SendMessageBatchRequest{ + Entries: []models.SendMessageBatchRequestEntry{ + { + Id: "test_msg_001", + MessageBody: "test%20message%20body%201", + }, + { + Id: "test_msg_002", + MessageBody: "test%20message%20body%202", + }, + { + Id: "test_msg_003", + MessageBody: "test%20message%20body%203", + }, + { + Id: "test_msg_004", + MessageBody: "test%20message%20body%204", + }, + { + Id: "test_msg_005", + MessageBody: "test%20message%20body%205", + }, + { + Id: "test_msg_006", + MessageBody: "test%20message%20body%206", + }, + { + Id: "test_msg_007", + MessageBody: "test%20message%20body%207", + }, + { + Id: "test_msg_008", + MessageBody: "test%20message%20body%208", + }, + { + Id: "test_msg_009", + MessageBody: "test%20message%20body%209", + }, + { + Id: "test_msg_010", + MessageBody: "test%20message%20body%210", + }, + { + Id: "test_msg_011", + MessageBody: "test%20message%20body%211", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SendMessageBatchRequest) + *v = sendMessageRequest_success + return true + } + + expected := models.ErrorResult{ + Type: "TooManyEntriesInBatchRequest", + Code: "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", + Message: "Maximum number of entries per request are 10.", + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := SendMessageBatchV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + assert.Equal(t, http.StatusBadRequest, status) + assert.Equal(t, expected, errorResult) + +} + +func TestSendMessageBatchV1_Error_transformer(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, _ := SendMessageBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, code) + +} diff --git a/app/models/responses.go b/app/models/responses.go index 07fa1e07..f2bdf7f6 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -243,6 +243,34 @@ func (r GetQueueUrlResponse) GetRequestId() string { return r.Metadata.RequestId } +type SendMessageBatchResultEntry struct { + Id string `xml:"Id"` + MessageId string `xml:"MessageId"` + MD5OfMessageBody string `xml:"MD5OfMessageBody,omitempty"` + MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes,omitempty"` + SequenceNumber string `xml:"SequenceNumber"` +} + +/*** Send Message Batch Response */ +type SendMessageBatchResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Result SendMessageBatchResult `xml:"SendMessageBatchResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +type SendMessageBatchResult struct { + Entry []SendMessageBatchResultEntry `xml:"SendMessageBatchResultEntry"` + Error []app.BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` +} + +func (r SendMessageBatchResponse) GetResult() interface{} { + return r.Result +} + +func (r SendMessageBatchResponse) GetRequestId() string { + return r.Metadata.RequestId +} + type SetQueueAttributesResponse struct { Xmlns string `xml:"xmlns,attr,omitempty"` Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` diff --git a/app/models/sqs.go b/app/models/sqs.go index 9e5fbc79..1467c6da 100644 --- a/app/models/sqs.go +++ b/app/models/sqs.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "strconv" + "strings" "github.com/Admiral-Piett/goaws/app" log "github.com/sirupsen/logrus" @@ -222,6 +223,76 @@ func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { } } +func NewSendMessageBatchRequest() *SendMessageBatchRequest { + return &SendMessageBatchRequest{} +} + +type SendMessageBatchRequest struct { + Entries []SendMessageBatchRequestEntry + QueueUrl string +} + +func (r *SendMessageBatchRequest) SetAttributesFromForm(values url.Values) { + for key := range values { + + keySegments := strings.Split(key, ".") + //If index value size is 3 or less, there is no attribute value + if len(keySegments) <= 3 { + continue + } + + // Both patterns below are supported here. + // strconv.Atoi(keySegments[1] - targets the index value in pattern: `Entries.1.MessageBody` + // strconv.Atoi(keySegments[3] - targets the index value in pattern: `Entries.1.MessageAttributes.1.Name` + entryIndex, err1 := strconv.Atoi(keySegments[1]) + attributeIndex, err2 := strconv.Atoi(keySegments[3]) + + // If the entry index and attribute index cannot be obtained, the attribute will not be set, so skip + if err1 != nil || err2 != nil { + continue + } + + nameKey := fmt.Sprintf("Entries.%d.MessageAttributes.%d.Name", entryIndex, attributeIndex) + if key != nameKey { + continue + } + name := values.Get(nameKey) + dataTypeKey := fmt.Sprintf("Entries.%d.MessageAttributes.%d.Value.DataType", entryIndex, attributeIndex) + dataType := values.Get(dataTypeKey) + if dataType == "" { + log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + continue + } + + stringValue := values.Get(fmt.Sprintf("Entries.%d.MessageAttributes.%d.Value.StringValue", entryIndex, attributeIndex)) + binaryValue := values.Get(fmt.Sprintf("Entries.%d.MessageAttributes.%d.Value.BinaryValue", entryIndex, attributeIndex)) + + if r.Entries[entryIndex].MessageAttributes == nil { + r.Entries[entryIndex].MessageAttributes = make(map[string]MessageAttributeValue) + } + + r.Entries[entryIndex].MessageAttributes[name] = MessageAttributeValue{ + DataType: dataType, + StringValue: stringValue, + BinaryValue: binaryValue, + } + + if _, ok := r.Entries[entryIndex].MessageAttributes[name]; !ok { + log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + } + } +} + +type SendMessageBatchRequestEntry struct { + Id string `json:"Id" schema:"Id"` + MessageBody string `json:"MessageBody" schema:"MessageBody"` + DelaySeconds int `json:"DelaySeconds" schema:"DelaySeconds"` // NOTE: not implemented + MessageAttributes map[string]MessageAttributeValue `json:"MessageAttributes" schema:"MessageAttributes"` + MessageDeduplicationId string `json:"MessageDeduplicationId" schema:"MessageDeduplicationId"` + MessageGroupId string `json:"MessageGroupId" schema:"MessageGroupId"` + MessageSystemAttributes map[string]MessageAttributeValue `json:"MessageSystemAttributes" schema:"MessageSystemAttributes"` // NOTE: not implemented +} + // Get Queue Url Request func NewGetQueueUrlRequest() *GetQueueUrlRequest { return &GetQueueUrlRequest{} diff --git a/app/router/router.go b/app/router/router.go index 6b7293fb..1c6bfc40 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -75,6 +75,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, + "SendMessageBatch": sqs.SendMessageBatchV1, // SNS "CreateTopic": sns.CreateTopicV1, @@ -84,7 +85,6 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR var routingTable = map[string]http.HandlerFunc{ // SQS - "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, // SNS diff --git a/app/router/router_test.go b/app/router/router_test.go index 529ff9b5..ea637d45 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -267,15 +267,16 @@ func TestActionHandler_v0_xml(t *testing.T) { "GetQueueUrl": sqs.GetQueueUrlV1, "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, + "SendMessageBatch": sqs.SendMessageBatchV1, // SNS "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, "Unsubscribe": sns.UnsubscribeV1, } + routingTable = map[string]http.HandlerFunc{ // SQS - "SendMessageBatch": sqs.SendMessageBatch, "DeleteMessageBatch": sqs.DeleteMessageBatch, // SNS diff --git a/app/sqs_messages.go b/app/sqs_messages.go index 20da794e..ae08a6d4 100644 --- a/app/sqs_messages.go +++ b/app/sqs_messages.go @@ -4,14 +4,6 @@ type DeleteMessageBatchResultEntry struct { Id string `xml:"Id"` } -type SendMessageBatchResultEntry struct { - Id string `xml:"Id"` - MessageId string `xml:"MessageId"` - MD5OfMessageBody string `xml:"MD5OfMessageBody,omitempty"` - MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes,omitempty"` - SequenceNumber string `xml:"SequenceNumber"` -} - type BatchResultErrorEntry struct { Code string `xml:"Code"` Id string `xml:"Id"` @@ -30,15 +22,3 @@ type DeleteMessageBatchResponse struct { Result DeleteMessageBatchResult `xml:"DeleteMessageBatchResult"` Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` } - -type SendMessageBatchResult struct { - Entry []SendMessageBatchResultEntry `xml:"SendMessageBatchResultEntry"` - Error []BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` -} - -/*** Delete Message Batch Response */ -type SendMessageBatchResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Result SendMessageBatchResult `xml:"SendMessageBatchResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} diff --git a/smoke_tests/sqs_send_message_batch_test.go b/smoke_tests/sqs_send_message_batch_test.go new file mode 100644 index 00000000..e274c572 --- /dev/null +++ b/smoke_tests/sqs_send_message_batch_test.go @@ -0,0 +1,466 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "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/sqs" + "github.com/aws/aws-sdk-go-v2/service/sqs/types" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_SendMessageBatchV1_Json_Error_Queue_Not_Found(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + testId := "test-msg" + messageBody := "test%20message%20body%201" + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + + SendMessageBatchOutput, error := sqsClient.SendMessageBatch(context.TODO(), &sqs.SendMessageBatchInput{ + Entries: []types.SendMessageBatchRequestEntry{ + { + Id: &testId, + MessageBody: &messageBody, + }, + }, + QueueUrl: &queueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.NonExistentQueue") + assert.Contains(t, error.Error(), "The specified queue does not exist for this wsdl version.") + assert.Nil(t, SendMessageBatchOutput) + +} + +func Test_SendMessageBatchV1_Json_Error_No_Entry(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + SendMessageBatchOutput, error := sqsClient.SendMessageBatch(context.TODO(), &sqs.SendMessageBatchInput{ + Entries: make([]types.SendMessageBatchRequestEntry, 0), + QueueUrl: &queueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.EmptyBatchRequest") + assert.Contains(t, error.Error(), "The batch request doesn't contain any entries.") + assert.Nil(t, SendMessageBatchOutput) + +} + +func TestSendMessageBatchV1_Json_Error_IdNotDistinct(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + duplicatedId := "test_msg_001" + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + messageBody3 := "test%20message%20body%203" + + SendMessageBatchOutput, error := sqsClient.SendMessageBatch(context.TODO(), &sqs.SendMessageBatchInput{ + Entries: []types.SendMessageBatchRequestEntry{ + { + Id: &duplicatedId, + MessageBody: &messageBody1, + }, + { + Id: &duplicatedId, + MessageBody: &messageBody2, + }, + { + Id: &duplicatedId, + MessageBody: &messageBody3, + }, + }, + QueueUrl: &queueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.BatchEntryIdsNotDistinct") + assert.Contains(t, error.Error(), "Two or more batch entries in the request have the same Id.") + assert.Nil(t, SendMessageBatchOutput) +} + +func TestSendMessageBatchV1_Json_Error_TooManyEntries(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + messageId1 := "test_msg_001" + messageId2 := "test_msg_002" + messageId3 := "test_msg_003" + messageId4 := "test_msg_004" + messageId5 := "test_msg_005" + messageId6 := "test_msg_006" + messageId7 := "test_msg_007" + messageId8 := "test_msg_008" + messageId9 := "test_msg_009" + messageId10 := "test_msg_010" + messageId11 := "test_msg_011" + + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + messageBody3 := "test%20message%20body%203" + messageBody4 := "test%20message%20body%204" + messageBody5 := "test%20message%20body%205" + messageBody6 := "test%20message%20body%206" + messageBody7 := "test%20message%20body%207" + messageBody8 := "test%20message%20body%208" + messageBody9 := "test%20message%20body%209" + messageBody10 := "test%20message%20body%210" + messageBody11 := "test%20message%20body%211" + + SendMessageBatchOutput, error := sqsClient.SendMessageBatch(context.TODO(), &sqs.SendMessageBatchInput{ + Entries: []types.SendMessageBatchRequestEntry{ + { + Id: &messageId1, + MessageBody: &messageBody1, + }, + { + Id: &messageId2, + MessageBody: &messageBody2, + }, + { + Id: &messageId3, + MessageBody: &messageBody3, + }, + { + Id: &messageId4, + MessageBody: &messageBody4, + }, + { + Id: &messageId5, + MessageBody: &messageBody5, + }, + { + Id: &messageId6, + MessageBody: &messageBody6, + }, + { + Id: &messageId7, + MessageBody: &messageBody7, + }, + { + Id: &messageId8, + MessageBody: &messageBody8, + }, + { + Id: &messageId9, + MessageBody: &messageBody9, + }, + { + Id: &messageId10, + MessageBody: &messageBody10, + }, + { + Id: &messageId11, + MessageBody: &messageBody11, + }, + }, + QueueUrl: &queueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.TooManyEntriesInBatchRequest") + assert.Contains(t, error.Error(), "Maximum number of entries per request are 10.") + assert.Nil(t, SendMessageBatchOutput) +} + +func TestSendMessageBatchV1_Json_Success(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, af.QueueName) + + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + messageId1 := "test_msg_001" + messageId2 := "test_msg_002" + + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + + binaryAttribute := "binary" + stringAttribute := "string" + numberAttribute := "number" + + binaryType := "Binary" + stringType := "String" + numberType := "Number" + + binaryValue := "base64-encoded-value" + stringValue := "hogeValue" + numberValue := "100" + + sendMessageBatchOutput, error := sqsClient.SendMessageBatch(context.TODO(), &sqs.SendMessageBatchInput{ + Entries: []types.SendMessageBatchRequestEntry{ + { + Id: &messageId1, + MessageBody: &messageBody1, + }, + { + Id: &messageId2, + MessageBody: &messageBody2, + MessageAttributes: map[string]types.MessageAttributeValue{ + binaryAttribute: { + BinaryValue: []byte(binaryValue), + DataType: &binaryType, + }, + stringAttribute: { + DataType: &stringType, + StringValue: &stringValue, + }, + numberAttribute: { + DataType: &numberType, + StringValue: &numberValue, + }, + }, + }, + }, + QueueUrl: &queueUrl, + }) + + assert.NotNil(t, sendMessageBatchOutput) + assert.Nil(t, error) + + getQueueAttributeOutput, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &queueUrl, + }) + assert.Equal(t, "2", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + receiveMessageOutput, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: &queueUrl, + MaxNumberOfMessages: 10, + }) + + receivedMessage1 := receiveMessageOutput.Messages[0] + receivedMessage2 := receiveMessageOutput.Messages[1] + + assert.Equal(t, messageBody1, string(*receivedMessage1.Body)) + assert.Equal(t, messageBody2, string(*receivedMessage2.Body)) + assert.Equal(t, 3, len(receivedMessage2.MessageAttributes)) + + var attr1, attr2, attr3 models.ResultMessageAttribute + for k, attr := range receivedMessage2.MessageAttributes { + if k == binaryAttribute { + attr1.Name = k + attr1.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + BinaryValue: string(attr.BinaryValue), + } + + } else if k == stringAttribute { + attr2.Name = k + attr2.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + } + } else if k == numberAttribute { + attr3.Name = k + attr3.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + } + } + } + assert.Equal(t, binaryAttribute, attr1.Name) + assert.Equal(t, binaryType, attr1.Value.DataType) + assert.Equal(t, "YmFzZTY0LWVuY29kZWQtdmFsdWU=", attr1.Value.BinaryValue) // base64 encoded value + + assert.Equal(t, stringAttribute, attr2.Name) + assert.Equal(t, stringType, attr2.Value.DataType) + assert.Equal(t, stringValue, attr2.Value.StringValue) + + assert.Equal(t, numberAttribute, attr3.Name) + assert.Equal(t, numberType, attr3.Value.DataType) + assert.Equal(t, numberValue, attr3.Value.StringValue) +} + +func TestSendMessageBatchV1_Xml_Success(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + e := httpexpect.Default(t, server.URL) + + messageId1 := "test_msg_001" + messageId2 := "test_msg_002" + + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + + binaryAttribute := "binary" + stringAttribute := "string" + numberAttribute := "number" + + binaryType := "Binary" + stringType := "String" + numberType := "Number" + + binaryValue := "YmFzZTY0LWVuY29kZWQtdmFsdWU=" + stringValue := "hogeValue" + numberValue := "100" + + // Target test: send a message + sendMessageBatchXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + }{ + Action: "SendMessageBatch", + Version: "2012-11-05", + QueueUrl: af.QueueUrl, + } + + r := e.POST("/").WithForm(sendMessageBatchXML). + WithFormField("Entries.0.Id", messageId1). + WithFormField("Entries.0.MessageBody", messageBody1). + WithFormField("Entries.1.Id", messageId2). + WithFormField("Entries.1.MessageBody", messageBody2). + WithFormField("Entries.1.MessageAttributes.1.Name", binaryAttribute). + WithFormField("Entries.1.MessageAttributes.1.Value.DataType", binaryType). + WithFormField("Entries.1.MessageAttributes.1.Value.BinaryValue", binaryValue). + WithFormField("Entries.1.MessageAttributes.2.Name", stringAttribute). + WithFormField("Entries.1.MessageAttributes.2.Value.DataType", stringType). + WithFormField("Entries.1.MessageAttributes.2.Value.StringValue", stringValue). + WithFormField("Entries.1.MessageAttributes.3.Name", numberAttribute). + WithFormField("Entries.1.MessageAttributes.3.Value.DataType", numberType). + WithFormField("Entries.1.MessageAttributes.3.Value.StringValue", numberValue). + Expect(). + Status(http.StatusOK). + Body().Raw() + + response := models.SendMessageBatchResponse{} + + xml.Unmarshal([]byte(r), &response) + + assert.NotNil(t, response.Result.Entry[0].MessageId) + + // Assert 1 message in the queue + getQueueAttributeOutput, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: &af.QueueUrl, + }) + assert.Equal(t, "2", getQueueAttributeOutput.Attributes["ApproximateNumberOfMessages"]) + + receiveMessageOutput, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: &af.QueueUrl, + MaxNumberOfMessages: 10, + }) + + receivedMessage1 := receiveMessageOutput.Messages[0] + receivedMessage2 := receiveMessageOutput.Messages[1] + + assert.Equal(t, messageBody1, string(*receivedMessage1.Body)) + assert.Equal(t, messageBody2, string(*receivedMessage2.Body)) + assert.Equal(t, 3, len(receivedMessage2.MessageAttributes)) + + var attr1, attr2, attr3 models.ResultMessageAttribute + for k, attr := range receivedMessage2.MessageAttributes { + if k == binaryAttribute { + attr1.Name = k + attr1.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + BinaryValue: string(attr.BinaryValue), + } + + } else if k == stringAttribute { + attr2.Name = k + attr2.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + } + } else if k == numberAttribute { + attr3.Name = k + attr3.Value = &models.ResultMessageAttributeValue{ + DataType: *attr.DataType, + StringValue: *attr.StringValue, + } + } + } + assert.Equal(t, binaryAttribute, attr1.Name) + assert.Equal(t, binaryType, attr1.Value.DataType) + assert.Equal(t, "YmFzZTY0LWVuY29kZWQtdmFsdWU=", attr1.Value.BinaryValue) // base64 encoded value + + assert.Equal(t, stringAttribute, attr2.Name) + assert.Equal(t, stringType, attr2.Value.DataType) + assert.Equal(t, stringValue, attr2.Value.StringValue) + + assert.Equal(t, numberAttribute, attr3.Name) + assert.Equal(t, numberType, attr3.Value.DataType) + assert.Equal(t, numberValue, attr3.Value.StringValue) + +} From 5a198085c0b2b82d7e6c88d4a2be4924cda68f4d Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Tue, 9 Jul 2024 10:00:17 +0900 Subject: [PATCH 32/41] added Delete Message Batch V1 added ut fix review ref review ref resolve conflicts rm sqs_messages.go --- app/gosqs/delete_message_batch.go | 121 ++++ app/gosqs/delete_message_batch_test.go | 330 +++++++++ app/gosqs/gosqs.go | 97 --- app/models/responses.go | 33 +- app/models/sqs.go | 16 + app/router/router.go | 4 +- app/router/router_test.go | 4 +- app/sqs_messages.go | 24 - smoke_tests/sqs_delete_message_batch_test.go | 696 +++++++++++++++++++ 9 files changed, 1197 insertions(+), 128 deletions(-) create mode 100644 app/gosqs/delete_message_batch.go create mode 100644 app/gosqs/delete_message_batch_test.go delete mode 100644 app/sqs_messages.go create mode 100644 smoke_tests/sqs_delete_message_batch_test.go diff --git a/app/gosqs/delete_message_batch.go b/app/gosqs/delete_message_batch.go new file mode 100644 index 00000000..f709be23 --- /dev/null +++ b/app/gosqs/delete_message_batch.go @@ -0,0 +1,121 @@ +package gosqs + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + + requestBody := models.NewDeleteMessageBatchRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + + if !ok { + log.Error("Invalid Request - DeleteMessageBatchV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", true) + } + + queueUrl := requestBody.QueueUrl + + queueName := "" + if queueUrl == "" { + vars := mux.Vars(req) + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(queueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } + + if _, ok := app.SyncQueues.Queues[queueName]; !ok { + return utils.CreateErrorResponseV1("QueueNotFound", true) + } + + if len(requestBody.Entries) == 0 { + return utils.CreateErrorResponseV1("EmptyBatchRequest", true) + } + + if len(requestBody.Entries) > 10 { + return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", true) + } + + ids := map[string]struct{}{} + for _, v := range requestBody.Entries { + if _, ok := ids[v.Id]; ok { + return utils.CreateErrorResponseV1("BatchEntryIdsNotDistinct", true) + } + ids[v.Id] = struct{}{} + } + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + + // create deleteMessageMap + deleteMessageMap := make(map[string]*deleteEntry) + for _, entry := range requestBody.Entries { + deleteMessageMap[entry.ReceiptHandle] = &deleteEntry{ + Id: entry.Id, + ReceiptHandle: entry.ReceiptHandle, + Deleted: false, + } + } + + deletedEntries := make([]models.DeleteMessageBatchResultEntry, 0) + // create a slice to hold messages that are not deleted + remainingMessages := make([]app.Message, 0, len(app.SyncQueues.Queues[queueName].Messages)) + + // delete message from queue + for _, message := range app.SyncQueues.Queues[queueName].Messages { + if deleteEntry, found := deleteMessageMap[message.ReceiptHandle]; found { + // Unlock messages for the group + log.Debugf("FIFO Queue %s unlocking group %s:", queueName, message.GroupID) + app.SyncQueues.Queues[queueName].UnlockGroup(message.GroupID) + delete(app.SyncQueues.Queues[queueName].Duplicates, message.DeduplicationID) + deleteEntry.Deleted = true + deletedEntries = append(deletedEntries, models.DeleteMessageBatchResultEntry{Id: deleteEntry.Id}) + } else { + remainingMessages = append(remainingMessages, message) + } + } + + // Update the queue with the remaining mesages + app.SyncQueues.Queues[queueName].Messages = remainingMessages + + // Process not found entries + notFoundEntries := make([]models.BatchResultErrorEntry, 0) + for _, deleteEntry := range deleteMessageMap { + if !deleteEntry.Deleted { + notFoundEntries = append(notFoundEntries, models.BatchResultErrorEntry{ + Code: "1", + Id: deleteEntry.Id, + Message: "Message not found", + SenderFault: true, + }) + } + } + + respStruct := models.DeleteMessageBatchResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.DeleteMessageBatchResult{ + Successful: deletedEntries, + Failed: notFoundEntries, + }, + Metadata: models.BASE_RESPONSE_METADATA, + } + + return http.StatusOK, respStruct + +} + +type deleteEntry struct { + Id string + ReceiptHandle string + Error string + Deleted bool +} diff --git a/app/gosqs/delete_message_batch_test.go b/app/gosqs/delete_message_batch_test.go new file mode 100644 index 00000000..b7abd961 --- /dev/null +++ b/app/gosqs/delete_message_batch_test.go @@ -0,0 +1,330 @@ +package gosqs + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/fixtures" + "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 TestDeleteMessageBatchV1_success_all_message(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + q := &app.Queue{ + Name: "testing", + Messages: []app.Message{ + { + MessageBody: []byte("test%20message%20body%201"), + ReceiptHandle: "test1", + }, + { + MessageBody: []byte("test%20message%20body%202"), + ReceiptHandle: "test2", + }, + { + MessageBody: []byte("test%20message%20body%203"), + ReceiptHandle: "test3", + }, + }, + } + app.SyncQueues.Queues["testing"] = q + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.DeleteMessageBatchRequest) + *v = models.DeleteMessageBatchRequest{ + Entries: []models.DeleteMessageBatchRequestEntry{ + { + Id: "delete-test-1", + ReceiptHandle: "test1", + }, + { + Id: "delete-test-2", + ReceiptHandle: "test2", + }, + { + Id: "delete-test-3", + ReceiptHandle: "test3", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "testing"), + } + return true + } + _, request2 := test.GenerateRequestInfo( + "POST", + "/", + nil, + true) + + status, response2 := DeleteMessageBatchV1(request2) + deleteMessageBatchResponse := response2.(models.DeleteMessageBatchResponse) + assert.Equal(t, status, http.StatusOK) + assert.Equal(t, "delete-test-1", deleteMessageBatchResponse.Result.Successful[0].Id) + assert.Equal(t, "delete-test-2", deleteMessageBatchResponse.Result.Successful[1].Id) + assert.Equal(t, "delete-test-3", deleteMessageBatchResponse.Result.Successful[2].Id) + assert.Empty(t, deleteMessageBatchResponse.Result.Failed) + assert.Empty(t, app.SyncQueues.Queues["testing"].Messages) +} +func TestDeleteMessageBatchV1_success_not_found_message(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + q := &app.Queue{ + Name: "testing", + Messages: []app.Message{ + { + MessageBody: []byte("test%20message%20body%201"), + ReceiptHandle: "test1", + }, + { + MessageBody: []byte("test%20message%20body%203"), + ReceiptHandle: "test3", + }, + }, + } + app.SyncQueues.Queues["testing"] = q + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.DeleteMessageBatchRequest) + *v = models.DeleteMessageBatchRequest{ + Entries: []models.DeleteMessageBatchRequestEntry{ + { + Id: "delete-test-1", + ReceiptHandle: "test1", + }, + { + Id: "delete-test-2", + ReceiptHandle: "test2", + }, + { + Id: "delete-test-3", + ReceiptHandle: "test3", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "testing"), + } + return true + } + _, request := test.GenerateRequestInfo( + "POST", + "/", + nil, + true) + + status, response := DeleteMessageBatchV1(request) + deleteMessageBatchResponse := response.(models.DeleteMessageBatchResponse) + assert.Equal(t, status, http.StatusOK) + assert.Equal(t, "delete-test-1", deleteMessageBatchResponse.Result.Successful[0].Id) + assert.Equal(t, "delete-test-3", deleteMessageBatchResponse.Result.Successful[1].Id) + assert.Equal(t, "1", deleteMessageBatchResponse.Result.Failed[0].Code) + assert.Equal(t, "delete-test-2", deleteMessageBatchResponse.Result.Failed[0].Id) + assert.Equal(t, "Message not found", deleteMessageBatchResponse.Result.Failed[0].Message) + assert.True(t, deleteMessageBatchResponse.Result.Failed[0].SenderFault) + assert.Empty(t, app.SyncQueues.Queues["testing"].Messages) +} + +func TestDeleteMessageBatchV1_error_not_found_queue(t *testing.T) { + app.CurrentEnvironment = fixtures.LOCAL_ENVIRONMENT + + 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.DeleteMessageBatchRequest) + *v = models.DeleteMessageBatchRequest{ + Entries: []models.DeleteMessageBatchRequestEntry{ + { + Id: "delete-test-1", + ReceiptHandle: "test1", + }, + { + Id: "delete-test-2", + ReceiptHandle: "test2", + }, + { + Id: "delete-test-3", + ReceiptHandle: "test3", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "not-exist-queue"), + } + return true + } + _, r := test.GenerateRequestInfo( + "POST", + "/", + nil, + true) + + status, _ := DeleteMessageBatchV1(r) + assert.Equal(t, status, http.StatusBadRequest) + +} + +func TestDeleteMessageBatchV1_error_no_entry(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.DeleteMessageBatchRequest) + *v = models.DeleteMessageBatchRequest{ + Entries: make([]models.DeleteMessageBatchRequestEntry, 0), + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + return true + } + _, r := test.GenerateRequestInfo( + "POST", + "/", + nil, + true) + + status, _ := DeleteMessageBatchV1(r) + assert.Equal(t, status, http.StatusBadRequest) +} + +func TestDeleteMessageBatchV1_error_too_many_entries(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.DeleteMessageBatchRequest) + *v = models.DeleteMessageBatchRequest{ + Entries: []models.DeleteMessageBatchRequestEntry{ + { + Id: "test-1", + ReceiptHandle: "test-1", + }, + { + Id: "test-2", + ReceiptHandle: "test-2", + }, + { + Id: "test-3", + ReceiptHandle: "test-3", + }, + { + Id: "test-4", + ReceiptHandle: "test-4", + }, + { + Id: "test-5", + ReceiptHandle: "test-5", + }, + { + Id: "test-6", + ReceiptHandle: "test-6", + }, + { + Id: "test-7", + ReceiptHandle: "test-7", + }, + { + Id: "test-8", + ReceiptHandle: "test-8", + }, + { + Id: "test-9", + ReceiptHandle: "test-9", + }, + { + Id: "test-10", + ReceiptHandle: "test-10", + }, + { + Id: "test-11", + ReceiptHandle: "test-11", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + return true + } + _, r := test.GenerateRequestInfo( + "POST", + "/", + nil, + true) + + status, _ := DeleteMessageBatchV1(r) + assert.Equal(t, status, http.StatusBadRequest) +} + +func TestDeleteMessageBatchV1_Error_IdNotDistinct(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.DeleteMessageBatchRequest) + *v = models.DeleteMessageBatchRequest{ + Entries: []models.DeleteMessageBatchRequestEntry{ + { + Id: "delete-test-1", + ReceiptHandle: "test1", + }, + { + Id: "delete-test-1", + ReceiptHandle: "test2", + }, + }, + QueueUrl: fmt.Sprintf("%s/%s", fixtures.BASE_URL, "unit-queue1"), + } + return true + } + _, r := test.GenerateRequestInfo( + "POST", + "/", + nil, + true) + + status, _ := DeleteMessageBatchV1(r) + assert.Equal(t, http.StatusBadRequest, status) +} + +func TestDeleteMessageBatchV1_Error_transformer(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) + status, _ := DeleteMessageBatchV1(r) + + assert.Equal(t, http.StatusBadRequest, status) + +} diff --git a/app/gosqs/gosqs.go b/app/gosqs/gosqs.go index 76a3655b..e25381cd 100644 --- a/app/gosqs/gosqs.go +++ b/app/gosqs/gosqs.go @@ -4,8 +4,6 @@ import ( "encoding/xml" "net/http" "net/url" - "strconv" - "strings" "time" "github.com/Admiral-Piett/goaws/app/models" @@ -13,7 +11,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/Admiral-Piett/goaws/app" - "github.com/gorilla/mux" ) func init() { @@ -77,100 +74,6 @@ func numberOfHiddenMessagesInQueue(queue app.Queue) int { return num } -type DeleteEntry struct { - Id string - ReceiptHandle string - Error string - Deleted bool -} - -func DeleteMessageBatch(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/xml") - req.ParseForm() - - queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) - queueName := "" - if queueUrl == "" { - vars := mux.Vars(req) - queueName = vars["queueName"] - } else { - uriSegments := strings.Split(queueUrl, "/") - queueName = uriSegments[len(uriSegments)-1] - } - - deleteEntries := []DeleteEntry{} - - for k, v := range req.Form { - keySegments := strings.Split(k, ".") - if keySegments[0] == "DeleteMessageBatchRequestEntry" { - keyIndex, err := strconv.Atoi(keySegments[1]) - if err != nil { - createErrorResponse(w, req, "Error") - return - } - - if len(deleteEntries) < keyIndex { - newDeleteEntries := make([]DeleteEntry, keyIndex) - copy(newDeleteEntries, deleteEntries) - deleteEntries = newDeleteEntries - } - - if keySegments[2] == "Id" { - deleteEntries[keyIndex-1].Id = v[0] - } - - if keySegments[2] == "ReceiptHandle" { - deleteEntries[keyIndex-1].ReceiptHandle = v[0] - } - } - } - - deletedEntries := make([]app.DeleteMessageBatchResultEntry, 0) - - app.SyncQueues.Lock() - if _, ok := app.SyncQueues.Queues[queueName]; ok { - for _, deleteEntry := range deleteEntries { - for i, msg := range app.SyncQueues.Queues[queueName].Messages { - if msg.ReceiptHandle == deleteEntry.ReceiptHandle { - // Unlock messages for the group - log.Printf("FIFO Queue %s unlocking group %s:", queueName, msg.GroupID) - app.SyncQueues.Queues[queueName].UnlockGroup(msg.GroupID) - app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages[:i], app.SyncQueues.Queues[queueName].Messages[i+1:]...) - delete(app.SyncQueues.Queues[queueName].Duplicates, msg.DeduplicationID) - - deleteEntry.Deleted = true - deletedEntry := app.DeleteMessageBatchResultEntry{Id: deleteEntry.Id} - deletedEntries = append(deletedEntries, deletedEntry) - break - } - } - } - } - app.SyncQueues.Unlock() - - notFoundEntries := make([]app.BatchResultErrorEntry, 0) - for _, deleteEntry := range deleteEntries { - if deleteEntry.Deleted { - notFoundEntries = append(notFoundEntries, app.BatchResultErrorEntry{ - Code: "1", - Id: deleteEntry.Id, - Message: "Message not found", - SenderFault: true}) - } - } - - respStruct := app.DeleteMessageBatchResponse{ - "http://queue.amazonaws.com/doc/2012-11-05/", - app.DeleteMessageBatchResult{Entry: deletedEntries, Error: notFoundEntries}, - app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} - - enc := xml.NewEncoder(w) - enc.Indent(" ", " ") - if err := enc.Encode(respStruct); err != nil { - log.Printf("error: %v\n", err) - } -} - func getQueueFromPath(formVal string, theUrl string) string { if formVal != "" { return formVal diff --git a/app/models/responses.go b/app/models/responses.go index f2bdf7f6..831201fc 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -260,7 +260,7 @@ type SendMessageBatchResponse struct { type SendMessageBatchResult struct { Entry []SendMessageBatchResultEntry `xml:"SendMessageBatchResultEntry"` - Error []app.BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` + Error []BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` } func (r SendMessageBatchResponse) GetResult() interface{} { @@ -271,6 +271,13 @@ func (r SendMessageBatchResponse) GetRequestId() string { return r.Metadata.RequestId } +type BatchResultErrorEntry struct { + Code string `xml:"Code"` + Id string `xml:"Id"` + Message string `xml:"Message,omitempty"` + SenderFault bool `xml:"SenderFault"` +} + type SetQueueAttributesResponse struct { Xmlns string `xml:"xmlns,attr,omitempty"` Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` @@ -370,3 +377,27 @@ func (r UnsubscribeResponse) GetResult() interface{} { func (r UnsubscribeResponse) GetRequestId() string { return r.Metadata.RequestId } + +type DeleteMessageBatchResultEntry struct { + Id string `xml:"Id"` +} + +type DeleteMessageBatchResult struct { + Successful []DeleteMessageBatchResultEntry `xml:"DeleteMessageBatchResultEntry"` + Failed []BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` +} + +/*** Delete Message Batch Response */ +type DeleteMessageBatchResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Result DeleteMessageBatchResult `xml:"DeleteMessageBatchResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r DeleteMessageBatchResponse) GetResult() interface{} { + return r.Result +} + +func (r DeleteMessageBatchResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sqs.go b/app/models/sqs.go index 1467c6da..7d18b323 100644 --- a/app/models/sqs.go +++ b/app/models/sqs.go @@ -526,3 +526,19 @@ type DeleteQueueRequest struct { } func (r *DeleteQueueRequest) SetAttributesFromForm(values url.Values) {} + +type DeleteMessageBatchRequestEntry struct { + Id string `json:"Id" schema:"Id"` + ReceiptHandle string `json:"ReceiptHandle" schema:"ReceiptHandle"` +} + +type DeleteMessageBatchRequest struct { + Entries []DeleteMessageBatchRequestEntry `json:"Entries" schema:"Entries"` + QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` +} + +func NewDeleteMessageBatchRequest() *DeleteMessageBatchRequest { + return &DeleteMessageBatchRequest{} +} + +func (r *DeleteMessageBatchRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index 1c6bfc40..d79effb8 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -76,6 +76,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, "SendMessageBatch": sqs.SendMessageBatchV1, + "DeleteMessageBatch": sqs.DeleteMessageBatchV1, // SNS "CreateTopic": sns.CreateTopicV1, @@ -84,9 +85,6 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR } var routingTable = map[string]http.HandlerFunc{ - // SQS - "DeleteMessageBatch": sqs.DeleteMessageBatch, - // SNS "ListTopics": sns.ListTopics, "DeleteTopic": sns.DeleteTopic, diff --git a/app/router/router_test.go b/app/router/router_test.go index ea637d45..07b275ee 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -268,6 +268,7 @@ func TestActionHandler_v0_xml(t *testing.T) { "PurgeQueue": sqs.PurgeQueueV1, "DeleteQueue": sqs.DeleteQueueV1, "SendMessageBatch": sqs.SendMessageBatchV1, + "DeleteMessageBatch": sqs.DeleteMessageBatchV1, // SNS "CreateTopic": sns.CreateTopicV1, @@ -276,9 +277,6 @@ func TestActionHandler_v0_xml(t *testing.T) { } routingTable = map[string]http.HandlerFunc{ - // SQS - "DeleteMessageBatch": sqs.DeleteMessageBatch, - // SNS "ListTopics": sns.ListTopics, "DeleteTopic": sns.DeleteTopic, diff --git a/app/sqs_messages.go b/app/sqs_messages.go deleted file mode 100644 index ae08a6d4..00000000 --- a/app/sqs_messages.go +++ /dev/null @@ -1,24 +0,0 @@ -package app - -type DeleteMessageBatchResultEntry struct { - Id string `xml:"Id"` -} - -type BatchResultErrorEntry struct { - Code string `xml:"Code"` - Id string `xml:"Id"` - Message string `xml:"Message,omitempty"` - SenderFault bool `xml:"SenderFault"` -} - -type DeleteMessageBatchResult struct { - Entry []DeleteMessageBatchResultEntry `xml:"DeleteMessageBatchResultEntry"` - Error []BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` -} - -/*** Delete Message Batch Response */ -type DeleteMessageBatchResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Result DeleteMessageBatchResult `xml:"DeleteMessageBatchResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} diff --git a/smoke_tests/sqs_delete_message_batch_test.go b/smoke_tests/sqs_delete_message_batch_test.go new file mode 100644 index 00000000..004fe65b --- /dev/null +++ b/smoke_tests/sqs_delete_message_batch_test.go @@ -0,0 +1,696 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "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/sqs" + "github.com/aws/aws-sdk-go-v2/service/sqs/types" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_DeleteMessageBatchV1_json_error_queue_not_found(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + queueUrl := fmt.Sprintf("%s/%s", af.BASE_URL, "testing") + + testId1 := "test1" + testId2 := "test2" + testId3 := "test3" + + receiptHandle1 := "delete-test-1" + receiptHandle2 := "delete-test-2" + receiptHandle3 := "delete-test-3" + + _, error := sqsClient.DeleteMessageBatch(context.TODO(), &sqs.DeleteMessageBatchInput{ + Entries: []types.DeleteMessageBatchRequestEntry{ + { + Id: &testId1, + ReceiptHandle: &receiptHandle1, + }, + { + Id: &testId2, + ReceiptHandle: &receiptHandle2, + }, + { + Id: &testId3, + ReceiptHandle: &receiptHandle3, + }, + }, + QueueUrl: &queueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.NonExistentQueue") + assert.Contains(t, error.Error(), "The specified queue does not exist for this wsdl version.") +} + +func Test_DeleteMessageBatchV1_json_error_no_entry(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + crateQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + _, error := sqsClient.DeleteMessageBatch(context.TODO(), &sqs.DeleteMessageBatchInput{ + Entries: make([]types.DeleteMessageBatchRequestEntry, 0), + QueueUrl: crateQueueResponse.QueueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.EmptyBatchRequest") + assert.Contains(t, error.Error(), "The batch request doesn't contain any entries.") +} + +func Test_DeleteMessageBatchV1_json_error_too_many_entry(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + crateQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + testId1 := "test1" + testId2 := "test2" + testId3 := "test3" + testId4 := "test4" + testId5 := "test5" + testId6 := "test6" + testId7 := "test7" + testId8 := "test8" + testId9 := "test9" + testId10 := "test10" + testId11 := "test11" + + receiptHandle1 := "delete-test-1" + receiptHandle2 := "delete-test-2" + receiptHandle3 := "delete-test-3" + receiptHandle4 := "delete-test-4" + receiptHandle5 := "delete-test-5" + receiptHandle6 := "delete-test-6" + receiptHandle7 := "delete-test-7" + receiptHandle8 := "delete-test-8" + receiptHandle9 := "delete-test-9" + receiptHandle10 := "delete-test-10" + receiptHandle11 := "delete-test-11" + + _, error := sqsClient.DeleteMessageBatch(context.TODO(), &sqs.DeleteMessageBatchInput{ + Entries: []types.DeleteMessageBatchRequestEntry{ + { + Id: &testId1, + ReceiptHandle: &receiptHandle1, + }, + { + Id: &testId2, + ReceiptHandle: &receiptHandle2, + }, + { + Id: &testId3, + ReceiptHandle: &receiptHandle3, + }, + { + Id: &testId4, + ReceiptHandle: &receiptHandle4, + }, + { + Id: &testId5, + ReceiptHandle: &receiptHandle5, + }, + { + Id: &testId6, + ReceiptHandle: &receiptHandle6, + }, + { + Id: &testId7, + ReceiptHandle: &receiptHandle7, + }, + { + Id: &testId8, + ReceiptHandle: &receiptHandle8, + }, + { + Id: &testId9, + ReceiptHandle: &receiptHandle9, + }, + { + Id: &testId10, + ReceiptHandle: &receiptHandle10, + }, + { + Id: &testId11, + ReceiptHandle: &receiptHandle11, + }, + }, + QueueUrl: crateQueueResponse.QueueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.TooManyEntriesInBatchRequest") + assert.Contains(t, error.Error(), "Maximum number of entries per request are 10.") +} + +func Test_DeleteMessageBatchV1_json_error_batch_entry_ids_not_distinct(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + crateQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + testId1 := "test1" + + receiptHandle1 := "delete-test-1" + receiptHandle2 := "delete-test-2" + receiptHandle3 := "delete-test-3" + + _, error := sqsClient.DeleteMessageBatch(context.TODO(), &sqs.DeleteMessageBatchInput{ + Entries: []types.DeleteMessageBatchRequestEntry{ + { + Id: &testId1, + ReceiptHandle: &receiptHandle1, + }, + { + Id: &testId1, + ReceiptHandle: &receiptHandle2, + }, + { + Id: &testId1, + ReceiptHandle: &receiptHandle3, + }, + }, + QueueUrl: crateQueueResponse.QueueUrl, + }) + + assert.Contains(t, error.Error(), "400") + assert.Contains(t, error.Error(), "AWS.SimpleQueueService.BatchEntryIdsNotDistinct") + assert.Contains(t, error.Error(), "Two or more batch entries in the request have the same Id.") +} + +func Test_DeleteMessageBatchV1_json_success_all_delete(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // create queue + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + testId1 := "test1" + testId2 := "test2" + testId3 := "test3" + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + messageBody3 := "test%20message%20body%203" + + // send messages + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + MessageBody: &messageBody1, + QueueUrl: createQueueResponse.QueueUrl, + }) + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + MessageBody: &messageBody2, + QueueUrl: createQueueResponse.QueueUrl, + }) + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + MessageBody: &messageBody3, + QueueUrl: createQueueResponse.QueueUrl, + }) + + receiveMessageOutput, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + MaxNumberOfMessages: 10, + }) + + // delete messages + deleteMessageBatchOutput, error := sqsClient.DeleteMessageBatch(context.TODO(), &sqs.DeleteMessageBatchInput{ + Entries: []types.DeleteMessageBatchRequestEntry{ + { + Id: &testId1, + ReceiptHandle: receiveMessageOutput.Messages[0].ReceiptHandle, + }, + { + Id: &testId2, + ReceiptHandle: receiveMessageOutput.Messages[1].ReceiptHandle, + }, + { + Id: &testId3, + ReceiptHandle: receiveMessageOutput.Messages[2].ReceiptHandle, + }, + }, + QueueUrl: createQueueResponse.QueueUrl, + }) + + // received no message + receiveMessageOutput2, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + MaxNumberOfMessages: 10, + }) + + assert.Nil(t, error) + assert.Empty(t, deleteMessageBatchOutput.Failed) + assert.Equal(t, &testId1, deleteMessageBatchOutput.Successful[0].Id) + assert.Equal(t, &testId2, deleteMessageBatchOutput.Successful[1].Id) + assert.Equal(t, &testId3, deleteMessageBatchOutput.Successful[2].Id) + assert.Empty(t, receiveMessageOutput2.Messages) +} + +func Test_DeleteMessageBatchV1_json_success_not_found_message(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // create queue + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + dummyQueue := "dummy" + // create dummy queue + createQueueResponse2, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &dummyQueue, + }) + + testId1 := "test1" + testId2 := "test2" + testId3 := "test3" + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + messageBody3 := "test%20message%20body%203" + + // send messages + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + MessageBody: &messageBody1, + QueueUrl: createQueueResponse.QueueUrl, + }) + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + MessageBody: &messageBody2, + QueueUrl: createQueueResponse2.QueueUrl, + }) + sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{ + MessageBody: &messageBody3, + QueueUrl: createQueueResponse.QueueUrl, + }) + + receiveMessageOutput, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + MaxNumberOfMessages: 10, + }) + + receiveMessageOutput2, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse2.QueueUrl, + MaxNumberOfMessages: 10, + }) + + // delete messages + deleteMessageBatchOutput, error := sqsClient.DeleteMessageBatch(context.TODO(), &sqs.DeleteMessageBatchInput{ + Entries: []types.DeleteMessageBatchRequestEntry{ + { + Id: &testId1, + ReceiptHandle: receiveMessageOutput.Messages[0].ReceiptHandle, + }, + { + Id: &testId2, + ReceiptHandle: receiveMessageOutput2.Messages[0].ReceiptHandle, + }, + { + Id: &testId3, + ReceiptHandle: receiveMessageOutput.Messages[1].ReceiptHandle, + }, + }, + QueueUrl: createQueueResponse.QueueUrl, + }) + + // received no message + receiveMessageOutput3, _ := sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{ + QueueUrl: createQueueResponse.QueueUrl, + MaxNumberOfMessages: 10, + }) + + // not error + assert.Nil(t, error) + + // deleted messages + assert.Equal(t, &testId1, deleteMessageBatchOutput.Successful[0].Id) + assert.Equal(t, &testId3, deleteMessageBatchOutput.Successful[1].Id) + + failedMessage := "Message not found" + // not founded message + assert.Equal(t, &testId2, deleteMessageBatchOutput.Failed[0].Id) + assert.Equal(t, &failedMessage, deleteMessageBatchOutput.Failed[0].Message) + + // confirm no message + assert.Empty(t, receiveMessageOutput3.Messages) + +} + +func Test_DeleteMessageBatchV1_xml_success_not_found_message(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // create queue + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + dummyQueue := "dummy" + // create dummy queue + createQueueResponse2, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &dummyQueue, + }) + + testId1 := "test1" + testId2 := "test2" + testId3 := "test3" + + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + messageBody3 := "test%20message%20body%203" + + type SendMessageRequestBodyXML struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + MessageBody string `xml:"MessageBody"` + } + + sendMessageRequest1 := SendMessageRequestBodyXML{ + Action: "SendMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MessageBody: messageBody1, + } + sendMessageRequest2 := SendMessageRequestBodyXML{ + Action: "SendMessage", + QueueUrl: *createQueueResponse2.QueueUrl, + Version: "2012-11-05", + MessageBody: messageBody2, + } + sendMessageRequest3 := SendMessageRequestBodyXML{ + Action: "SendMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MessageBody: messageBody3, + } + + // send messages + e := httpexpect.Default(t, server.URL) + e.POST("/"). + WithForm(sendMessageRequest1). + Expect(). + Status(http.StatusOK). + Body().Raw() + + e.POST("/"). + WithForm(sendMessageRequest2). + Expect(). + Status(http.StatusOK). + Body().Raw() + + e.POST("/"). + WithForm(sendMessageRequest3). + Expect(). + Status(http.StatusOK). + Body().Raw() + + type ReceiveMessageRequestBodyXML struct { + Action string `xml:"Action"` + QueueUrl string `xml:"QueueUrl"` + Version string `xml:"Version"` + MaxNumberOfMessages string `xml:"MaxNumberOfMessages"` + } + + receiveMessageRequestBodyXML1 := ReceiveMessageRequestBodyXML{ + Action: "ReceiveMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MaxNumberOfMessages: "10", + } + + receiveMessageRequestBodyXML2 := ReceiveMessageRequestBodyXML{ + Action: "ReceiveMessage", + QueueUrl: *createQueueResponse2.QueueUrl, + Version: "2012-11-05", + MaxNumberOfMessages: "10", + } + + // received messages + receivedMessages1 := e.POST("/"). + WithForm(receiveMessageRequestBodyXML1). + Expect(). + Status(http.StatusOK). + Body().Raw() + receivedMessageResponse1 := models.ReceiveMessageResponse{} + xml.Unmarshal([]byte(receivedMessages1), &receivedMessageResponse1) + + // received messages + receivedMessages2 := e.POST("/"). + WithForm(receiveMessageRequestBodyXML2). + Expect(). + Status(http.StatusOK). + Body().Raw() + receivedMessageResponse2 := models.ReceiveMessageResponse{} + xml.Unmarshal([]byte(receivedMessages2), &receivedMessageResponse2) + + deleteMessageBatchRequestBodyXML := struct { + Action string `xml:"Action"` + QueueUrl string `xml:"QueueUrl"` + Version string `xml:"Version"` + }{ + Action: "DeleteMessageBatch", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + } + + // delete messages + deletedMessages := e.POST("/"). + WithForm(deleteMessageBatchRequestBodyXML). + WithFormField("Entries.0.Id", testId1). + WithFormField("Entries.0.ReceiptHandle", receivedMessageResponse1.Result.Messages[0].ReceiptHandle). + WithFormField("Entries.1.Id", testId2). + WithFormField("Entries.1.ReceiptHandle", receivedMessageResponse2.Result.Messages[0].ReceiptHandle). + WithFormField("Entries.2.Id", testId3). + WithFormField("Entries.2.ReceiptHandle", receivedMessageResponse1.Result.Messages[1].ReceiptHandle). + Expect(). + Status(http.StatusOK). + Body().Raw() + + deleteMessageBatchResponse := models.DeleteMessageBatchResponse{} + xml.Unmarshal([]byte(deletedMessages), &deleteMessageBatchResponse) + + // confirm no message + receivedMessages3 := e.POST("/"). + WithForm(receiveMessageRequestBodyXML1). + Expect(). + Status(http.StatusOK). + Body().Raw() + receivedMessageResponse3 := models.ReceiveMessageResponse{} + xml.Unmarshal([]byte(receivedMessages3), &receivedMessageResponse3) + + // success: delete messages + assert.Contains(t, deleteMessageBatchResponse.Result.Successful[0].Id, testId1) + assert.Contains(t, deleteMessageBatchResponse.Result.Successful[1].Id, testId3) + + failedMessage := "Message not found" + // not founded message + assert.NotEmpty(t, deleteMessageBatchResponse.Result.Failed) + assert.Contains(t, deleteMessageBatchResponse.Result.Failed[0].Id, testId2) + assert.Contains(t, deleteMessageBatchResponse.Result.Failed[0].Message, failedMessage) + + // confirm no message + assert.Empty(t, receivedMessageResponse3.Result.Messages) +} + +func Test_DeleteMessageBatchV1_xml_success_all_deletes(t *testing.T) { + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // create queue + createQueueResponse, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + testId1 := "test1" + testId2 := "test2" + testId3 := "test3" + + messageBody1 := "test%20message%20body%201" + messageBody2 := "test%20message%20body%202" + messageBody3 := "test%20message%20body%203" + + type SendMessageRequestBodyXML struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + QueueUrl string `xml:"QueueUrl"` + MessageBody string `xml:"MessageBody"` + } + + sendMessageRequest1 := SendMessageRequestBodyXML{ + Action: "SendMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MessageBody: messageBody1, + } + sendMessageRequest2 := SendMessageRequestBodyXML{ + Action: "SendMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MessageBody: messageBody2, + } + sendMessageRequest3 := SendMessageRequestBodyXML{ + Action: "SendMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MessageBody: messageBody3, + } + + // send messages + e := httpexpect.Default(t, server.URL) + e.POST("/"). + WithForm(sendMessageRequest1). + Expect(). + Status(http.StatusOK). + Body().Raw() + + e.POST("/"). + WithForm(sendMessageRequest2). + Expect(). + Status(http.StatusOK). + Body().Raw() + + e.POST("/"). + WithForm(sendMessageRequest3). + Expect(). + Status(http.StatusOK). + Body().Raw() + + var ReceiveMessageRequestBodyXML = struct { + Action string `xml:"Action"` + QueueUrl string `xml:"QueueUrl"` + Version string `xml:"Version"` + MaxNumberOfMessages string `xml:"MaxNumberOfMessages"` + }{ + Action: "ReceiveMessage", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + MaxNumberOfMessages: "10", + } + + // received messages + receivedMessages := e.POST("/"). + WithForm(ReceiveMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + receivedMessageResponse := models.ReceiveMessageResponse{} + xml.Unmarshal([]byte(receivedMessages), &receivedMessageResponse) + + deleteMessageBatchRequestBodyXML := struct { + Action string `xml:"Action"` + QueueUrl string `xml:"QueueUrl"` + Version string `xml:"Version"` + }{ + Action: "DeleteMessageBatch", + QueueUrl: *createQueueResponse.QueueUrl, + Version: "2012-11-05", + } + + // delete messages + deletedMessages := e.POST("/"). + WithForm(deleteMessageBatchRequestBodyXML). + WithFormField("Entries.0.Id", testId1). + WithFormField("Entries.0.ReceiptHandle", receivedMessageResponse.Result.Messages[0].ReceiptHandle). + WithFormField("Entries.1.Id", testId2). + WithFormField("Entries.1.ReceiptHandle", receivedMessageResponse.Result.Messages[1].ReceiptHandle). + WithFormField("Entries.2.Id", testId3). + WithFormField("Entries.2.ReceiptHandle", receivedMessageResponse.Result.Messages[2].ReceiptHandle). + Expect(). + Status(http.StatusOK). + Body().Raw() + + deleteMessageBatchResponse := models.DeleteMessageBatchResponse{} + xml.Unmarshal([]byte(deletedMessages), &deleteMessageBatchResponse) + + // confirm no message + receivedMessages2 := e.POST("/"). + WithForm(ReceiveMessageRequestBodyXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + receivedMessageResponse2 := models.ReceiveMessageResponse{} + xml.Unmarshal([]byte(receivedMessages2), &receivedMessageResponse) + + // check no error + assert.Empty(t, deleteMessageBatchResponse.Result.Failed) + + // checked delete message batch resoponse + assert.Contains(t, deleteMessageBatchResponse.Result.Successful[0].Id, testId1) + assert.Contains(t, deleteMessageBatchResponse.Result.Successful[1].Id, testId2) + assert.Contains(t, deleteMessageBatchResponse.Result.Successful[2].Id, testId3) + + // confirm no message + assert.Empty(t, receivedMessageResponse2) +} From 233c2ed2e9648107a4d145f4eb2a4f989bdfc6cc Mon Sep 17 00:00:00 2001 From: Devin Humphreys Date: Mon, 15 Jul 2024 16:54:02 -0400 Subject: [PATCH 33/41] Add PublishV1 for JSON support --- .github/README.md | 2 - app/conf/mock-data/mock-config.yaml | 15 +- app/gosns/gosns.go | 202 +--------- app/gosns/gosns_create_message_test.go | 14 +- app/gosns/gosns_test.go | 235 ------------ app/gosns/publish.go | 201 ++++++++++ app/gosns/publish_test.go | 499 ++++++++++++++++++++++++ app/gosns/subscribe_test.go | 6 +- app/gosqs/send_message.go | 31 +- app/gosqs/send_message_batch.go | 2 +- app/models/models.go | 9 + app/models/responses.go | 19 + app/models/sns.go | 43 ++- app/models/sqs.go | 11 - app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/servertest/server_test.go | 173 --------- app/sns_messages.go | 12 - app/utils/utils.go | 31 ++ smoke_tests/sns_publish_test.go | 510 +++++++++++++++++++++++++ 20 files changed, 1341 insertions(+), 678 deletions(-) create mode 100644 app/gosns/publish.go create mode 100644 app/gosns/publish_test.go create mode 100644 smoke_tests/sns_publish_test.go diff --git a/.github/README.md b/.github/README.md index 76d3cec5..7b45ad07 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,8 +1,6 @@ # GoAws [![Build Status](https://travis-ci.org/p4tin/goaws.svg?branch=master)](https://travis-ci.org/p4tin/goaws) -You are always welcome to [tweet the creator in chief](https://twitter.com/gocodecloud) or [buy him a coffee](https://www.paypal.me/p4tin) - Written in Go this is a clone of the AWS SQS/SNS systems. This system is designed to emulate SQS and SNS in a local environment so developers can test their interfaces without having to connect to the AWS Cloud and possibly incurring the expense, or even worse actually write to production topics/queues by mistake. If you see any problems or would like to see a new feature, please open an issue here in github. As well, I will logon to Gitter so we can discuss your deployment issues or the weather. diff --git a/app/conf/mock-data/mock-config.yaml b/app/conf/mock-data/mock-config.yaml index 45ecd342..b8bf9ceb 100644 --- a/app/conf/mock-data/mock-config.yaml +++ b/app/conf/mock-data/mock-config.yaml @@ -59,10 +59,21 @@ BaseUnitTests: - Name: unit-queue2 RedrivePolicy: '{"maxReceiveCount": 100, "deadLetterTargetArn":"arn:aws:sqs:us-east-1:100010001000:other-queue1"}' - Name: other-queue1 - - Name: subscribed-queue2 + - Name: subscribed-queue1 + - Name: subscribed-queue3 Topics: - Name: unit-topic1 Subscriptions: - - QueueName: subscribed-queue2 + - QueueName: subscribed-queue1 Raw: true - Name: unit-topic2 + - Name: unit-topic3 + Subscriptions: + - QueueName: subscribed-queue3 + Raw: false + - Name: unit-topic-http + Subscriptions: + - Protocol: http + EndPoint: http://over.ride.me/for/tests + TopicArn: arn:aws:sqs:region:accountID:unit-topic-http + Raw: true diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 8f3ef1b5..16b0f038 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -329,137 +329,8 @@ func DeleteTopic(w http.ResponseWriter, req *http.Request) { } -// aws --endpoint-url http://localhost:47194 sns publish --topic-arn arn:aws:sns:yopa-local:000000000000:test1 --message "This is a test" -func Publish(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - topicArn := req.FormValue("TopicArn") - subject := req.FormValue("Subject") - messageBody := req.FormValue("Message") - messageStructure := req.FormValue("MessageStructure") - messageAttributes := getMessageAttributesFromRequest(req) - - arnSegments := strings.Split(topicArn, ":") - topicName := arnSegments[len(arnSegments)-1] - - _, ok := app.SyncTopics.Topics[topicName] - if ok { - log.WithFields(log.Fields{ - "topic": topicName, - "topicArn": topicArn, - "subject": subject, - }).Debug("Publish to Topic") - for _, subs := range app.SyncTopics.Topics[topicName].Subscriptions { - switch app.Protocol(subs.Protocol) { - case app.ProtocolSQS: - publishSQS(w, req, subs, messageBody, messageAttributes, subject, topicArn, topicName, messageStructure) - case app.ProtocolHTTP: - fallthrough - case app.ProtocolHTTPS: - publishHTTP(subs, messageBody, messageAttributes, subject, topicArn) - } - } - } else { - createErrorResponse(w, req, "TopicNotFound") - return - } - - //Create the response - msgId, _ := common.NewUUID() - uuid, _ := common.NewUUID() - respStruct := app.PublishResponse{Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", Result: app.PublishResult{MessageId: msgId}, Metadata: app.ResponseMetadata{RequestId: uuid}} - SendResponseBack(w, req, respStruct, content) -} - -func publishSQS(w http.ResponseWriter, req *http.Request, - subs *app.Subscription, messageBody string, messageAttributes map[string]app.MessageAttributeValue, - subject string, topicArn string, topicName string, messageStructure string) { - if subs.FilterPolicy != nil && !subs.FilterPolicy.IsSatisfiedBy(messageAttributes) { - return - } - - endPoint := subs.EndPoint - uriSegments := strings.Split(endPoint, "/") - queueName := uriSegments[len(uriSegments)-1] - arnSegments := strings.Split(queueName, ":") - queueName = arnSegments[len(arnSegments)-1] - - if _, ok := app.SyncQueues.Queues[queueName]; ok { - msg := app.Message{} - - if subs.Raw == false { - m, err := CreateMessageBody(subs, messageBody, subject, messageStructure, messageAttributes) - if err != nil { - createErrorResponse(w, req, err.Error()) - return - } - - msg.MessageBody = m - } else { - msg.MessageAttributes = messageAttributes - msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) - m, err := extractMessageFromJSON(messageBody, subs.Protocol) - if err == nil { - msg.MessageBody = []byte(m) - } else { - msg.MessageBody = []byte(messageBody) - } - } - - msg.MD5OfMessageBody = common.GetMD5Hash(messageBody) - msg.Uuid, _ = common.NewUUID() - app.SyncQueues.Lock() - app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) - app.SyncQueues.Unlock() - - log.Infof("%s: Topic: %s(%s), Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), topicName, queueName, msg.MessageBody) - } else { - log.Infof("%s: Queue %s does not exist, message discarded\n", time.Now().Format("2006-01-02 15:04:05"), queueName) - } -} - -func publishHTTP(subs *app.Subscription, messageBody string, messageAttributes map[string]app.MessageAttributeValue, - subject string, topicArn string) { - id, _ := common.NewUUID() - msg := app.SNSMessage{ - Type: "Notification", - MessageId: id, - TopicArn: topicArn, - Subject: subject, - Message: messageBody, - Timestamp: time.Now().UTC().Format(time.RFC3339), - SignatureVersion: "1", - SigningCertURL: "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/SimpleNotificationService/" + id + ".pem", - UnsubscribeURL: "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/?Action=Unsubscribe&SubscriptionArn=" + subs.SubscriptionArn, - MessageAttributes: formatAttributes(messageAttributes), - } - - signature, err := signMessage(PrivateKEY, &msg) - if err != nil { - log.Error(err) - } else { - msg.Signature = signature - } - err = callEndpoint(subs.EndPoint, subs.SubscriptionArn, msg, subs.Raw) - if err != nil { - log.WithFields(log.Fields{ - "EndPoint": subs.EndPoint, - "ARN": subs.SubscriptionArn, - "error": err.Error(), - }).Error("Error calling endpoint") - } -} - -func formatAttributes(values map[string]app.MessageAttributeValue) map[string]app.MsgAttr { - attr := make(map[string]app.MsgAttr) - for k, v := range values { - attr[k] = app.MsgAttr{ - Type: v.DataType, - Value: v.Value, - } - } - return attr -} - +// NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially +// it is a localized subscription to some non-AWS endpoint. func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { log.WithFields(log.Fields{ "sns": msg, @@ -524,75 +395,6 @@ func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) return nil } -func getMessageAttributesFromRequest(req *http.Request) map[string]app.MessageAttributeValue { - attributes := make(map[string]app.MessageAttributeValue) - - for i := 1; true; i++ { - name := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Name", i)) - if name == "" { - break - } - - dataType := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Value.DataType", i)) - if dataType == "" { - log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - continue - } - - // StringListValue and BinaryListValue is currently not implemented - for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { - value := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Value.%s", i, valueKey)) - if value != "" { - attributes[name] = app.MessageAttributeValue{Name: name, DataType: dataType, Value: value, ValueKey: valueKey} - } - } - - if _, ok := attributes[name]; !ok { - log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - } - } - - return attributes -} - -func CreateMessageBody(subs *app.Subscription, msg string, subject string, messageStructure string, - messageAttributes map[string]app.MessageAttributeValue) ([]byte, error) { - - msgId, _ := common.NewUUID() - - message := app.SNSMessage{ - Type: "Notification", - MessageId: msgId, - TopicArn: subs.TopicArn, - Subject: subject, - Timestamp: time.Now().UTC().Format(time.RFC3339), - SignatureVersion: "1", - SigningCertURL: "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/SimpleNotificationService/" + msgId + ".pem", - UnsubscribeURL: "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/?Action=Unsubscribe&SubscriptionArn=" + subs.SubscriptionArn, - MessageAttributes: formatAttributes(messageAttributes), - } - - if app.MessageStructure(messageStructure) == app.MessageStructureJSON { - m, err := extractMessageFromJSON(msg, subs.Protocol) - if err != nil { - return nil, err - } - message.Message = m - } else { - message.Message = msg - } - - signature, err := signMessage(PrivateKEY, &message) - if err != nil { - log.Error(err) - } else { - message.Signature = signature - } - - byteMsg, _ := json.Marshal(message) - return byteMsg, nil -} - func extractMessageFromJSON(msg string, protocol string) (string, error) { var msgWithProtocols map[string]string if err := json.Unmarshal([]byte(msg), &msgWithProtocols); err != nil { diff --git a/app/gosns/gosns_create_message_test.go b/app/gosns/gosns_create_message_test.go index f2080522..a859d5d0 100644 --- a/app/gosns/gosns_create_message_test.go +++ b/app/gosns/gosns_create_message_test.go @@ -15,6 +15,8 @@ const ( messageAttributesKey = "MessageAttributes" ) +// TODO - Admiral-Piett - merge these with `publish_test.go` + // When simple message string is passed, // it must be used for all subscribers (no matter the protocol) func TestCreateMessageBody_NonJson(t *testing.T) { @@ -27,7 +29,7 @@ func TestCreateMessageBody_NonJson(t *testing.T) { Raw: false, } - snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureEmpty, make(map[string]app.MessageAttributeValue)) + snsMessage, err := createMessageBody(subs, message, subject, messageStructureEmpty, make(map[string]app.MessageAttributeValue)) if err != nil { t.Fatalf(`error creating SNS message: %s`, err) } @@ -69,7 +71,7 @@ func TestCreateMessageBody_OnlyDefaultValueInJson(t *testing.T) { message := `{"default": "default message text", "http": "HTTP message text"}` subject := "subject" - snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureJSON, nil) + snsMessage, err := createMessageBody(subs, message, subject, messageStructureJSON, nil) if err != nil { t.Fatalf(`error creating SNS message: %s`, err) } @@ -112,7 +114,7 @@ func TestCreateMessageBody_OnlySqsValueInJson(t *testing.T) { message := `{"sqs": "message text"}` subject := "subject" - snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureJSON, nil) + snsMessage, err := createMessageBody(subs, message, subject, messageStructureJSON, nil) if err == nil { t.Fatalf(`error expected but instead SNS message was returned: %s`, snsMessage) } @@ -130,7 +132,7 @@ func TestCreateMessageBody_BothDefaultAndSqsValuesInJson(t *testing.T) { message := `{"default": "default message text", "sqs": "sqs message text"}` subject := "subject" - snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureJSON, nil) + snsMessage, err := createMessageBody(subs, message, subject, messageStructureJSON, nil) if err != nil { t.Fatalf(`error creating SNS message: %s`, err) } @@ -173,7 +175,7 @@ func TestCreateMessageBody_NonJsonContainingJson(t *testing.T) { message := `{"default": "default message text", "sqs": "sqs message text"}` subject := "subject" - snsMessage, err := CreateMessageBody(subs, message, subject, "", nil) + snsMessage, err := createMessageBody(subs, message, subject, "", nil) if err != nil { t.Fatalf(`error creating SNS message: %s`, err) } @@ -219,7 +221,7 @@ func TestCreateMessageBody_WithMessageAttributes(t *testing.T) { attributes := map[string]app.MessageAttributeValue{ stringMessageAttributeValue.DataType: stringMessageAttributeValue, } - snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureEmpty, attributes) + snsMessage, err := createMessageBody(subs, message, subject, messageStructureEmpty, attributes) if err != nil { t.Fatalf(`error creating SNS message: %s`, err) } diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index 2e64c2e2..a5ddce98 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -45,241 +45,6 @@ func TestListTopicshandler_POST_NoTopics(t *testing.T) { } } -func TestPublishhandler_POST_SendMessage(t *testing.T) { - defer func() { - test.ResetApp() - }() - - // 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:UnitTestTopic1") - form.Add("Message", "TestMessage1") - req.PostForm = form - - // Prepare existant topic - topic := &app.Topic{ - Name: "UnitTestTopic1", - Arn: "arn:aws:sns:local:000000000000:UnitTestTopic1", - } - app.SyncTopics.Topics["UnitTestTopic1"] = topic - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(Publish) - - // 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) - } -} - -func TestPublishHandler_POST_FilterPolicyRejectsTheMessage(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 set up queue so later we can check if anything was posted there - queueName := "testingQueue" - queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/queue/" + queueName - queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":000000000000:" + queueName - app.SyncQueues.Queues[queueName] = &app.Queue{ - Name: queueName, - VisibilityTimeout: 30, - Arn: queueArn, - URL: queueUrl, - IsFIFO: app.HasFIFOQueueName(queueName), - } - - // We set up a topic with the corresponding Subscription including FilterPolicy - topicName := "testingTopic" - topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName - subArn, _ := common.NewUUID() - subArn = topicArn + ":" + subArn - app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ - { - EndPoint: app.SyncQueues.Queues[queueName].Arn, - Protocol: "sqs", - SubscriptionArn: subArn, - FilterPolicy: &app.FilterPolicy{ - "foo": {"bar"}, // set up FilterPolicy for attribute `foo` to be equal `bar` - }, - }, - }} - - form := url.Values{} - form.Add("TopicArn", topicArn) - form.Add("Message", "TestMessage1") - form.Add("MessageAttributes.entry.1.Name", "foo") // special format of parameter for MessageAttribute - form.Add("MessageAttributes.entry.1.Value.StringValue", "baz") // we actually sent attribute `foo` to be equal `baz` - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(Publish) - - // 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) - } - - // check of the queue is empty - if len(app.SyncQueues.Queues[queueName].Messages) != 0 { - t.Errorf("queue contains unexpected messages: got %v want %v", - len(app.SyncQueues.Queues[queueName].Messages), 0) - } -} - -func TestPublishHandler_POST_FilterPolicyPassesTheMessage(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 set up queue so later we can check if anything was posted there - queueName := "testingQueue" - queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/queue/" + queueName - queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":000000000000:" + queueName - app.SyncQueues.Queues[queueName] = &app.Queue{ - Name: queueName, - VisibilityTimeout: 30, - Arn: queueArn, - URL: queueUrl, - IsFIFO: app.HasFIFOQueueName(queueName), - } - - // We set up a topic with the corresponding Subscription including FilterPolicy - topicName := "testingTopic" - topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName - subArn, _ := common.NewUUID() - subArn = topicArn + ":" + subArn - app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ - { - EndPoint: app.SyncQueues.Queues[queueName].Arn, - Protocol: "sqs", - SubscriptionArn: subArn, - FilterPolicy: &app.FilterPolicy{ - "foo": {"bar"}, // set up FilterPolicy for attribute `foo` to be equal `bar` - }, - }, - }} - - form := url.Values{} - form.Add("TopicArn", topicArn) - form.Add("Message", "TestMessage1") - form.Add("MessageAttributes.entry.1.Name", "foo") // special format of parameter for MessageAttribute - form.Add("MessageAttributes.entry.1.Value.DataType", "String") // Datatype must be specified for proper parsing by aws - form.Add("MessageAttributes.entry.1.Value.StringValue", "bar") // we actually sent attribute `foo` to be equal `baz` - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(Publish) - - // 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) - } - - // check of the queue is empty - if len(app.SyncQueues.Queues[queueName].Messages) != 1 { - t.Errorf("queue contains unexpected messages: got %v want %v", - len(app.SyncQueues.Queues[queueName].Messages), 1) - } -} - -func TestPublish_No_Queue_Error_handler_POST_Success(t *testing.T) { - defer func() { - test.ResetApp() - }() - - // 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:UnitTestTopic1") - form.Add("Message", "TestMessage1") - req.PostForm = form - - // Prepare existant topic - topic := &app.Topic{ - Name: "UnitTestTopic1", - Arn: "arn:aws:sns:local:000000000000:UnitTestTopic1", - } - app.SyncTopics.Topics["UnitTestTopic1"] = topic - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(Publish) - - // 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") diff --git a/app/gosns/publish.go b/app/gosns/publish.go new file mode 100644 index 00000000..34c7b0ae --- /dev/null +++ b/app/gosns/publish.go @@ -0,0 +1,201 @@ +package gosns + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + log "github.com/sirupsen/logrus" +) + +// TODO - Admiral-Piett - Pick a MessageAttribute style and get rid of `utils.ConvertToOldMessageAttributeValueStructure` + +// aws --endpoint-url http://localhost:47194 sns publish --topic-arn arn:aws:sns:yopa-local:000000000000:test1 --message "This is a test" +func PublishV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewPublishRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - PublishV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + // TODO - support TargetArn + if requestBody.TopicArn == "" || requestBody.Message == "" { + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + arnSegments := strings.Split(requestBody.TopicArn, ":") + topicName := arnSegments[len(arnSegments)-1] + + _, ok = app.SyncTopics.Topics[topicName] + if ok { + log.WithFields(log.Fields{ + "topic": topicName, + "topicArn": requestBody.TopicArn, + "subject": requestBody.Subject, + }).Debug("Publish to Topic") + for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { + switch app.Protocol(subscription.Protocol) { + case app.ProtocolSQS: + err := publishSQS(subscription, topicName, requestBody) + if err != nil { + utils.CreateErrorResponseV1(err.Error(), false) + } + case app.ProtocolHTTP: + fallthrough + case app.ProtocolHTTPS: + publishHTTP(subscription, requestBody) + } + } + } else { + return utils.CreateErrorResponseV1("TopicNotFound", false) + } + + //Create the response + respStruct := models.PublishResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.PublishResult{ + MessageId: uuid.NewString(), + }, + Metadata: app.ResponseMetadata{ + RequestId: uuid.NewString(), + }, + } + return http.StatusOK, respStruct +} + +func publishSQS(subscription *app.Subscription, topicName string, requestBody *models.PublishRequest) error { + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(requestBody.MessageAttributes) + if subscription.FilterPolicy != nil && !subscription.FilterPolicy.IsSatisfiedBy(messageAttributes) { + return nil + } + + endPoint := subscription.EndPoint + uriSegments := strings.Split(endPoint, "/") + queueName := uriSegments[len(uriSegments)-1] + arnSegments := strings.Split(queueName, ":") + queueName = arnSegments[len(arnSegments)-1] + + if _, ok := app.SyncQueues.Queues[queueName]; ok { + msg := app.Message{} + + if subscription.Raw == false { + m, err := createMessageBody(subscription, requestBody.Message, requestBody.Subject, requestBody.MessageStructure, messageAttributes) + if err != nil { + return err + } + + msg.MessageBody = m + } else { + msg.MessageAttributes = messageAttributes + msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) + m, err := extractMessageFromJSON(requestBody.Message, subscription.Protocol) + if err == nil { + msg.MessageBody = []byte(m) + } else { + msg.MessageBody = []byte(requestBody.Message) + } + } + + msg.MD5OfMessageBody = common.GetMD5Hash(requestBody.Message) + msg.Uuid, _ = common.NewUUID() + app.SyncQueues.Lock() + app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) + app.SyncQueues.Unlock() + + log.Infof("%s: Topic: %s(%s), Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), topicName, queueName, msg.MessageBody) + } else { + log.Infof("%s: Queue %s does not exist, message discarded\n", time.Now().Format("2006-01-02 15:04:05"), queueName) + } + return nil +} + +func publishHTTP(subs *app.Subscription, requestBody *models.PublishRequest) { + messageAttributes := utils.ConvertToOldMessageAttributeValueStructure(requestBody.MessageAttributes) + id := uuid.NewString() + msg := app.SNSMessage{ + Type: "Notification", + MessageId: id, + TopicArn: requestBody.TopicArn, + Subject: requestBody.Subject, + Message: requestBody.Message, + Timestamp: time.Now().UTC().Format(time.RFC3339), + SignatureVersion: "1", + SigningCertURL: fmt.Sprintf("http://%s:%s/SimpleNotificationService/%s.pem", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, id), + UnsubscribeURL: fmt.Sprintf("http://%s:%s/?Action=Unsubscribe&SubscriptionArn=%s", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, subs.SubscriptionArn), + MessageAttributes: formatAttributes(messageAttributes), + } + + signature, err := signMessage(PrivateKEY, &msg) + if err != nil { + log.Error(err) + } else { + msg.Signature = signature + } + err = callEndpoint(subs.EndPoint, subs.SubscriptionArn, msg, subs.Raw) + if err != nil { + log.WithFields(log.Fields{ + "EndPoint": subs.EndPoint, + "ARN": subs.SubscriptionArn, + "error": err.Error(), + }).Error("Error calling endpoint") + } +} + +func createMessageBody(subs *app.Subscription, msg string, subject string, messageStructure string, + messageAttributes map[string]app.MessageAttributeValue) ([]byte, error) { + + msgId := uuid.NewString() + message := app.SNSMessage{ + Type: "Notification", + MessageId: msgId, + TopicArn: subs.TopicArn, + Subject: subject, + Timestamp: time.Now().UTC().Format(time.RFC3339), + SignatureVersion: "1", + SigningCertURL: fmt.Sprintf("http://%s:%s/SimpleNotificationService/%s.pem", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, msgId), + UnsubscribeURL: fmt.Sprintf("http://%s:%s/?Action=Unsubscribe&SubscriptionArn=%s", app.CurrentEnvironment.Host, app.CurrentEnvironment.Port, subs.SubscriptionArn), + MessageAttributes: formatAttributes(messageAttributes), + } + + if app.MessageStructure(messageStructure) == app.MessageStructureJSON { + m, err := extractMessageFromJSON(msg, subs.Protocol) + if err != nil { + return nil, err + } + message.Message = m + } else { + message.Message = msg + } + + signature, err := signMessage(PrivateKEY, &message) + if err != nil { + log.Error(err) + } else { + message.Signature = signature + } + + byteMsg, _ := json.Marshal(message) + return byteMsg, nil +} + +func formatAttributes(values map[string]app.MessageAttributeValue) map[string]app.MsgAttr { + attr := make(map[string]app.MsgAttr) + for k, v := range values { + attr[k] = app.MsgAttr{ + Type: v.DataType, + Value: v.Value, + } + } + return attr +} diff --git a/app/gosns/publish_test.go b/app/gosns/publish_test.go new file mode 100644 index 00000000..bf93bb86 --- /dev/null +++ b/app/gosns/publish_test.go @@ -0,0 +1,499 @@ +package gosns + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Admiral-Piett/goaws/app/fixtures" + + "github.com/Admiral-Piett/goaws/app" + "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 TestPublishV1_success_sqs(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := PublishV1(r) + + assert.Equal(t, http.StatusOK, status) + _, ok := response.(models.PublishResponse) + assert.True(t, ok) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 1) + assert.Equal(t, message, string(messages[0].MessageBody)) +} + +func TestPublishV1_success_http(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := PublishV1(r) + + assert.Equal(t, http.StatusOK, status) + _, ok := response.(models.PublishResponse) + assert.True(t, ok) +} + +func TestPublishV1_success_https(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Unlock() + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := PublishV1(r) + + assert.Equal(t, http.StatusOK, status) + _, ok := response.(models.PublishResponse) + assert.True(t, ok) +} + +func TestPublishV1_success_with_optional_fields(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + TopicArn: topicArn, + Message: message, + MessageAttributes: map[string]models.MessageAttributeValue{ + "test": models.MessageAttributeValue{ + DataType: "string", + StringValue: "value", + }, + }, + MessageDeduplicationId: "dedupe-id", + MessageGroupId: "group-id", + MessageStructure: "json", + PhoneNumber: "phone-number", + Subject: "subject", + TargetArn: "target-arn", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, response := PublishV1(r) + + assert.Equal(t, http.StatusOK, status) + _, ok := response.(models.PublishResponse) + assert.True(t, ok) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 1) + assert.Equal(t, message, string(messages[0].MessageBody)) +} + +func TestPublishV1_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) + status, _ := PublishV1(r) + + assert.Equal(t, http.StatusBadRequest, status) +} + +func TestPublishV1_request_missing_topic_arn(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + Message: message, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, _ := PublishV1(r) + + assert.Equal(t, http.StatusBadRequest, status) +} + +func TestPublishV1_request_missing_message(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + TopicArn: topicArn, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, _ := PublishV1(r) + + assert.Equal(t, http.StatusBadRequest, status) +} + +func TestPublishV1_request_invalid_topic(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + message := "{\"IAm\": \"aMessage\"}" + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.PublishRequest) + *v = models.PublishRequest{ + TopicArn: "garbage", + Message: message, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + status, _ := PublishV1(r) + + assert.Equal(t, http.StatusBadRequest, status) +} + +func Test_publishSQS_success_raw(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + request := models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + err := publishSQS(sub, "unit-topic1", &request) + + assert.Nil(t, err) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 1) + assert.Equal(t, message, string(messages[0].MessageBody)) +} + +// Most other scenarios should be tested in the functions above, if reasonably possible +func Test_publishSQS_success_json(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + + app.SyncTopics.Lock() + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + sub.Raw = false + app.SyncTopics.Unlock() + request := models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + err := publishSQS(sub, "unit-topic1", &request) + + assert.Nil(t, err) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 1) + + body := string(messages[0].MessageBody) + assert.Contains(t, body, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, body, "Type") + assert.Contains(t, body, "MessageId") + assert.Contains(t, body, "TopicArn") + assert.Contains(t, body, "Signature") + assert.Contains(t, body, "SigningCertURL") + assert.Contains(t, body, "UnsubscribeURL") + assert.Contains(t, body, "SubscribeURL") + assert.Contains(t, body, "MessageAttributes") +} + +func Test_publishSQS_filter_policy_not_satisfied_by_attributes(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + + app.SyncTopics.Lock() + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + sub.FilterPolicy = &app.FilterPolicy{"foo": []string{"bar"}} + app.SyncTopics.Unlock() + + request := models.PublishRequest{ + TopicArn: topicArn, + Message: message, + MessageAttributes: map[string]models.MessageAttributeValue{ + "invalid": models.MessageAttributeValue{ + DataType: "string", + StringValue: "garbage", + }, + }, + } + err := publishSQS(sub, "unit-topic1", &request) + + assert.Nil(t, err) +} + +func Test_publishSQS_missing_queue_returns_nil(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + + message := "{\"IAm\": \"aMessage\"}" + + app.SyncTopics.Lock() + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + sub.EndPoint = "garbage" + app.SyncTopics.Unlock() + + request := models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + err := publishSQS(sub, "unit-topic1", &request) + + assert.Nil(t, err) +} + +// Most other scenarios should be tested in the functions above, if reasonably possible +func Test_publishHTTP_success(t *testing.T) { + called := false + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(200) + })) + + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + subscribedServer.Close() + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + + app.SyncTopics.Lock() + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + sub.EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + request := models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + + publishHTTP(sub, &request) + + assert.True(t, called) +} + +func Test_publishHTTP_callEndpoint_failure(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + + app.SyncTopics.Lock() + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + app.SyncTopics.Unlock() + + request := models.PublishRequest{ + TopicArn: topicArn, + Message: message, + } + + publishHTTP(sub, &request) + // swallows all errors +} + +func Test_createMessageBody_success_json(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + message := "{\"default\": \"message\"}" + subject := "I'm the subject" + attrs := map[string]app.MessageAttributeValue{ + "test": app.MessageAttributeValue{ + Name: "MyAttr", + DataType: "string", + Value: "value", + }, + } + + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + + result, err := createMessageBody(sub, message, subject, "json", attrs) + + assert.Nil(t, err) + + msg := &app.SNSMessage{} + json.Unmarshal(result, msg) + + assert.Equal(t, "Notification", msg.Type) + assert.Equal(t, "", msg.Token) + assert.Equal(t, fmt.Sprintf("%s:unit-topic1", fixtures.BASE_SNS_ARN), msg.TopicArn) + assert.Equal(t, "I'm the subject", msg.Subject) + assert.Equal(t, "message", msg.Message) + assert.Equal(t, "1", msg.SignatureVersion) + assert.Contains(t, msg.SigningCertURL, "http://host:port/SimpleNotificationService/") + assert.Contains(t, msg.UnsubscribeURL, "http://host:port/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:region:accountID:unit-topic1:") + assert.Equal(t, msg.MessageAttributes, map[string]app.MsgAttr{"test": app.MsgAttr{Type: "string", Value: "value"}}) +} + +func Test_createMessageBody_success_raw(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + }() + + message := "{\"default\": \"message\"}" + subject := "I'm the subject" + attrs := map[string]app.MessageAttributeValue{ + "test": app.MessageAttributeValue{ + Name: "MyAttr", + DataType: "string", + Value: "value", + }, + } + + sub := app.SyncTopics.Topics["unit-topic1"].Subscriptions[0] + + result, err := createMessageBody(sub, message, subject, "not-json", attrs) + + assert.Nil(t, err) + + msg := &app.SNSMessage{} + json.Unmarshal(result, msg) + + assert.Equal(t, "Notification", msg.Type) + assert.Equal(t, "", msg.Token) + assert.Equal(t, fmt.Sprintf("%s:unit-topic1", fixtures.BASE_SNS_ARN), msg.TopicArn) + assert.Equal(t, "I'm the subject", msg.Subject) + assert.Equal(t, message, msg.Message) + assert.Equal(t, "1", msg.SignatureVersion) + assert.Contains(t, msg.SigningCertURL, "http://host:port/SimpleNotificationService/") + assert.Contains(t, msg.UnsubscribeURL, "http://host:port/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:region:accountID:unit-topic1:") + assert.Equal(t, msg.MessageAttributes, map[string]app.MsgAttr{"test": app.MsgAttr{Type: "string", Value: "value"}}) +} + +func Test_formatAttributes_success(t *testing.T) { + attrs := map[string]app.MessageAttributeValue{ + "test1": app.MessageAttributeValue{ + Name: "MyAttr", + DataType: "string", + Value: "value1", + }, + "test2": app.MessageAttributeValue{ + Name: "MyAttr", + DataType: "string", + Value: "value2", + }, + } + expected := map[string]app.MsgAttr{ + "test1": app.MsgAttr{Type: "string", Value: "value1"}, + "test2": app.MsgAttr{Type: "string", Value: "value2"}, + } + + result := formatAttributes(attrs) + + assert.Equal(t, expected, result) +} diff --git a/app/gosns/subscribe_test.go b/app/gosns/subscribe_test.go index dc670cf2..2ae46e79 100644 --- a/app/gosns/subscribe_test.go +++ b/app/gosns/subscribe_test.go @@ -108,7 +108,7 @@ func TestSubscribeV1_success_duplicate_subscription(t *testing.T) { v := resultingStruct.(*models.SubscribeRequest) *v = models.SubscribeRequest{ TopicArn: fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic1"), - Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue2"), + Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue1"), Protocol: "sqs", } return true @@ -122,7 +122,7 @@ func TestSubscribeV1_success_duplicate_subscription(t *testing.T) { subscriptions := app.SyncTopics.Topics["unit-topic1"].Subscriptions assert.Len(t, subscriptions, 1) - assert.Equal(t, fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue1"), subscriptions[0].EndPoint) assert.Equal(t, "sqs", subscriptions[0].Protocol) assert.True(t, subscriptions[0].Raw) assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "unit-topic1")) @@ -157,7 +157,7 @@ func TestSubscribeV1_error_missing_topic(t *testing.T) { v := resultingStruct.(*models.SubscribeRequest) *v = models.SubscribeRequest{ TopicArn: fmt.Sprintf("%s:%s", fixtures.BASE_SNS_ARN, "garbage"), - Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue2"), + Endpoint: fmt.Sprintf("%s:%s", fixtures.BASE_SQS_ARN, "subscribed-queue1"), Protocol: "sqs", } return true diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index ceb237a8..cc180f1a 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -60,7 +60,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { log.Debugf("Putting Message in Queue: [%s]", queueName) msg := app.Message{MessageBody: []byte(messageBody)} if len(messageAttributes) > 0 { - oldStyleMessageAttributes := convertToOldMessageAttributeValueStructure(messageAttributes) + oldStyleMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(messageAttributes) msg.MessageAttributes = oldStyleMessageAttributes msg.MD5OfMessageAttributes = common.HashAttributes(oldStyleMessageAttributes) } @@ -100,32 +100,3 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { return http.StatusOK, respStruct } - -// TODO: -// Refactor internal model for MessageAttribute between SendMessage and ReceiveMessage -// from app.MessageAttributeValue(old) to models.MessageAttributeValue(new) and remove this temporary function. -func convertToOldMessageAttributeValueStructure(newValues map[string]models.MessageAttributeValue) map[string]app.MessageAttributeValue { - attributes := make(map[string]app.MessageAttributeValue) - - for name, entry := range newValues { - // StringListValue and BinaryListValue is currently not implemented - // Please refer app/gosqs/message_attributes.go - value := "" - valueKey := "" - if entry.StringValue != "" { - value = entry.StringValue - valueKey = "StringValue" - } else if entry.BinaryValue != "" { - value = entry.BinaryValue - valueKey = "BinaryValue" - } - attributes[name] = app.MessageAttributeValue{ - Name: name, - DataType: entry.DataType, - Value: value, - ValueKey: valueKey, - } - } - - return attributes -} diff --git a/app/gosqs/send_message_batch.go b/app/gosqs/send_message_batch.go index cce6d8fe..e670c0d4 100644 --- a/app/gosqs/send_message_batch.go +++ b/app/gosqs/send_message_batch.go @@ -62,7 +62,7 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody for _, sendEntry := range sendEntries { msg := app.Message{MessageBody: []byte(sendEntry.MessageBody)} if len(sendEntry.MessageAttributes) > 0 { - oldStyleMessageAttributes := convertToOldMessageAttributeValueStructure(sendEntry.MessageAttributes) + oldStyleMessageAttributes := utils.ConvertToOldMessageAttributeValueStructure(sendEntry.MessageAttributes) msg.MessageAttributes = oldStyleMessageAttributes msg.MD5OfMessageAttributes = common.HashAttributes(oldStyleMessageAttributes) } diff --git a/app/models/models.go b/app/models/models.go index 5a42bb4f..351eaafc 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -23,3 +23,12 @@ var AVAILABLE_QUEUE_ATTRIBUTES = map[string]bool{ "LastModifiedTimestamp": true, "QueueArn": true, } + +// TODO - reconcile this with app.MessageAttributeValue - deal with ConvertToOldMessageAttributeValueStructure +type MessageAttributeValue struct { + BinaryListValues []string `json:"BinaryListValues"` // currently unsupported by AWS + BinaryValue string `json:"BinaryValue"` + DataType string `json:"DataType"` + StringListValues []string `json:"StringListValues"` // currently unsupported by AWS + StringValue string `json:"StringValue"` +} diff --git a/app/models/responses.go b/app/models/responses.go index 831201fc..a82e4bb4 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -401,3 +401,22 @@ func (r DeleteMessageBatchResponse) GetResult() interface{} { func (r DeleteMessageBatchResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Publish ***/ +type PublishResult struct { + MessageId string `xml:"MessageId"` +} + +type PublishResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result PublishResult `xml:"PublishResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r PublishResponse) GetResult() interface{} { + return r.Result +} + +func (r PublishResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index eab5352b..422e8cd2 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -46,7 +46,6 @@ type TopicAttributes struct { } func (r *CreateTopicRequest) SetAttributesFromForm(values url.Values) { - for i := 1; true; i++ { nameKey := fmt.Sprintf("Attribute.%d.Name", i) attrName := values.Get(nameKey) @@ -183,3 +182,45 @@ type UnsubscribeRequest struct { } func (r *UnsubscribeRequest) SetAttributesFromForm(values url.Values) {} + +func NewPublishRequest() *PublishRequest { + return &PublishRequest{} +} + +type PublishRequest struct { + Message string `json:"Message" schema:"Message"` + MessageAttributes map[string]MessageAttributeValue `json:"MessageAttributes" schema:"MessageAttributes"` + MessageDeduplicationId string `json:"MessageDeduplicationId" schema:"MessageDeduplicationId"` // Not implemented + MessageGroupId string `json:"MessageGroupId" schema:"MessageGroupId"` // Not implemented + MessageStructure string `json:"MessageStructure" schema:"MessageStructure"` + PhoneNumber string `json:"PhoneNumber" schema:"PhoneNumber"` // Not implemented + Subject string `json:"Subject" schema:"Subject"` + TargetArn string `json:"TargetArn" schema:"TargetArn"` // Not implemented + TopicArn string `json:"TopicArn" schema:"TopicArn"` +} + +func (r *PublishRequest) SetAttributesFromForm(values url.Values) { + for i := 1; true; i++ { + nameKey := fmt.Sprintf("MessageAttributes.entry.%d.Name", i) + name := values.Get(nameKey) + if name == "" { + break + } + + dataTypeKey := fmt.Sprintf("MessageAttributes.entry.%d.Value.DataType", i) + dataType := values.Get(dataTypeKey) + if dataType == "" { + log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) + continue + } + + stringValue := values.Get(fmt.Sprintf("MessageAttributes.entry.%d.Value.StringValue", i)) + binaryValue := values.Get(fmt.Sprintf("MessageAttributes.entry.%d.Value.BinaryValue", i)) + + r.MessageAttributes[name] = MessageAttributeValue{ + DataType: dataType, + StringValue: stringValue, + BinaryValue: binaryValue, + } + } +} diff --git a/app/models/sqs.go b/app/models/sqs.go index 7d18b323..5bc9f464 100644 --- a/app/models/sqs.go +++ b/app/models/sqs.go @@ -185,13 +185,6 @@ type SendMessageRequest struct { MessageSystemAttributes map[string]MessageAttributeValue `json:"MessageSystemAttributes" schema:"MessageSystemAttributes"` QueueUrl string `json:"QueueUrl" schema:"QueueUrl"` } -type MessageAttributeValue struct { - BinaryListValues []string `json:"BinaryListValues"` // currently unsupported by AWS - BinaryValue string `json:"BinaryValue"` - DataType string `json:"DataType"` - StringListValues []string `json:"StringListValues"` // currently unsupported by AWS - StringValue string `json:"StringValue"` -} func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { for i := 1; true; i++ { @@ -216,10 +209,6 @@ func (r *SendMessageRequest) SetAttributesFromForm(values url.Values) { StringValue: stringValue, BinaryValue: binaryValue, } - - if _, ok := r.MessageAttributes[name]; !ok { - log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) - } } } diff --git a/app/router/router.go b/app/router/router.go index d79effb8..48fa8571 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -82,6 +82,7 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, "Unsubscribe": sns.UnsubscribeV1, + "Publish": sns.PublishV1, } var routingTable = map[string]http.HandlerFunc{ @@ -92,7 +93,6 @@ var routingTable = map[string]http.HandlerFunc{ "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, "ListSubscriptions": sns.ListSubscriptions, - "Publish": sns.Publish, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/router/router_test.go b/app/router/router_test.go index 07b275ee..b2f918ae 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -274,6 +274,7 @@ func TestActionHandler_v0_xml(t *testing.T) { "CreateTopic": sns.CreateTopicV1, "Subscribe": sns.SubscribeV1, "Unsubscribe": sns.UnsubscribeV1, + "Publish": sns.PublishV1, } routingTable = map[string]http.HandlerFunc{ @@ -284,7 +285,6 @@ func TestActionHandler_v0_xml(t *testing.T) { "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, "ListSubscriptions": sns.ListSubscriptions, - "Publish": sns.Publish, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/servertest/server_test.go b/app/servertest/server_test.go index 48be85a0..271047c0 100644 --- a/app/servertest/server_test.go +++ b/app/servertest/server_test.go @@ -4,24 +4,11 @@ import ( "errors" "testing" - "encoding/json" - "fmt" - "io/ioutil" - "net" - "net/http" - "strings" - "time" - - "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/router" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sqs" "github.com/aws/aws-sdk-go/service/sqs/sqsiface" - "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -115,46 +102,6 @@ func TestNewIntegration(t *testing.T) { } } -func TestSNSRoutes(t *testing.T) { - // Consume address - srv, err := NewSNSTest("localhost:4100", &snsTest{t: t}) - - noSetupError(t, err) - defer srv.Quit() - - creds := credentials.NewStaticCredentials("id", "secret", "token") - - awsConfig := aws.NewConfig(). - WithRegion("us-east-1"). - WithEndpoint(srv.URL()). - WithCredentials(creds) - - session1 := session.New(awsConfig) - client := sns.New(session1) - - response, err := client.CreateTopic(&sns.CreateTopicInput{ - Name: aws.String("testing"), - }) - require.NoError(t, err, "SNS Create Topic Failed") - - params := &sns.SubscribeInput{ - Protocol: aws.String("sqs"), // Required - TopicArn: response.TopicArn, // Required - Endpoint: aws.String(srv.URL() + "/local-sns"), - } - subscribeResponse, err := client.Subscribe(params) - require.NoError(t, err, "SNS Subscribe Failed") - t.Logf("Succesfully subscribed: %s\n", *subscribeResponse.SubscriptionArn) - - publishParams := &sns.PublishInput{ - Message: aws.String("Cool"), - TopicArn: response.TopicArn, - } - publishResponse, err := client.Publish(publishParams) - require.NoError(t, err, "SNS Publish Failed") - t.Logf("Succesfully published: %s\n", *publishResponse.MessageId) -} - func newSQS(t *testing.T, region string, endpoint string) *sqs.SQS { creds := credentials.NewStaticCredentials("id", "secret", "token") @@ -176,123 +123,3 @@ func noOp(sqsiface.SQSAPI, *string) error { func noSetupError(t *testing.T, err error) { require.NoError(t, err, "Failed to setup for test") } - -type snsTest struct { - t *testing.T -} - -func NewSNSTest(addr string, snsTest *snsTest) (*Server, error) { - if addr == "" { - addr = "localhost:0" - } - localURL := strings.Split(addr, ":") - app.CurrentEnvironment.Host = localURL[0] - app.CurrentEnvironment.Port = localURL[1] - log.WithFields(log.Fields{ - "host": app.CurrentEnvironment.Host, - "port": app.CurrentEnvironment.Port, - }).Info("URL Starting to listen") - - l, err := net.Listen("tcp", addr) - if err != nil { - return nil, fmt.Errorf("cannot listen on localhost: %v", err) - } - if err != nil { - return nil, fmt.Errorf("cannot listen on localhost: %v", err) - } - - r := mux.NewRouter() - r.Handle("/", router.New()) - snsTest.SetSNSRoutes("/local-sns", r, nil) - - srv := Server{listener: l, handler: r} - - go http.Serve(l, &srv) - - return &srv, nil -} - -// Define handlers for various AWS SNS POST calls -func (s *snsTest) SetSNSRoutes(urlPath string, r *mux.Router, handler http.Handler) { - - r.HandleFunc(urlPath, s.SubscribeConfirmHandle).Methods("POST").Headers("x-amz-sns-message-type", "SubscriptionConfirmation") - if handler != nil { - log.WithFields(log.Fields{ - "urlPath": urlPath, - }).Debug("handler not nil") - // handler is supposed to be wrapper that inturn calls NotificationHandle - r.Handle(urlPath, handler).Methods("POST").Headers("x-amz-sns-message-type", "Notification") - } else { - log.WithFields(log.Fields{ - "urlPath": urlPath, - }).Debug("handler nil") - // if no wrapper handler available then define anonymous handler and directly call NotificationHandle - r.HandleFunc(urlPath, func(rw http.ResponseWriter, req *http.Request) { - s.NotificationHandle(rw, req) - }).Methods("POST").Headers("x-amz-sns-message-type", "Notification") - } -} - -func (s *snsTest) SubscribeConfirmHandle(rw http.ResponseWriter, req *http.Request) { - //params := &sns.ConfirmSubscriptionInput{ - // Token: aws.String(msg.Token), // Required - // TopicArn: aws.String(msg.TopicArn), // Required - //} - var f interface{} - body, err := ioutil.ReadAll(req.Body) - if err != nil { - s.t.Log("Unable to Parse Body") - } - s.t.Log(string(body)) - err = json.Unmarshal(body, &f) - if err != nil { - s.t.Log("Unable to Unmarshal request") - } - - data := f.(map[string]interface{}) - s.t.Log(data["Type"].(string)) - - if data["Type"].(string) == "SubscriptionConfirmation" { - subscribeURL := data["SubscribeURL"].(string) - time.Sleep(time.Second) - response, err := http.Get(subscribeURL) - if err != nil { - s.t.Logf("Unable to confirm subscriptions. %s\n", err) - s.t.Fail() - } else { - s.t.Logf("Subscription Confirmed successfully. %d\n", response.StatusCode) - } - } else if data["Type"].(string) == "Notification" { - s.t.Log("Received this message : ", data["Message"].(string)) - } -} - -func (s *snsTest) NotificationHandle(rw http.ResponseWriter, req *http.Request) []byte { - subArn := req.Header.Get("X-Amz-Sns-Subscription-Arn") - - msg := app.SNSMessage{} - _, err := DecodeJSONMessage(req, &msg) - if err != nil { - log.Error(err) - return []byte{} - } - - s.t.Logf("NotificationHandle %s MSG(%s)", subArn, msg.Message) - return []byte(msg.Message) -} - -func DecodeJSONMessage(req *http.Request, v interface{}) ([]byte, error) { - - payload, err := ioutil.ReadAll(req.Body) - if err != nil { - return nil, err - } - if len(payload) == 0 { - return nil, errors.New("empty payload") - } - err = json.Unmarshal([]byte(payload), v) - if err != nil { - return nil, err - } - return payload, nil -} diff --git a/app/sns_messages.go b/app/sns_messages.go index 3e4fff4a..5917e54c 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -81,18 +81,6 @@ type ListSubscriptionsByTopicResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Publish ***/ - -type PublishResult struct { - MessageId string `xml:"MessageId"` -} - -type PublishResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result PublishResult `xml:"PublishResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} - /*** Delete Topic ***/ type DeleteTopicResponse struct { Xmlns string `xml:"xmlns,attr"` diff --git a/app/utils/utils.go b/app/utils/utils.go index 0ed52dda..e5b7305d 100644 --- a/app/utils/utils.go +++ b/app/utils/utils.go @@ -7,6 +7,8 @@ import ( "net/http" "net/url" + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/interfaces" @@ -89,3 +91,32 @@ func CreateErrorResponseV1(errKey string, isSqs bool) (int, interfaces.AbstractR } return err.StatusCode(), respStruct } + +// TODO: +// Refactor internal model for MessageAttribute between SendMessage and ReceiveMessage +// from app.MessageAttributeValue(old) to models.MessageAttributeValue(new) and remove this temporary function. +func ConvertToOldMessageAttributeValueStructure(newValues map[string]models.MessageAttributeValue) map[string]app.MessageAttributeValue { + attributes := make(map[string]app.MessageAttributeValue) + + for name, entry := range newValues { + // StringListValue and BinaryListValue is currently not implemented + // Please refer app/gosqs/message_attributes.go + value := "" + valueKey := "" + if entry.StringValue != "" { + value = entry.StringValue + valueKey = "StringValue" + } else if entry.BinaryValue != "" { + value = entry.BinaryValue + valueKey = "BinaryValue" + } + attributes[name] = app.MessageAttributeValue{ + Name: name, + DataType: entry.DataType, + Value: value, + ValueKey: valueKey, + } + } + + return attributes +} diff --git a/smoke_tests/sns_publish_test.go b/smoke_tests/sns_publish_test.go new file mode 100644 index 00000000..30082e57 --- /dev/null +++ b/smoke_tests/sns_publish_test.go @@ -0,0 +1,510 @@ +package smoke_tests + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/config" + + "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/test" + + "github.com/gavv/httpexpect/v2" + + "github.com/Admiral-Piett/goaws/app" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sns" + "github.com/stretchr/testify/assert" +) + +func Test_Publish_sqs_json_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ + TopicArn: &topicArn, + Message: &message, + Subject: &subject, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 1) + assert.Equal(t, message, string(messages[0].MessageBody)) +} + +func Test_Publish_sqs_json_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + topicArn := app.SyncTopics.Topics["unit-topic3"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ + TopicArn: &topicArn, + Message: &message, + Subject: &subject, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + messages := app.SyncQueues.Queues["subscribed-queue3"].Messages + assert.Len(t, messages, 1) + + body := string(messages[0].MessageBody) + assert.Contains(t, body, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, body, "Type") + assert.Contains(t, body, "MessageId") + assert.Contains(t, body, "TopicArn") + assert.Contains(t, body, subject) + assert.Contains(t, body, "Signature") + assert.Contains(t, body, "SigningCertURL") + assert.Contains(t, body, "UnsubscribeURL") + assert.Contains(t, body, "SubscribeURL") + assert.Contains(t, body, "MessageAttributes") +} + +func Test_Publish_http_json(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ + TopicArn: &topicArn, + Message: &message, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_https_json_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ + TopicArn: &topicArn, + Message: &message, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_https_json_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Raw = false + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + response, err := snsClient.Publish(context.TODO(), &sns.PublishInput{ + TopicArn: &topicArn, + Message: &message, + Subject: &subject, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + assert.True(t, called) + assert.Contains(t, httpMessage, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, httpMessage, "Type") + assert.Contains(t, httpMessage, "MessageId") + assert.Contains(t, httpMessage, "TopicArn") + assert.Contains(t, httpMessage, subject) + assert.Contains(t, httpMessage, "Signature") + assert.Contains(t, httpMessage, "SigningCertURL") + assert.Contains(t, httpMessage, "UnsubscribeURL") + assert.Contains(t, httpMessage, "SubscribeURL") + assert.Contains(t, httpMessage, "MessageAttributes") +} + +func Test_Publish_sqs_xml_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + topicArn := app.SyncTopics.Topics["unit-topic1"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + Action: "Publish", + TopicArn: topicArn, + Message: message, + Subject: subject, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + messages := app.SyncQueues.Queues["subscribed-queue1"].Messages + assert.Len(t, messages, 1) + assert.Equal(t, message, string(messages[0].MessageBody)) +} + +func Test_Publish_sqs_xml_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + topicArn := app.SyncTopics.Topics["unit-topic3"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + Action: "Publish", + TopicArn: topicArn, + Message: message, + Subject: subject, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + messages := app.SyncQueues.Queues["subscribed-queue3"].Messages + assert.Len(t, messages, 1) + + body := string(messages[0].MessageBody) + assert.Contains(t, body, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, body, "Type") + assert.Contains(t, body, "MessageId") + assert.Contains(t, body, "TopicArn") + assert.Contains(t, body, subject) + assert.Contains(t, body, "Signature") + assert.Contains(t, body, "SigningCertURL") + assert.Contains(t, body, "UnsubscribeURL") + assert.Contains(t, body, "SubscribeURL") + assert.Contains(t, body, "MessageAttributes") +} + +func Test_Publish_http_xml(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + Action: "Publish", + TopicArn: topicArn, + Message: message, + Subject: subject, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_https_xml_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + Action: "Publish", + TopicArn: topicArn, + Message: message, + Subject: subject, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + assert.True(t, called) + assert.Equal(t, "\"{\\\"IAm\\\": \\\"aMessage\\\"}\"", httpMessage) +} + +func Test_Publish_https_xml_not_raw(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + + called := false + httpMessage := "" + subscribedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + io.Copy(buf, r.Body) + httpMessage = buf.String() + + called = true + w.WriteHeader(200) + })) + + defer func() { + server.Close() + subscribedServer.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + e := httpexpect.Default(t, server.URL) + + app.SyncTopics.Lock() + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Protocol = "https" + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].Raw = false + app.SyncTopics.Topics["unit-topic-http"].Subscriptions[0].EndPoint = subscribedServer.URL + app.SyncTopics.Unlock() + + topicArn := app.SyncTopics.Topics["unit-topic-http"].Arn + message := "{\"IAm\": \"aMessage\"}" + subject := "I am a subject" + + requestBody := struct { + Action string `schema:"Action"` + TopicArn string `schema:"TopicArn"` + Message string `schema:"Message"` + Subject string `schema:"Subject"` + }{ + Action: "Publish", + TopicArn: topicArn, + Message: message, + Subject: subject, + } + + e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + assert.True(t, called) + assert.Contains(t, httpMessage, "\"Message\":\"{\\\"IAm\\\": \\\"aMessage\\\"}\"") + assert.Contains(t, httpMessage, "Type") + assert.Contains(t, httpMessage, "MessageId") + assert.Contains(t, httpMessage, "TopicArn") + assert.Contains(t, httpMessage, subject) + assert.Contains(t, httpMessage, "Signature") + assert.Contains(t, httpMessage, "SigningCertURL") + assert.Contains(t, httpMessage, "UnsubscribeURL") + assert.Contains(t, httpMessage, "SubscribeURL") + assert.Contains(t, httpMessage, "MessageAttributes") +} From 150a02befed2718973f003b5f1c15078505a54ed Mon Sep 17 00:00:00 2001 From: Nicholas Raffone Date: Tue, 9 Jul 2024 14:07:31 +0900 Subject: [PATCH 34/41] 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) +} From 4d51d273ff0b0f34d39bf70f80243efad78a2e2e Mon Sep 17 00:00:00 2001 From: Nicholas Raffone Date: Tue, 9 Jul 2024 15:56:14 +0900 Subject: [PATCH 35/41] add delete topic v1 support add request transform test to delete topic revise comments - change deletetopicv1 println to log.Info - remove unnecessary deletetopicv1 request setAttributesFromForm update deletetopics test to handle new mock config --- app/gosns/delete_topic.go | 47 +++++++ app/gosns/delete_topic_test.go | 88 ++++++++++++++ app/gosns/gosns.go | 23 ---- app/gosns/gosns_test.go | 58 --------- app/models/responses.go | 14 +++ app/models/sns.go | 12 ++ app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sns_messages.go | 6 - smoke_tests/sns_delete_topic_test.go | 175 +++++++++++++++++++++++++++ 10 files changed, 338 insertions(+), 89 deletions(-) create mode 100644 app/gosns/delete_topic.go create mode 100644 app/gosns/delete_topic_test.go create mode 100644 smoke_tests/sns_delete_topic_test.go diff --git a/app/gosns/delete_topic.go b/app/gosns/delete_topic.go new file mode 100644 index 00000000..2d3e214e --- /dev/null +++ b/app/gosns/delete_topic.go @@ -0,0 +1,47 @@ +package gosns + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/common" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + + log "github.com/sirupsen/logrus" +) + +func DeleteTopicV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewDeleteTopicRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - DeleteTopicV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + topicArn := requestBody.TopicArn + uriSegments := strings.Split(topicArn, ":") + topicName := uriSegments[len(uriSegments)-1] + + log.Info("Delete Topic - TopicName:", topicName) + + _, ok = app.SyncTopics.Topics[topicName] + + if !ok { + return utils.CreateErrorResponseV1("TopicNotFound", false) + } + + app.SyncTopics.Lock() + delete(app.SyncTopics.Topics, topicName) + app.SyncTopics.Unlock() + uuid, _ := common.NewUUID() + respStruct := models.DeleteTopicResponse{ + Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", + Metadata: app.ResponseMetadata{RequestId: uuid}, + } + + return http.StatusOK, respStruct + +} diff --git a/app/gosns/delete_topic_test.go b/app/gosns/delete_topic_test.go new file mode 100644 index 00000000..a7528c21 --- /dev/null +++ b/app/gosns/delete_topic_test.go @@ -0,0 +1,88 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "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 TestDeleteTopicV1_Success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + initial_num_topics := len(app.SyncTopics.Topics) + + topicName1 := "unit-topic1" + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.DeleteTopicRequest) + *v = models.DeleteTopicRequest{ + TopicArn: "arn:aws:sns:region:accountID:" + topicName1, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := DeleteTopicV1(r) + + response, _ := res.(models.DeleteTopicResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, models.BASE_XMLNS, response.Xmlns) + assert.NotEqual(t, "", response.Metadata) + + topics := app.SyncTopics.Topics + assert.Equal(t, initial_num_topics-1, len(topics)) + _, ok := topics[topicName1] + assert.False(t, ok) +} + +func TestDeleteTopicV1_NotFound(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.DeleteTopicRequest) + *v = models.DeleteTopicRequest{ + TopicArn: "asdf", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := DeleteTopicV1(r) + resp := res.(models.ErrorResponse) + + assert.Equal(t, http.StatusBadRequest, code) + assert.Equal(t, resp.Result.Type, "Not Found") +} + +func TestDeleteTopicV1_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, _ := DeleteTopicV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index d23f7759..bc453d02 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -288,29 +288,6 @@ func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { createErrorResponse(w, req, "SubscriptionNotFound") } -func DeleteTopic(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - topicArn := req.FormValue("TopicArn") - - uriSegments := strings.Split(topicArn, ":") - topicName := uriSegments[len(uriSegments)-1] - - log.Println("Delete Topic - TopicName:", topicName) - - _, ok := app.SyncTopics.Topics[topicName] - if ok { - app.SyncTopics.Lock() - delete(app.SyncTopics.Topics, topicName) - app.SyncTopics.Unlock() - uuid, _ := common.NewUUID() - respStruct := app.DeleteTopicResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} - SendResponseBack(w, req, respStruct, content) - } else { - createErrorResponse(w, req, "TopicNotFound") - } - -} - // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index 25775a7a..f9b2260e 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -9,7 +9,6 @@ import ( "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app/test" - "github.com/stretchr/testify/assert" "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/common" @@ -134,63 +133,6 @@ func TestListSubscriptionsResponse_No_Owner(t *testing.T) { } } -func TestDeleteTopichandler_POST_Success(t *testing.T) { - conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") - defer func() { - test.ResetApp() - }() - - // 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") - form.Add("Message", "TestMessage1") - req.PostForm = form - - // Prepare existant topic - topic := &app.Topic{ - Name: "local-topic1", - Arn: "arn:aws:sns:local:000000000000:local-topic1", - } - app.SyncTopics.Topics["local-topic1"] = topic - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(DeleteTopic) - - // 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) - } - // 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) - } - - // Target topic should be disappeared - _, ok := app.SyncTopics.Topics["local-topic1"] - assert.False(t, ok) -} - 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/models/responses.go b/app/models/responses.go index db7d6eed..5a01c632 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -447,3 +447,17 @@ func (r ListTopicsResponse) GetResult() interface{} { func (r ListTopicsResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Delete Topic ***/ +type DeleteTopicResponse struct { + Xmlns string `xml:"xmlns,attr"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r DeleteTopicResponse) GetResult() interface{} { + return nil +} + +func (r DeleteTopicResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index a4859d1a..5cbdf105 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -236,3 +236,15 @@ type ListTopicsRequest struct { } func (r *ListTopicsRequest) SetAttributesFromForm(values url.Values) {} + +// DeleteTopicV1 + +func NewDeleteTopicRequest() *DeleteTopicRequest { + return &DeleteTopicRequest{} +} + +type DeleteTopicRequest struct { + TopicArn string `json:"TopicArn" schema:"TopicArn"` +} + +func (r *DeleteTopicRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index e05412f7..5b78b332 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -84,11 +84,11 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "Publish": sns.PublishV1, "ListTopics": sns.ListTopicsV1, "CreateTopic": sns.CreateTopicV1, + "DeleteTopic": sns.DeleteTopicV1, } var routingTable = map[string]http.HandlerFunc{ // SNS - "DeleteTopic": sns.DeleteTopic, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, diff --git a/app/router/router_test.go b/app/router/router_test.go index c2b92438..10d66b72 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -276,11 +276,11 @@ func TestActionHandler_v0_xml(t *testing.T) { "Publish": sns.PublishV1, "ListTopics": sns.ListTopicsV1, "CreateTopic": sns.CreateTopicV1, + "DeleteTopic": sns.DeleteTopicV1, } routingTable = map[string]http.HandlerFunc{ // SNS - "DeleteTopic": sns.DeleteTopic, "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, diff --git a/app/sns_messages.go b/app/sns_messages.go index d720f17b..06465ef1 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -61,9 +61,3 @@ type ListSubscriptionsByTopicResponse struct { Result ListSubscriptionsByTopicResult `xml:"ListSubscriptionsByTopicResult"` Metadata ResponseMetadata `xml:"ResponseMetadata"` } - -/*** Delete Topic ***/ -type DeleteTopicResponse struct { - Xmlns string `xml:"xmlns,attr"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} diff --git a/smoke_tests/sns_delete_topic_test.go b/smoke_tests/sns_delete_topic_test.go new file mode 100644 index 00000000..04b2919a --- /dev/null +++ b/smoke_tests/sns_delete_topic_test.go @@ -0,0 +1,175 @@ +package smoke_tests + +import ( + "context" + "net/http" + "testing" + + "encoding/xml" + + "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" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sns" + + "github.com/stretchr/testify/assert" + + "github.com/gavv/httpexpect/v2" +) + +func Test_Delete_Topic_json_success(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" + resp, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName1), + }) + + topicName2 := "topic-2" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName2), + }) + + _, err := snsClient.DeleteTopic(context.TODO(), &sns.DeleteTopicInput{ + TopicArn: resp.TopicArn, + }) + + assert.Nil(t, err) + + app.SyncQueues.Lock() + + defer app.SyncQueues.Unlock() + + topics := app.SyncTopics.Topics + assert.Len(t, topics, 1) + + _, ok := topics[topicName1] + assert.False(t, ok) + + _, ok = topics[topicName2] + assert.True(t, ok) +} + +func Test_Delete_Topic_json_NotFound(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) + + _, err := snsClient.DeleteTopic(context.TODO(), &sns.DeleteTopicInput{ + TopicArn: aws.String("asdf"), + }) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "400") + assert.Contains(t, err.Error(), "SimpleNotificationService.NonExistentTopic") + + app.SyncQueues.Lock() + defer app.SyncQueues.Unlock() + + topics := app.SyncTopics.Topics + assert.Len(t, topics, 0) +} + +func Test_Delete_Topic_xml_success(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" + resp, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName1), + }) + + topicName2 := "topic-2" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: aws.String(topicName2), + }) + + requestBody := struct { + Action string `schema:"DeleteTopic"` + TopicArn string `schema:"TopicArn"` + }{ + Action: "DeleteTopic", + TopicArn: *resp.TopicArn, + } + + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + deleteTopicResponseObject := models.DeleteTopicResponse{} + xml.Unmarshal([]byte(r), &deleteTopicResponseObject) + + assert.Equal(t, "http://queue.amazonaws.com/doc/2012-11-05/", deleteTopicResponseObject.Xmlns) + app.SyncQueues.Lock() + + defer app.SyncQueues.Unlock() + + topics := app.SyncTopics.Topics + assert.Len(t, topics, 1) + + _, ok := topics[topicName1] + assert.False(t, ok) + + _, ok = topics[topicName2] + assert.True(t, ok) + +} + +func Test_Delete_Topic_xml_NotFound(t *testing.T) { + server := generateServer() + + defer func() { + server.Close() + test.ResetResources() + }() + + requestBody := struct { + Action string `schema:"DeleteTopic"` + TopicArn string `schema:"TopicArn"` + }{ + Action: "DeleteTopic", + TopicArn: "asdf", + } + + e := httpexpect.Default(t, server.URL) + r := e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusBadRequest). + Body().Raw() + + deleteTopicErrorResponseObject := models.ErrorResponse{} + xml.Unmarshal([]byte(r), &deleteTopicErrorResponseObject) + + assert.Equal(t, deleteTopicErrorResponseObject.Result.Type, "Not Found") +} From 5555f5e63dde4d546deba9e2211c29e16d557fd8 Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Tue, 30 Jul 2024 09:42:53 +0900 Subject: [PATCH 36/41] fixed subsribe logic --- app/gosns/subscribe.go | 11 +++--- smoke_tests/sns_subscribe_test.go | 57 ++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/app/gosns/subscribe.go b/app/gosns/subscribe.go index ad8cea10..e35ab5ba 100644 --- a/app/gosns/subscribe.go +++ b/app/gosns/subscribe.go @@ -39,20 +39,19 @@ func SubscribeV1(req *http.Request) (int, interfaces.AbstractResponseBody) { subscription := &app.Subscription{EndPoint: requestBody.Endpoint, Protocol: requestBody.Protocol, TopicArn: requestBody.TopicArn, Raw: requestBody.Attributes.RawMessageDelivery, FilterPolicy: &requestBody.Attributes.FilterPolicy} - subArn := uuid.NewString() subscription.SubscriptionArn = fmt.Sprintf("%s:%s", requestBody.TopicArn, uuid.NewString()) //Create the response requestId := uuid.NewString() - respStruct := models.SubscribeResponse{Xmlns: models.BASE_XMLNS, Result: models.SubscribeResult{SubscriptionArn: subArn}, Metadata: app.ResponseMetadata{RequestId: requestId}} + respStruct := models.SubscribeResponse{Xmlns: models.BASE_XMLNS, Result: models.SubscribeResult{SubscriptionArn: subscription.SubscriptionArn}, Metadata: app.ResponseMetadata{RequestId: requestId}} if app.SyncTopics.Topics[topicName] != nil { app.SyncTopics.Lock() isDuplicate := false // Duplicate check - for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { - if subscription.EndPoint == requestBody.Endpoint && subscription.TopicArn == requestBody.TopicArn { + for _, sub := range app.SyncTopics.Topics[topicName].Subscriptions { + if sub.EndPoint == requestBody.Endpoint && sub.TopicArn == requestBody.TopicArn { isDuplicate = true - subArn = subscription.SubscriptionArn + sub.SubscriptionArn = subscription.SubscriptionArn } } if !isDuplicate { @@ -66,7 +65,7 @@ func SubscribeV1(req *http.Request) (int, interfaces.AbstractResponseBody) { token := uuid.NewString() TOPIC_DATA[requestBody.TopicArn] = &pendingConfirm{ - subArn: subArn, + subArn: subscription.SubscriptionArn, token: token, } diff --git a/smoke_tests/sns_subscribe_test.go b/smoke_tests/sns_subscribe_test.go index 16fd8dec..9a857d6e 100644 --- a/smoke_tests/sns_subscribe_test.go +++ b/smoke_tests/sns_subscribe_test.go @@ -2,6 +2,7 @@ package smoke_tests import ( "context" + "encoding/xml" "fmt" "net/http" "testing" @@ -9,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/Admiral-Piett/goaws/app/conf" + "github.com/Admiral-Piett/goaws/app/models" "github.com/Admiral-Piett/goaws/app/test" "github.com/gavv/httpexpect/v2" @@ -58,6 +60,55 @@ func Test_Subscribe_json(t *testing.T) { assert.Equal(t, "sqs", subscriptions[0].Protocol) assert.False(t, subscriptions[0].Raw) assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, response.SubscriptionArn, &subscriptions[0].SubscriptionArn) +} + +func Test_Subscribe_json_with_duplicate_subscription(t *testing.T) { + server := generateServer() + defaultEnv := app.CurrentEnvironment + conf.LoadYamlConfig("../app/conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + server.Close() + test.ResetResources() + app.CurrentEnvironment = defaultEnv + }() + + sdkConfig, _ := config.LoadDefaultConfig(context.TODO()) + sdkConfig.BaseEndpoint = aws.String(server.URL) + snsClient := sns.NewFromConfig(sdkConfig) + + snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2")), + ReturnSubscriptionArn: true, + }) + + response, err := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: aws.String("sqs"), + TopicArn: aws.String(fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")), + Attributes: map[string]string{}, + Endpoint: aws.String(fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2")), + ReturnSubscriptionArn: true, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + + app.SyncTopics.Lock() + defer app.SyncTopics.Unlock() + + subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions + assert.Len(t, subscriptions, 1) + + expectedFilterPolicy := app.FilterPolicy(nil) + assert.Equal(t, fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, "unit-queue2"), subscriptions[0].EndPoint) + assert.Equal(t, &expectedFilterPolicy, subscriptions[0].FilterPolicy) + assert.Equal(t, "sqs", subscriptions[0].Protocol) + assert.False(t, subscriptions[0].Raw) + assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, response.SubscriptionArn, &subscriptions[0].SubscriptionArn) assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) } @@ -101,6 +152,7 @@ func Test_Subscribe_json_with_additional_fields(t *testing.T) { assert.Equal(t, "sqs", subscriptions[0].Protocol) assert.True(t, subscriptions[0].Raw) assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, response.SubscriptionArn, &subscriptions[0].SubscriptionArn) assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) } @@ -128,7 +180,7 @@ func Test_Subscribe_xml(t *testing.T) { Protocol: "sqs", } - e.POST("/"). + r := e.POST("/"). WithForm(requestBody). WithFormField("Attributes.entry.1.key", "RawMessageDelivery"). WithFormField("Attributes.entry.1.value", "true"). @@ -138,6 +190,8 @@ func Test_Subscribe_xml(t *testing.T) { Status(http.StatusOK). Body().Raw() + response := models.SubscribeResponse{} + xml.Unmarshal([]byte(r), &response) subscriptions := app.SyncTopics.Topics["unit-topic2"].Subscriptions assert.Len(t, subscriptions, 1) @@ -147,5 +201,6 @@ func Test_Subscribe_xml(t *testing.T) { assert.Equal(t, "sqs", subscriptions[0].Protocol) assert.True(t, subscriptions[0].Raw) assert.Contains(t, subscriptions[0].SubscriptionArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) + assert.Equal(t, response.Result.SubscriptionArn, subscriptions[0].SubscriptionArn) assert.Equal(t, subscriptions[0].TopicArn, fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, "unit-topic2")) } From bacb1d3b3b3957f18ee9a9b290a99ec5f1ffd815 Mon Sep 17 00:00:00 2001 From: Nicholas Raffone Date: Tue, 6 Aug 2024 15:20:23 +0900 Subject: [PATCH 37/41] add sns list subcriptions support add bad request ut case to listsubscriptionsv1 rename listSubscriptions test case review ref --- app/gosns/gosns.go | 20 -- app/gosns/gosns_test.go | 59 ------ app/gosns/list_subscriptions.go | 40 ++++ app/gosns/list_subscriptions_test.go | 85 +++++++++ app/models/responses.go | 32 ++++ app/models/sns.go | 12 ++ app/router/router.go | 14 +- app/router/router_test.go | 14 +- app/sns_messages.go | 15 +- smoke_tests/sns_list_subscriptions_test.go | 208 +++++++++++++++++++++ 10 files changed, 393 insertions(+), 106 deletions(-) create mode 100644 app/gosns/list_subscriptions.go create mode 100644 app/gosns/list_subscriptions_test.go create mode 100644 smoke_tests/sns_list_subscriptions_test.go 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) +} From f06f3d7d7d355147fc683e6e86f6ae16542058e1 Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Wed, 7 Aug 2024 17:12:48 +0900 Subject: [PATCH 38/41] added get subscription attributes rename review ref review ref --- app/gosns/get_subscription_attributes.go | 69 ++++++ app/gosns/get_subscription_attributes_test.go | 121 ++++++++++ app/gosns/gosns.go | 49 ---- app/gosns/gosns_test.go | 68 ------ app/gosns/unsubscribe_test.go | 2 +- app/models/errors.go | 2 +- app/models/responses.go | 29 +++ app/models/sns.go | 11 + app/router/router.go | 16 +- app/router/router_test.go | 16 +- app/sns_messages.go | 21 -- .../sns_get_subscription_attributes_test.go | 228 ++++++++++++++++++ 12 files changed, 476 insertions(+), 156 deletions(-) create mode 100644 app/gosns/get_subscription_attributes.go create mode 100644 app/gosns/get_subscription_attributes_test.go create mode 100644 smoke_tests/sns_get_subscription_attributes_test.go diff --git a/app/gosns/get_subscription_attributes.go b/app/gosns/get_subscription_attributes.go new file mode 100644 index 00000000..2cd2e376 --- /dev/null +++ b/app/gosns/get_subscription_attributes.go @@ -0,0 +1,69 @@ +package gosns + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +func GetSubscriptionAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + + requestBody := models.NewGetSubscriptionAttributesRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + + if !ok { + log.Error("Invalid Request - GetSubscriptionAttributesV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + subscriptionArn := requestBody.SubscriptionArn + + for _, topic := range app.SyncTopics.Topics { + for _, sub := range topic.Subscriptions { + if sub.SubscriptionArn == subscriptionArn { + + entries := make([]models.SubscriptionAttributeEntry, 0, 0) + entry := models.SubscriptionAttributeEntry{Key: "Owner", Value: app.CurrentEnvironment.AccountID} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "RawMessageDelivery", Value: strconv.FormatBool(sub.Raw)} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "TopicArn", Value: sub.TopicArn} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "Endpoint", Value: sub.EndPoint} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "PendingConfirmation", Value: "false"} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "ConfirmationWasAuthenticated", Value: "true"} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "SubscriptionArn", Value: sub.SubscriptionArn} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "Protocol", Value: sub.Protocol} + entries = append(entries, entry) + + if sub.FilterPolicy != nil { + filterPolicyBytes, _ := json.Marshal(sub.FilterPolicy) + entry = models.SubscriptionAttributeEntry{Key: "FilterPolicy", Value: string(filterPolicyBytes)} + entries = append(entries, entry) + } + + result := models.GetSubscriptionAttributesResult{Attributes: models.GetSubscriptionAttributes{Entries: entries}} + uuid := uuid.NewString() + respStruct := models.GetSubscriptionAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Result: result, + Metadata: app.ResponseMetadata{RequestId: uuid}} + + return http.StatusOK, respStruct + + } + } + } + return utils.CreateErrorResponseV1("SubscriptionNotFound", false) +} diff --git a/app/gosns/get_subscription_attributes_test.go b/app/gosns/get_subscription_attributes_test.go new file mode 100644 index 00000000..91d2ef36 --- /dev/null +++ b/app/gosns/get_subscription_attributes_test.go @@ -0,0 +1,121 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "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 TestGetSubscriptionAttributesV1_NonExistentSubscription(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.GetSubscriptionAttributesRequest) + *v = models.GetSubscriptionAttributesRequest{ + SubscriptionArn: "hogehoge", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := GetSubscriptionAttributesV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + expected := models.ErrorResult{ + Type: "Not Found", + Code: "AWS.SimpleNotificationService.NonExistentSubscription", + Message: "The specified subscription does not exist for this wsdl version.", + } + assert.Equal(t, http.StatusNotFound, code) + assert.Equal(t, expected, errorResult) +} + +func TestGetSubscriptionAttributesV1_TransformError(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) { + return false + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := GetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestGetSubscriptionAttributesV1_success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + subscriptions := localTopic1.Subscriptions + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.GetSubscriptionAttributesRequest) + *v = models.GetSubscriptionAttributesRequest{ + // local-queue5 + SubscriptionArn: subscriptions[1].SubscriptionArn, + } + return true + } + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := GetSubscriptionAttributesV1(r) + + result := response.GetResult().(models.GetSubscriptionAttributesResult) + assert.Equal(t, http.StatusOK, code) + expectedAttributes := []models.SubscriptionAttributeEntry{ + { + Key: "Owner", + Value: app.CurrentEnvironment.AccountID, + }, + { + Key: "RawMessageDelivery", + Value: "true", + }, + { + Key: "TopicArn", + Value: localTopic1.Arn, + }, + { + Key: "Endpoint", + Value: subscriptions[1].EndPoint, + }, + { + Key: "PendingConfirmation", + Value: "false", + }, + { + Key: "ConfirmationWasAuthenticated", + Value: "true", + }, { + Key: "SubscriptionArn", + Value: subscriptions[1].SubscriptionArn, + }, { + Key: "Protocol", + Value: "sqs", + }, + { + Key: "FilterPolicy", + Value: "{\"foo\":[\"bar\"]}", + }, + } + + assert.ElementsMatch(t, expectedAttributes, result.Attributes.Entries) +} diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 649d824c..1a3d3b11 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "strings" "time" @@ -220,54 +219,6 @@ func SetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { createErrorResponse(w, req, "SubscriptionNotFound") } -func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { - - content := req.FormValue("ContentType") - subsArn := req.FormValue("SubscriptionArn") - - for _, topic := range app.SyncTopics.Topics { - for _, sub := range topic.Subscriptions { - if sub.SubscriptionArn == subsArn { - - entries := make([]app.SubscriptionAttributeEntry, 0, 0) - entry := app.SubscriptionAttributeEntry{Key: "Owner", Value: app.CurrentEnvironment.AccountID} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "RawMessageDelivery", Value: strconv.FormatBool(sub.Raw)} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "TopicArn", Value: sub.TopicArn} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "Endpoint", Value: sub.EndPoint} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "PendingConfirmation", Value: "false"} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "ConfirmationWasAuthenticated", Value: "true"} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "SubscriptionArn", Value: sub.SubscriptionArn} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "Protocol", Value: sub.Protocol} - entries = append(entries, entry) - entry = app.SubscriptionAttributeEntry{Key: "Endpoint", Value: sub.EndPoint} - entries = append(entries, entry) - - if sub.FilterPolicy != nil { - filterPolicyBytes, _ := json.Marshal(sub.FilterPolicy) - entry = app.SubscriptionAttributeEntry{Key: "FilterPolicy", Value: string(filterPolicyBytes)} - entries = append(entries, entry) - } - - result := app.GetSubscriptionAttributesResult{SubscriptionAttributes: app.SubscriptionAttributes{Entries: entries}} - uuid, _ := common.NewUUID() - respStruct := app.GetSubscriptionAttributesResponse{"http://sns.amazonaws.com/doc/2010-03-31", result, app.ResponseMetadata{RequestId: uuid}} - - SendResponseBack(w, req, respStruct, content) - - return - } - } - } - createErrorResponse(w, req, "SubscriptionNotFound") -} - // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index f67490aa..1a6f7c76 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -74,74 +74,6 @@ func TestListSubscriptionByTopicResponse_No_Owner(t *testing.T) { } } -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. - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - defer func() { - test.ResetApp() - }() - - topicName := "testing" - topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName - subArn, _ := common.NewUUID() - subArn = topicArn + ":" + subArn - app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ - { - SubscriptionArn: subArn, - FilterPolicy: &app.FilterPolicy{ - "foo": {"bar"}, - }, - }, - }} - - form := url.Values{} - form.Add("SubscriptionArn", subArn) - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(GetSubscriptionAttributes) - - // 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) - } - - expectedElements := []string{"Owner", "RawMessageDelivery", "TopicArn", "Endpoint", "PendingConfirmation", - "ConfirmationWasAuthenticated", "SubscriptionArn", "Protocol", "FilterPolicy"} - for _, element := range expectedElements { - expected := "" + element + "" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } - } - - // Check the response body is what we expect. - expected = "{"foo":["bar"]}" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} - func TestSetSubscriptionAttributesHandler_FilterPolicy_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/unsubscribe_test.go b/app/gosns/unsubscribe_test.go index 49bf6cfd..ddc290af 100644 --- a/app/gosns/unsubscribe_test.go +++ b/app/gosns/unsubscribe_test.go @@ -79,5 +79,5 @@ func TestUnsubscribeV1_invalid_subscription_arn(t *testing.T) { _, r := test.GenerateRequestInfo("POST", "/", nil, true) status, _ := UnsubscribeV1(r) - assert.Equal(t, http.StatusBadRequest, status) + assert.Equal(t, http.StatusNotFound, status) } diff --git a/app/models/errors.go b/app/models/errors.go index ab421a0e..deb5f68e 100644 --- a/app/models/errors.go +++ b/app/models/errors.go @@ -20,7 +20,7 @@ func init() { SnsErrors = map[string]SnsErrorType{ "InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleNotificationService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."}, "TopicNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentTopic", Message: "The specified topic does not exist for this wsdl version."}, - "SubscriptionNotFound": {HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."}, + "SubscriptionNotFound": {HttpError: http.StatusNotFound, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."}, "TopicExists": {HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleNotificationService.TopicAlreadyExists", Message: "The specified topic already exists."}, "ValidationError": {HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."}, } diff --git a/app/models/responses.go b/app/models/responses.go index d25e428b..52371566 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -493,3 +493,32 @@ func (r ListSubscriptionsResponse) GetResult() interface{} { func (r ListSubscriptionsResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** Get Subscription Attributes ***/ +type GetSubscriptionAttributesResult struct { + Attributes GetSubscriptionAttributes `xml:"Attributes,omitempty"` +} + +type GetSubscriptionAttributes struct { + /* SubscriptionArn, FilterPolicy */ + Entries []SubscriptionAttributeEntry `xml:"entry,omitempty"` +} + +type SubscriptionAttributeEntry struct { + Key string `xml:"key,omitempty"` + Value string `xml:"value,omitempty"` +} + +type GetSubscriptionAttributesResponse struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Result GetSubscriptionAttributesResult `xml:"GetSubscriptionAttributesResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata,omitempty"` +} + +func (r GetSubscriptionAttributesResponse) GetResult() interface{} { + return r.Result +} + +func (r GetSubscriptionAttributesResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index 1e38e913..1f7647ba 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -260,3 +260,14 @@ type ListSubscriptionsRequest struct { } func (r *ListSubscriptionsRequest) SetAttributesFromForm(values url.Values) {} + +// Get Subscription Attributes V1 +func NewGetSubscriptionAttributesRequest() *GetSubscriptionAttributesRequest { + return &GetSubscriptionAttributesRequest{} +} + +type GetSubscriptionAttributesRequest struct { + SubscriptionArn string `json:"SubscriptionArn" schema:"SubscriptionArn"` +} + +func (r *GetSubscriptionAttributesRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index e0a6acd8..0514a5a9 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -79,19 +79,19 @@ 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, - "ListSubscriptions": sns.ListSubscriptionsV1, + "Subscribe": sns.SubscribeV1, + "Unsubscribe": sns.UnsubscribeV1, + "Publish": sns.PublishV1, + "ListTopics": sns.ListTopicsV1, + "CreateTopic": sns.CreateTopicV1, + "DeleteTopic": sns.DeleteTopicV1, + "ListSubscriptions": sns.ListSubscriptionsV1, + "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, } var routingTable = map[string]http.HandlerFunc{ // SNS "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, - "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, // SNS Internal diff --git a/app/router/router_test.go b/app/router/router_test.go index 8c4d58b1..b1f0af0b 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -271,19 +271,19 @@ 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, - "ListSubscriptions": sns.ListSubscriptionsV1, + "Subscribe": sns.SubscribeV1, + "Unsubscribe": sns.UnsubscribeV1, + "Publish": sns.PublishV1, + "ListTopics": sns.ListTopicsV1, + "CreateTopic": sns.CreateTopicV1, + "DeleteTopic": sns.DeleteTopicV1, + "ListSubscriptions": sns.ListSubscriptionsV1, + "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, } routingTable = map[string]http.HandlerFunc{ // SNS "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, - "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, // SNS Internal diff --git a/app/sns_messages.go b/app/sns_messages.go index abb66078..7ec83f36 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -6,27 +6,6 @@ type SetSubscriptionAttributesResponse struct { Metadata ResponseMetadata `xml:"ResponseMetadata"` } -/*** Get Subscription Attributes ***/ -type GetSubscriptionAttributesResult struct { - SubscriptionAttributes SubscriptionAttributes `xml:"Attributes,omitempty"` -} - -type SubscriptionAttributes struct { - /* SubscriptionArn, FilterPolicy */ - Entries []SubscriptionAttributeEntry `xml:"entry,omitempty"` -} - -type SubscriptionAttributeEntry struct { - Key string `xml:"key,omitempty"` - Value string `xml:"value,omitempty"` -} - -type GetSubscriptionAttributesResponse struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - Result GetSubscriptionAttributesResult `xml:"GetSubscriptionAttributesResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` -} - /*** List Subscriptions By Topic Response */ type TopicMemberResult struct { diff --git a/smoke_tests/sns_get_subscription_attributes_test.go b/smoke_tests/sns_get_subscription_attributes_test.go new file mode 100644 index 00000000..efeab908 --- /dev/null +++ b/smoke_tests/sns_get_subscription_attributes_test.go @@ -0,0 +1,228 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + "strconv" + "testing" + + "github.com/Admiral-Piett/goaws/app" + af "github.com/Admiral-Piett/goaws/app/fixtures" + "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/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_GetSubscriptionAttributes_json_error_subscription_not_found(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) + + arn := "hogehoge" + input := sns.GetSubscriptionAttributesInput{ + SubscriptionArn: &arn, + } + + _, err := snsClient.GetSubscriptionAttributes(context.TODO(), &input) + + assert.Contains(t, err.Error(), strconv.Itoa(http.StatusNotFound)) + assert.Contains(t, err.Error(), "AWS.SimpleNotificationService.NonExistentSubscription") + assert.Contains(t, err.Error(), "The specified subscription does not exist for this wsdl version.") +} + +func Test_GetSubscriptionAttributes_json_success(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) + sqsClient := sqs.NewFromConfig(sdkConfig) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + topicName := "new-topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + response, _ := 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, af.QueueName)), + ReturnSubscriptionArn: true, + }) + + getSubscriptionAttributesOutput, err := snsClient.GetSubscriptionAttributes(context.TODO(), &sns.GetSubscriptionAttributesInput{ + SubscriptionArn: response.SubscriptionArn, + }) + + assert.Contains(t, getSubscriptionAttributesOutput.Attributes["Protocol"], "sqs") + assert.Contains(t, getSubscriptionAttributesOutput.Attributes["TopicArn"], fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, topicName)) + assert.Contains(t, getSubscriptionAttributesOutput.Attributes["Endpoint"], fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName)) + assert.Nil(t, err) +} + +func Test_GetSubscriptionAttributes_xml_error_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) + sqsClient := sqs.NewFromConfig(sdkConfig) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + topicName := "new-topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + 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, af.QueueName)), + ReturnSubscriptionArn: true, + }) + + e := httpexpect.Default(t, server.URL) + + getSubscriptionAttributesXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + SubscriptionArn string `xml:"SubscriptionArn"` + }{ + Action: "GetSubscriptionAttributes", + Version: "2012-11-05", + SubscriptionArn: "not-exist-arn", + } + + r := e.POST("/"). + WithForm(getSubscriptionAttributesXML). + Expect(). + Status(http.StatusNotFound). + Body().Raw() + + getSubscriptionAttributesResponse := models.GetSubscriptionAttributesResponse{} + xml.Unmarshal([]byte(r), &getSubscriptionAttributesResponse) + + assert.Nil(t, getSubscriptionAttributesResponse.Result.Attributes.Entries) + +} + +func Test_GetSubscriptionAttributes_xml_success(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) + sqsClient := sqs.NewFromConfig(sdkConfig) + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + topicName := "new-topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + response, _ := 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, af.QueueName)), + ReturnSubscriptionArn: true, + }) + + e := httpexpect.Default(t, server.URL) + + getSubscriptionAttributesXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + SubscriptionArn string `xml:"SubscriptionArn"` + }{ + Action: "GetSubscriptionAttributes", + Version: "2012-11-05", + SubscriptionArn: *response.SubscriptionArn, + } + + r := e.POST("/"). + WithForm(getSubscriptionAttributesXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + getSubscriptionAttributesResponse := models.GetSubscriptionAttributesResponse{} + xml.Unmarshal([]byte(r), &getSubscriptionAttributesResponse) + + expectedAttributes := []models.SubscriptionAttributeEntry{ + { + Key: "Owner", + Value: app.CurrentEnvironment.AccountID, + }, + { + Key: "RawMessageDelivery", + Value: "false", + }, + { + Key: "TopicArn", + Value: fmt.Sprintf("%s:%s", af.BASE_SNS_ARN, topicName), + }, + { + Key: "Endpoint", + Value: fmt.Sprintf("%s:%s", af.BASE_SQS_ARN, af.QueueName), + }, + { + Key: "PendingConfirmation", + Value: "false", + }, + { + Key: "ConfirmationWasAuthenticated", + Value: "true", + }, { + Key: "SubscriptionArn", + Value: *response.SubscriptionArn, + }, { + Key: "Protocol", + Value: "sqs", + }, + { + Key: "FilterPolicy", + Value: "null", + }, + } + + assert.ElementsMatch(t, expectedAttributes, getSubscriptionAttributesResponse.Result.Attributes.Entries) +} From ff7a55ecddbc23c22d37c393763d42e284baca81 Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Wed, 14 Aug 2024 16:58:40 +0900 Subject: [PATCH 39/41] add list subscriptions by topic v1 review ref --- app/gosns/gosns.go | 26 -- app/gosns/gosns_test.go | 61 ----- app/gosns/list_subscriptions_by_topic.go | 53 ++++ app/gosns/list_subscriptions_by_topic_test.go | 99 +++++++ app/models/responses.go | 20 ++ app/models/sns.go | 13 + app/router/router.go | 2 +- app/router/router_test.go | 2 +- app/sns_messages.go | 24 -- .../sns_list_subscriptions_by_topic_test.go | 244 ++++++++++++++++++ 10 files changed, 431 insertions(+), 113 deletions(-) create mode 100644 app/gosns/list_subscriptions_by_topic.go create mode 100644 app/gosns/list_subscriptions_by_topic_test.go create mode 100644 smoke_tests/sns_list_subscriptions_by_topic_test.go diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 1a3d3b11..93474392 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "strings" "time" "github.com/google/uuid" @@ -145,31 +144,6 @@ func ConfirmSubscription(w http.ResponseWriter, req *http.Request) { } -func ListSubscriptionsByTopic(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - topicArn := req.FormValue("TopicArn") - - uriSegments := strings.Split(topicArn, ":") - topicName := uriSegments[len(uriSegments)-1] - - if topic, ok := app.SyncTopics.Topics[topicName]; ok { - uuid, _ := common.NewUUID() - respStruct := app.ListSubscriptionsByTopicResponse{} - respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" - respStruct.Metadata.RequestId = uuid - respStruct.Result.Subscriptions.Member = make([]app.TopicMemberResult, 0, 0) - - 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) - } else { - createErrorResponse(w, req, "TopicNotFound") - } -} - func SetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { content := req.FormValue("ContentType") subsArn := req.FormValue("SubscriptionArn") diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go index 1a6f7c76..9032e4cd 100644 --- a/app/gosns/gosns_test.go +++ b/app/gosns/gosns_test.go @@ -7,73 +7,12 @@ import ( "strings" "testing" - "github.com/Admiral-Piett/goaws/app/conf" "github.com/Admiral-Piett/goaws/app/test" "github.com/Admiral-Piett/goaws/app" "github.com/Admiral-Piett/goaws/app/common" ) -// 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") - 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(ListSubscriptionsByTopic) - - // 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 TestSetSubscriptionAttributesHandler_FilterPolicy_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_by_topic.go b/app/gosns/list_subscriptions_by_topic.go new file mode 100644 index 00000000..9e8984fd --- /dev/null +++ b/app/gosns/list_subscriptions_by_topic.go @@ -0,0 +1,53 @@ +package gosns + +import ( + "net/http" + "strings" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +func ListSubscriptionsByTopicV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewListSubscriptionsByTopicRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - ListSubscriptionsByTopicV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + topicArn := requestBody.TopicArn + uriSegments := strings.Split(topicArn, ":") + topicName := uriSegments[len(uriSegments)-1] + var topic app.Topic + + if value, ok := app.SyncTopics.Topics[topicName]; ok { + topic = *value + } else { + return utils.CreateErrorResponseV1("TopicNotFound", false) + } + + resultMember := make([]models.TopicMemberResult, 0) + + for _, sub := range topic.Subscriptions { + tar := models.TopicMemberResult{TopicArn: topic.Arn, Protocol: sub.Protocol, + SubscriptionArn: sub.SubscriptionArn, Endpoint: sub.EndPoint, Owner: app.CurrentEnvironment.AccountID} + resultMember = append(resultMember, tar) + } + + respStruct := models.ListSubscriptionsByTopicResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.ListSubscriptionsByTopicResult{ + Subscriptions: models.TopicSubscriptions{ + Member: resultMember, + }, + }, + Metadata: app.ResponseMetadata{RequestId: uuid.NewString()}, + } + return http.StatusOK, respStruct + +} diff --git a/app/gosns/list_subscriptions_by_topic_test.go b/app/gosns/list_subscriptions_by_topic_test.go new file mode 100644 index 00000000..91753dce --- /dev/null +++ b/app/gosns/list_subscriptions_by_topic_test.go @@ -0,0 +1,99 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "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 TestListSubscriptionsByTopicV1_Not_Found(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.ListSubscriptionsByTopicRequest) + *v = models.ListSubscriptionsByTopicRequest{ + NextToken: "", + TopicArn: "not exist arn", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := ListSubscriptionsByTopicV1(r) + response, _ := res.(models.ListSubscriptionsByTopicResponse) + + assert.Equal(t, http.StatusBadRequest, code) + assert.Empty(t, response.Result.Subscriptions.Member) +} + +func TestListSubscriptionsByTopicV1_Transform_Error(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) { + return false + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := ListSubscriptionsByTopicV1(r) + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestListSubscriptionsByTopicV1_Success_Multiple_Subscription(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + topicArn := app.SyncTopics.Topics["local-topic1"].Arn + subscriptions := app.SyncTopics.Topics["local-topic1"].Subscriptions + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.ListSubscriptionsByTopicRequest) + *v = models.ListSubscriptionsByTopicRequest{ + NextToken: "", + TopicArn: topicArn, + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, res := ListSubscriptionsByTopicV1(r) + response, _ := res.(models.ListSubscriptionsByTopicResponse) + + assert.Equal(t, http.StatusOK, code) + assert.Len(t, response.Result.Subscriptions.Member, 2) + + expectedMember := []models.TopicMemberResult{ + { + TopicArn: subscriptions[0].TopicArn, + SubscriptionArn: subscriptions[0].SubscriptionArn, + Protocol: subscriptions[0].Protocol, + Owner: app.CurrentEnvironment.AccountID, + Endpoint: subscriptions[0].EndPoint, + }, + { + TopicArn: subscriptions[1].TopicArn, + SubscriptionArn: subscriptions[1].SubscriptionArn, + Protocol: subscriptions[1].Protocol, + Owner: app.CurrentEnvironment.AccountID, + Endpoint: subscriptions[1].EndPoint, + }, + } + + assert.ElementsMatch(t, expectedMember, response.Result.Subscriptions.Member) +} diff --git a/app/models/responses.go b/app/models/responses.go index 52371566..6bac9ca9 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -522,3 +522,23 @@ func (r GetSubscriptionAttributesResponse) GetResult() interface{} { func (r GetSubscriptionAttributesResponse) GetRequestId() string { return r.Metadata.RequestId } + +/*** List Subscriptions By Topic Response */ +type ListSubscriptionsByTopicResult struct { + NextToken string `xml:"NextToken"` // not implemented + Subscriptions TopicSubscriptions `xml:"Subscriptions"` +} + +type ListSubscriptionsByTopicResponse struct { + Xmlns string `xml:"xmlns,attr"` + Result ListSubscriptionsByTopicResult `xml:"ListSubscriptionsByTopicResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r ListSubscriptionsByTopicResponse) GetResult() interface{} { + return r.Result +} + +func (r ListSubscriptionsByTopicResponse) GetRequestId() string { + return r.Metadata.RequestId +} diff --git a/app/models/sns.go b/app/models/sns.go index 1f7647ba..021b0184 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -271,3 +271,16 @@ type GetSubscriptionAttributesRequest struct { } func (r *GetSubscriptionAttributesRequest) SetAttributesFromForm(values url.Values) {} + +// List Subscriptions By Topic + +func NewListSubscriptionsByTopicRequest() *ListSubscriptionsByTopicRequest { + return &ListSubscriptionsByTopicRequest{} +} + +type ListSubscriptionsByTopicRequest struct { + NextToken string `json:"NextToken" schema:"NextToken"` // not implemented + TopicArn string `json:"TopicArn" schema:"TopicArn"` +} + +func (r *ListSubscriptionsByTopicRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index 0514a5a9..bc38b741 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -87,12 +87,12 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "DeleteTopic": sns.DeleteTopicV1, "ListSubscriptions": sns.ListSubscriptionsV1, "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, + "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopicV1, } var routingTable = map[string]http.HandlerFunc{ // SNS "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, - "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/router/router_test.go b/app/router/router_test.go index b1f0af0b..354cde52 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -279,12 +279,12 @@ func TestActionHandler_v0_xml(t *testing.T) { "DeleteTopic": sns.DeleteTopicV1, "ListSubscriptions": sns.ListSubscriptionsV1, "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, + "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopicV1, } routingTable = map[string]http.HandlerFunc{ // SNS "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, - "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, diff --git a/app/sns_messages.go b/app/sns_messages.go index 7ec83f36..7925ddd1 100644 --- a/app/sns_messages.go +++ b/app/sns_messages.go @@ -5,27 +5,3 @@ type SetSubscriptionAttributesResponse struct { Xmlns string `xml:"xmlns,attr"` Metadata ResponseMetadata `xml:"ResponseMetadata"` } - -/*** List Subscriptions By Topic Response */ - -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 ListSubscriptionsByTopicResult struct { - Subscriptions TopicSubscriptions `xml:"Subscriptions"` -} - -type ListSubscriptionsByTopicResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result ListSubscriptionsByTopicResult `xml:"ListSubscriptionsByTopicResult"` - Metadata ResponseMetadata `xml:"ResponseMetadata"` -} diff --git a/smoke_tests/sns_list_subscriptions_by_topic_test.go b/smoke_tests/sns_list_subscriptions_by_topic_test.go new file mode 100644 index 00000000..764f258a --- /dev/null +++ b/smoke_tests/sns_list_subscriptions_by_topic_test.go @@ -0,0 +1,244 @@ +package smoke_tests + +import ( + "context" + "encoding/xml" + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + af "github.com/Admiral-Piett/goaws/app/fixtures" + "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/aws/aws-sdk-go-v2/service/sns/types" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_ListSubscriptionsByTopic_Success_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) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse1, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + createQueueResponse2, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: aws.String("new-queue-2"), + }) + + getQueueAttributesOutput1, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: createQueueResponse1.QueueUrl, + }) + + getQueueAttributesOutput2, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: createQueueResponse2.QueueUrl, + }) + + protocol := aws.String("sqs") + topicName := "new-topic-1" + createTopicResponse, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + subscribeResponse1, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: protocol, + TopicArn: createTopicResponse.TopicArn, + Attributes: map[string]string{}, + Endpoint: aws.String(getQueueAttributesOutput1.Attributes["QueueArn"]), + ReturnSubscriptionArn: true, + }) + + subscribeResponse2, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: protocol, + TopicArn: createTopicResponse.TopicArn, + Attributes: map[string]string{}, + Endpoint: aws.String(getQueueAttributesOutput2.Attributes["QueueArn"]), + ReturnSubscriptionArn: true, + }) + + listSubscriptionsByTopicOutput, _ := snsClient.ListSubscriptionsByTopic(context.TODO(), &sns.ListSubscriptionsByTopicInput{ + TopicArn: createTopicResponse.TopicArn, + }) + + assert.NotNil(t, listSubscriptionsByTopicOutput) + assert.Len(t, listSubscriptionsByTopicOutput.Subscriptions, 2) + + subscriptionMap := make(map[string]types.Subscription, 2) + for _, subscription := range listSubscriptionsByTopicOutput.Subscriptions { + subscriptionMap[*subscription.SubscriptionArn] = subscription + } + + subscription1, exists := subscriptionMap[*subscribeResponse1.SubscriptionArn] + assert.True(t, exists) + assert.Equal(t, createTopicResponse.TopicArn, subscription1.TopicArn) + assert.Equal(t, *subscribeResponse1.SubscriptionArn, *subscription1.SubscriptionArn) + assert.Equal(t, *protocol, *(subscription1.Protocol)) + assert.Equal(t, app.CurrentEnvironment.AccountID, *(subscription1.Owner)) + assert.Equal(t, getQueueAttributesOutput1.Attributes["QueueArn"], *(subscription1.Endpoint)) + + subscription2, exists := subscriptionMap[*subscribeResponse2.SubscriptionArn] + assert.True(t, exists) + assert.Equal(t, createTopicResponse.TopicArn, subscription2.TopicArn) + assert.Equal(t, subscribeResponse2.SubscriptionArn, subscription2.SubscriptionArn) + assert.Equal(t, *protocol, *(subscription2.Protocol)) + assert.Equal(t, app.CurrentEnvironment.AccountID, *(subscription2.Owner)) + assert.Equal(t, getQueueAttributesOutput2.Attributes["QueueArn"], *(subscription2.Endpoint)) +} + +func Test_ListSubscriptionsByTopic_Json_Not_Found(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) + + listSubscriptionsByTopicOutput, err := snsClient.ListSubscriptionsByTopic(context.TODO(), &sns.ListSubscriptionsByTopicInput{ + TopicArn: aws.String("not exist arn"), + }) + + assert.Nil(t, listSubscriptionsByTopicOutput) + assert.Contains(t, err.Error(), "AWS.SimpleNotificationService.NonExistentTopic") +} + +func Test_ListSubscriptionsByTopic_Xml_Success_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) + sqsClient := sqs.NewFromConfig(sdkConfig) + + createQueueResponse1, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &af.QueueName, + }) + + createQueueResponse2, _ := sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: aws.String("new-queue-2"), + }) + + getQueueAttributesOutput1, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: createQueueResponse1.QueueUrl, + }) + + getQueueAttributesOutput2, _ := sqsClient.GetQueueAttributes(context.TODO(), &sqs.GetQueueAttributesInput{ + QueueUrl: createQueueResponse2.QueueUrl, + }) + + protocol := aws.String("sqs") + topicName := "new-topic-1" + createTopicResponse, _ := snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + + subscribeResponse1, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: protocol, + TopicArn: createTopicResponse.TopicArn, + Attributes: map[string]string{}, + Endpoint: aws.String(getQueueAttributesOutput1.Attributes["QueueArn"]), + ReturnSubscriptionArn: true, + }) + + subscribeResponse2, _ := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{ + Protocol: protocol, + TopicArn: createTopicResponse.TopicArn, + Attributes: map[string]string{}, + Endpoint: aws.String(getQueueAttributesOutput2.Attributes["QueueArn"]), + ReturnSubscriptionArn: true, + }) + + requestBody := struct { + Action string `xml:"Action"` + TopicArn string `xml:"TopicArn"` + Version string `xml:"Version"` + }{ + Action: "ListSubscriptionsByTopic", + TopicArn: *createTopicResponse.TopicArn, + Version: "2012-11-05", + } + e := httpexpect.Default(t, server.URL) + + r := e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusOK). + Body().Raw() + + listSubscriptionsByTopicResponse := models.ListSubscriptionsByTopicResponse{} + xml.Unmarshal([]byte(r), &listSubscriptionsByTopicResponse) + + assert.NotNil(t, listSubscriptionsByTopicResponse) + assert.Len(t, listSubscriptionsByTopicResponse.Result.Subscriptions.Member, 2) + + expectedMember := []models.TopicMemberResult{ + { + TopicArn: *createTopicResponse.TopicArn, + SubscriptionArn: *subscribeResponse1.SubscriptionArn, + Protocol: *protocol, + Owner: app.CurrentEnvironment.AccountID, + Endpoint: getQueueAttributesOutput1.Attributes["QueueArn"], + }, + { + TopicArn: *createTopicResponse.TopicArn, + SubscriptionArn: *subscribeResponse2.SubscriptionArn, + Protocol: *protocol, + Owner: app.CurrentEnvironment.AccountID, + Endpoint: getQueueAttributesOutput2.Attributes["QueueArn"], + }, + } + + assert.ElementsMatch(t, expectedMember, listSubscriptionsByTopicResponse.Result.Subscriptions.Member) +} + +func Test_ListSubscriptionsByTopic_Xml_Not_Found(t *testing.T) { + + server := generateServer() + defer func() { + server.Close() + test.ResetResources() + }() + + e := httpexpect.Default(t, server.URL) + + requestBody := struct { + Action string `xml:"Action"` + TopicArn string `xml:"TopicArn"` + Version string `xml:"Version"` + }{ + Action: "ListSubscriptionsByTopic", + TopicArn: "not exist arn", + Version: "2012-11-05", + } + + r := e.POST("/"). + WithForm(requestBody). + Expect(). + Status(http.StatusBadRequest). + Body().Raw() + + listSubscriptionsByTopicResponse := models.ListSubscriptionsByTopicResponse{} + xml.Unmarshal([]byte(r), &listSubscriptionsByTopicResponse) + assert.Empty(t, listSubscriptionsByTopicResponse.Result.Subscriptions.Member) + +} From 8a8707aae7a66550271e7c77bee67df61e865bdd Mon Sep 17 00:00:00 2001 From: ksaiki Date: Tue, 3 Sep 2024 21:13:16 +0900 Subject: [PATCH 40/41] add set subscription attributes v1 --- app/gosns/get_subscription_attributes.go | 74 ++-- app/gosns/gosns.go | 61 +--- app/gosns/gosns_test.go | 69 ---- app/gosns/set_subscription_attributes.go | 66 ++++ app/gosns/set_subscription_attributes_test.go | 317 ++++++++++++++++++ app/models/responses.go | 14 + app/models/sns.go | 15 + app/router/router.go | 4 +- app/router/router_test.go | 4 +- .../sns_set_subscription_attributes_test.go | 161 +++++++++ 10 files changed, 620 insertions(+), 165 deletions(-) delete mode 100644 app/gosns/gosns_test.go create mode 100644 app/gosns/set_subscription_attributes.go create mode 100644 app/gosns/set_subscription_attributes_test.go create mode 100644 smoke_tests/sns_set_subscription_attributes_test.go diff --git a/app/gosns/get_subscription_attributes.go b/app/gosns/get_subscription_attributes.go index 2cd2e376..781375dc 100644 --- a/app/gosns/get_subscription_attributes.go +++ b/app/gosns/get_subscription_attributes.go @@ -23,47 +23,41 @@ func GetSubscriptionAttributesV1(req *http.Request) (int, interfaces.AbstractRes return utils.CreateErrorResponseV1("InvalidParameterValue", false) } - subscriptionArn := requestBody.SubscriptionArn - - for _, topic := range app.SyncTopics.Topics { - for _, sub := range topic.Subscriptions { - if sub.SubscriptionArn == subscriptionArn { - - entries := make([]models.SubscriptionAttributeEntry, 0, 0) - entry := models.SubscriptionAttributeEntry{Key: "Owner", Value: app.CurrentEnvironment.AccountID} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "RawMessageDelivery", Value: strconv.FormatBool(sub.Raw)} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "TopicArn", Value: sub.TopicArn} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "Endpoint", Value: sub.EndPoint} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "PendingConfirmation", Value: "false"} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "ConfirmationWasAuthenticated", Value: "true"} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "SubscriptionArn", Value: sub.SubscriptionArn} - entries = append(entries, entry) - entry = models.SubscriptionAttributeEntry{Key: "Protocol", Value: sub.Protocol} - entries = append(entries, entry) - - if sub.FilterPolicy != nil { - filterPolicyBytes, _ := json.Marshal(sub.FilterPolicy) - entry = models.SubscriptionAttributeEntry{Key: "FilterPolicy", Value: string(filterPolicyBytes)} - entries = append(entries, entry) - } + sub := getSubscription(requestBody.SubscriptionArn) + if sub == nil { + return utils.CreateErrorResponseV1("SubscriptionNotFound", false) + } - result := models.GetSubscriptionAttributesResult{Attributes: models.GetSubscriptionAttributes{Entries: entries}} - uuid := uuid.NewString() - respStruct := models.GetSubscriptionAttributesResponse{ - Xmlns: models.BASE_XMLNS, - Result: result, - Metadata: app.ResponseMetadata{RequestId: uuid}} + entries := make([]models.SubscriptionAttributeEntry, 0, 0) + entry := models.SubscriptionAttributeEntry{Key: "Owner", Value: app.CurrentEnvironment.AccountID} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "RawMessageDelivery", Value: strconv.FormatBool(sub.Raw)} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "TopicArn", Value: sub.TopicArn} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "Endpoint", Value: sub.EndPoint} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "PendingConfirmation", Value: "false"} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "ConfirmationWasAuthenticated", Value: "true"} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "SubscriptionArn", Value: sub.SubscriptionArn} + entries = append(entries, entry) + entry = models.SubscriptionAttributeEntry{Key: "Protocol", Value: sub.Protocol} + entries = append(entries, entry) + + if sub.FilterPolicy != nil { + filterPolicyBytes, _ := json.Marshal(sub.FilterPolicy) + entry = models.SubscriptionAttributeEntry{Key: "FilterPolicy", Value: string(filterPolicyBytes)} + entries = append(entries, entry) + } - return http.StatusOK, respStruct + result := models.GetSubscriptionAttributesResult{Attributes: models.GetSubscriptionAttributes{Entries: entries}} + uuid := uuid.NewString() + respStruct := models.GetSubscriptionAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Result: result, + Metadata: app.ResponseMetadata{RequestId: uuid}} - } - } - } - return utils.CreateErrorResponseV1("SubscriptionNotFound", false) + return http.StatusOK, respStruct } diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index 93474392..e920248c 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -25,7 +25,6 @@ import ( "math/big" "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/common" log "github.com/sirupsen/logrus" ) @@ -144,55 +143,6 @@ func ConfirmSubscription(w http.ResponseWriter, req *http.Request) { } -func SetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { - content := req.FormValue("ContentType") - subsArn := req.FormValue("SubscriptionArn") - Attribute := req.FormValue("AttributeName") - Value := req.FormValue("AttributeValue") - - for _, topic := range app.SyncTopics.Topics { - for _, sub := range topic.Subscriptions { - if sub.SubscriptionArn == subsArn { - if Attribute == "RawMessageDelivery" { - app.SyncTopics.Lock() - if Value == "true" { - sub.Raw = true - } else { - sub.Raw = false - } - app.SyncTopics.Unlock() - //Good Response == return - uuid, _ := common.NewUUID() - respStruct := app.SetSubscriptionAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} - SendResponseBack(w, req, respStruct, content) - return - } - - if Attribute == "FilterPolicy" { - filterPolicy := &app.FilterPolicy{} - err := json.Unmarshal([]byte(Value), filterPolicy) - if err != nil { - createErrorResponse(w, req, "ValidationError") - return - } - - app.SyncTopics.Lock() - sub.FilterPolicy = filterPolicy - app.SyncTopics.Unlock() - - //Good Response == return - uuid, _ := common.NewUUID() - respStruct := app.SetSubscriptionAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} - SendResponseBack(w, req, respStruct, content) - return - } - - } - } - } - createErrorResponse(w, req, "SubscriptionNotFound") -} - // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { @@ -277,6 +227,17 @@ func extractMessageFromJSON(msg string, protocol string) (string, error) { return defaultMsg, nil } +func getSubscription(subsArn string) *app.Subscription { + for _, topic := range app.SyncTopics.Topics { + for _, sub := range topic.Subscriptions { + if sub.SubscriptionArn == subsArn { + return sub + } + } + } + return nil +} + func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { er := models.SnsErrors[err] respStruct := models.ErrorResponse{ diff --git a/app/gosns/gosns_test.go b/app/gosns/gosns_test.go deleted file mode 100644 index 9032e4cd..00000000 --- a/app/gosns/gosns_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package gosns - -import ( - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/Admiral-Piett/goaws/app/test" - - "github.com/Admiral-Piett/goaws/app" - "github.com/Admiral-Piett/goaws/app/common" -) - -func TestSetSubscriptionAttributesHandler_FilterPolicy_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. - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } - - defer func() { - test.ResetApp() - }() - - topicName := "testing" - topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName - subArn, _ := common.NewUUID() - subArn = topicArn + ":" + subArn - app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ - { - SubscriptionArn: subArn, - }, - }} - - form := url.Values{} - form.Add("SubscriptionArn", subArn) - form.Add("AttributeName", "FilterPolicy") - form.Add("AttributeValue", "{\"foo\": [\"bar\"]}") - req.PostForm = form - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - handler := http.HandlerFunc(SetSubscriptionAttributes) - - // 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) - } - - actualFilterPolicy := app.SyncTopics.Topics[topicName].Subscriptions[0].FilterPolicy - if (*actualFilterPolicy)["foo"][0] != "bar" { - t.Errorf("filter policy has not need applied") - } -} diff --git a/app/gosns/set_subscription_attributes.go b/app/gosns/set_subscription_attributes.go new file mode 100644 index 00000000..ef285e62 --- /dev/null +++ b/app/gosns/set_subscription_attributes.go @@ -0,0 +1,66 @@ +package gosns + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +func SetSubscriptionAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewSetSubscriptionAttributesRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - SetSubscriptionAttributesV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + subsArn := requestBody.SubscriptionArn + attrName := requestBody.AttributeName + attrValue := requestBody.AttributeValue + + sub := getSubscription(subsArn) + if sub == nil { + return utils.CreateErrorResponseV1("SubscriptionNotFound", false) + } + + switch attrName { + case "RawMessageDelivery": + app.SyncTopics.Lock() + if attrValue == "true" { + sub.Raw = true + } else { + sub.Raw = false + } + app.SyncTopics.Unlock() + + case "FilterPolicy": + filterPolicy := &app.FilterPolicy{} + err := json.Unmarshal([]byte(attrValue), filterPolicy) + if err != nil { + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + app.SyncTopics.Lock() + sub.FilterPolicy = filterPolicy + app.SyncTopics.Unlock() + + case "DeliveryPolicy", "FilterPolicyScope", "RedrivePolicy", "SubscriptionRoleArn": + log.Info(fmt.Sprintf("AttributeName [%s] is valid on AWS but it is not implemented.", attrName)) + + default: + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + + uuid := uuid.NewString() + respStruct := models.SetSubscriptionAttributesResponse{ + Xmlns: models.BASE_XMLNS, + Metadata: app.ResponseMetadata{RequestId: uuid}} + + return http.StatusOK, respStruct +} diff --git a/app/gosns/set_subscription_attributes_test.go b/app/gosns/set_subscription_attributes_test.go new file mode 100644 index 00000000..d634b6e8 --- /dev/null +++ b/app/gosns/set_subscription_attributes_test.go @@ -0,0 +1,317 @@ +package gosns + +import ( + "net/http" + "testing" + + "github.com/Admiral-Piett/goaws/app" + "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 TestSetSubscriptionAttributesV1_success_SetRawMessageDelivery_true(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + assert.False(t, sub.Raw) + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "RawMessageDelivery", + AttributeValue: "true", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) + + // Assert SubscriptionAttribute has been updated + assert.True(t, sub.Raw) +} + +func TestSetSubscriptionAttributesV1_success_SetRawMessageDelivery_false(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[1] + assert.True(t, sub.Raw) + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "RawMessageDelivery", + AttributeValue: "false", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) + + // Assert SubscriptionAttribute has been updated + assert.False(t, sub.Raw) +} + +func TestSetSubscriptionAttributesV1_success_SetFilterPolicy(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + assert.Empty(t, sub.FilterPolicy) + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "FilterPolicy", + AttributeValue: "{\"foo\":[\"bar\"]}", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) + + // Assert SubscriptionAttribute has been updated + expectedFilterPolicy := make(app.FilterPolicy) + expectedFilterPolicy["foo"] = []string{"bar"} + assert.Equal(t, &expectedFilterPolicy, sub.FilterPolicy) +} + +func TestSetSubscriptionAttributesV1_error_SetFilterPolicy_invalid(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + assert.Empty(t, sub.FilterPolicy) + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "FilterPolicy", + AttributeValue: "Not a json string", // Invalid value + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} + +func TestSetSubscriptionAttributesV1_success_SetDeliveryPolicy(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "DeliveryPolicy", + AttributeValue: "foo", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) +} + +func TestSetSubscriptionAttributesV1_success_SetFilterPolicyScope(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "FilterPolicyScope", + AttributeValue: "foo", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) +} + +func TestSetSubscriptionAttributesV1_success_SetRedrivePolicy(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "SubscriptionRoleArn", + AttributeValue: "foo", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) +} + +func TestSetSubscriptionAttributesV1_success_SetSubscriptionRoleArn(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + sub := localTopic1.Subscriptions[0] + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: sub.SubscriptionArn, + AttributeName: "SubscriptionRoleArn", + AttributeValue: "foo", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusOK, code) +} + +func TestSetSubscriptionAttributesV1_error_InvalidAttribute(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "Local") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + }() + + localTopic1 := app.SyncTopics.Topics["local-topic1"] + subscriptions := localTopic1.Subscriptions + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: subscriptions[1].SubscriptionArn, + AttributeName: "InvalidAttribute", + AttributeValue: "foo", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := SetSubscriptionAttributesV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + expected := models.ErrorResult{ + Type: "InvalidParameterValue", + Code: "AWS.SimpleNotificationService.InvalidParameterValue", + Message: "An invalid or out-of-range value was supplied for the input parameter.", + } + + assert.Equal(t, http.StatusBadRequest, code) + assert.Equal(t, expected, errorResult) +} + +func TestSetSubscriptionAttributesV1_error_NonExistentSubscription(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.SetSubscriptionAttributesRequest) + *v = models.SetSubscriptionAttributesRequest{ + SubscriptionArn: "foo", + AttributeName: "RawMessageDelivery", + AttributeValue: "true", + } + return true + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := SetSubscriptionAttributesV1(r) + errorResult := response.GetResult().(models.ErrorResult) + + expected := models.ErrorResult{ + Type: "Not Found", + Code: "AWS.SimpleNotificationService.NonExistentSubscription", + Message: "The specified subscription does not exist for this wsdl version.", + } + assert.Equal(t, http.StatusNotFound, code) + assert.Equal(t, expected, errorResult) +} + +func TestSetSubscriptionAttributesV1_error_invalid_request(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, _ := SetSubscriptionAttributesV1(r) + + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/models/responses.go b/app/models/responses.go index 6bac9ca9..68dc76d9 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -523,6 +523,20 @@ func (r GetSubscriptionAttributesResponse) GetRequestId() string { return r.Metadata.RequestId } +/*** Set Subscription Attributes ***/ +type SetSubscriptionAttributesResponse struct { + Xmlns string `xml:"xmlns,attr"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +func (r SetSubscriptionAttributesResponse) GetResult() interface{} { + return nil +} + +func (r SetSubscriptionAttributesResponse) GetRequestId() string { + return r.Metadata.RequestId +} + /*** List Subscriptions By Topic Response */ type ListSubscriptionsByTopicResult struct { NextToken string `xml:"NextToken"` // not implemented diff --git a/app/models/sns.go b/app/models/sns.go index 021b0184..60f7f42d 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -272,6 +272,21 @@ type GetSubscriptionAttributesRequest struct { func (r *GetSubscriptionAttributesRequest) SetAttributesFromForm(values url.Values) {} +// SetSubscriptionAttributes + +func NewSetSubscriptionAttributesRequest() *SetSubscriptionAttributesRequest { + return &SetSubscriptionAttributesRequest{} +} + +// Ref: https://docs.aws.amazon.com/sns/latest/api/API_SetSubscriptionAttributes.html +type SetSubscriptionAttributesRequest struct { + SubscriptionArn string `json:"SubscriptionArn" schema:"SubscriptionArn"` + AttributeName string `json:"AttributeName" schema:"AttributeName"` + AttributeValue string `json:"AttributeValue" schema:"AttributeValue"` +} + +func (r *SetSubscriptionAttributesRequest) SetAttributesFromForm(values url.Values) {} + // List Subscriptions By Topic func NewListSubscriptionsByTopicRequest() *ListSubscriptionsByTopicRequest { diff --git a/app/router/router.go b/app/router/router.go index bc38b741..1d560e96 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -87,13 +87,11 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "DeleteTopic": sns.DeleteTopicV1, "ListSubscriptions": sns.ListSubscriptionsV1, "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, + "SetSubscriptionAttributes": sns.SetSubscriptionAttributesV1, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopicV1, } var routingTable = map[string]http.HandlerFunc{ - // SNS - "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, - // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, } diff --git a/app/router/router_test.go b/app/router/router_test.go index 354cde52..c209ee27 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -279,13 +279,11 @@ func TestActionHandler_v0_xml(t *testing.T) { "DeleteTopic": sns.DeleteTopicV1, "ListSubscriptions": sns.ListSubscriptionsV1, "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, + "SetSubscriptionAttributes": sns.SetSubscriptionAttributesV1, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopicV1, } routingTable = map[string]http.HandlerFunc{ - // SNS - "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, - // SNS Internal "ConfirmSubscription": sns.ConfirmSubscription, } diff --git a/smoke_tests/sns_set_subscription_attributes_test.go b/smoke_tests/sns_set_subscription_attributes_test.go new file mode 100644 index 00000000..deec2ded --- /dev/null +++ b/smoke_tests/sns_set_subscription_attributes_test.go @@ -0,0 +1,161 @@ +package smoke_tests + +import ( + "context" + "fmt" + "net/http" + "testing" + + af "github.com/Admiral-Piett/goaws/app/fixtures" + "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/aws/aws-sdk-go-v2/service/sqs" + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/assert" +) + +func Test_SetSubscriptionAttributes_json_success(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) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // Create a subscription + queueName := "new-queue-1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName, + }) + topicName := "new-topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + subResp, _ := 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, queueName)), + ReturnSubscriptionArn: true, + }) + + // Check initial attribute + getResp, err := snsClient.GetSubscriptionAttributes(context.TODO(), &sns.GetSubscriptionAttributesInput{ + SubscriptionArn: subResp.SubscriptionArn, + }) + assert.Equal(t, "false", getResp.Attributes["RawMessageDelivery"]) + assert.Nil(t, err) + + // Target test: Set attribute + attrName := "RawMessageDelivery" + attrValue := "true" + _, err = snsClient.SetSubscriptionAttributes(context.TODO(), &sns.SetSubscriptionAttributesInput{ + SubscriptionArn: subResp.SubscriptionArn, + AttributeName: &attrName, + AttributeValue: &attrValue, + }) + assert.Nil(t, err) + + // Assert the attribute has been updated + getResp, err = snsClient.GetSubscriptionAttributes(context.TODO(), &sns.GetSubscriptionAttributesInput{ + SubscriptionArn: subResp.SubscriptionArn, + }) + assert.Equal(t, "true", getResp.Attributes["RawMessageDelivery"]) + assert.Nil(t, err) +} + +func Test_SetSubscriptionAttributes_json_error_SubscriptionNotExistence(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) + + // Target test: Set attribute + subscriptionArn := "not existence sub" + attrName := "RawMessageDelivery" + attrValue := "true" + response, err := snsClient.SetSubscriptionAttributes(context.TODO(), &sns.SetSubscriptionAttributesInput{ + SubscriptionArn: &subscriptionArn, + AttributeName: &attrName, + AttributeValue: &attrValue, + }) + assert.Contains(t, err.Error(), "404") + assert.Contains(t, err.Error(), "AWS.SimpleNotificationService.NonExistentSubscription") + assert.Nil(t, response) +} + +func Test_SetSubscriptionAttributes_xml_success(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) + sqsClient := sqs.NewFromConfig(sdkConfig) + + // Create a subscription + queueName := "new-queue-1" + sqsClient.CreateQueue(context.TODO(), &sqs.CreateQueueInput{ + QueueName: &queueName, + }) + topicName := "new-topic-1" + snsClient.CreateTopic(context.TODO(), &sns.CreateTopicInput{ + Name: &topicName, + }) + subResp, _ := 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, queueName)), + ReturnSubscriptionArn: true, + }) + + // Check initial attribute + getResp, err := snsClient.GetSubscriptionAttributes(context.TODO(), &sns.GetSubscriptionAttributesInput{ + SubscriptionArn: subResp.SubscriptionArn, + }) + assert.Equal(t, "false", getResp.Attributes["RawMessageDelivery"]) + assert.Nil(t, err) + + // Target test: Set attribute + setSubscriptionAttributesXML := struct { + Action string `xml:"Action"` + Version string `xml:"Version"` + SubscriptionArn string `xml:"SubscriptionArn"` + AttributeName string `xml:"AttributeName"` + AttributeValue string `xml:"AttributeValue"` + }{ + Action: "SetSubscriptionAttributes", + Version: "2012-11-05", + SubscriptionArn: *subResp.SubscriptionArn, + AttributeName: "RawMessageDelivery", + AttributeValue: "true", + } + e := httpexpect.Default(t, server.URL) + e.POST("/"). + WithForm(setSubscriptionAttributesXML). + Expect(). + Status(http.StatusOK). + Body().Raw() + + // Assert the attribute has been updated + getResp, err = snsClient.GetSubscriptionAttributes(context.TODO(), &sns.GetSubscriptionAttributesInput{ + SubscriptionArn: subResp.SubscriptionArn, + }) + assert.Equal(t, "true", getResp.Attributes["RawMessageDelivery"]) + assert.Nil(t, err) +} From 1a058d0cdc55d2c56fbac86e62a6db723ae8da57 Mon Sep 17 00:00:00 2001 From: "Dai.Otsuka" Date: Wed, 4 Sep 2024 12:31:56 +0900 Subject: [PATCH 41/41] added confirm subscription update udpate fixed update --- app/gosns/confirm_subscription.go | 40 ++++++++ app/gosns/confirm_subscription_test.go | 125 +++++++++++++++++++++++++ app/gosns/gosns.go | 16 ---- app/models/responses.go | 18 +++- app/models/sns.go | 14 +++ app/router/router.go | 16 +--- app/router/router_test.go | 60 ------------ 7 files changed, 198 insertions(+), 91 deletions(-) create mode 100644 app/gosns/confirm_subscription.go create mode 100644 app/gosns/confirm_subscription_test.go diff --git a/app/gosns/confirm_subscription.go b/app/gosns/confirm_subscription.go new file mode 100644 index 00000000..3510d234 --- /dev/null +++ b/app/gosns/confirm_subscription.go @@ -0,0 +1,40 @@ +package gosns + +import ( + "net/http" + + "github.com/Admiral-Piett/goaws/app" + "github.com/Admiral-Piett/goaws/app/interfaces" + "github.com/Admiral-Piett/goaws/app/models" + "github.com/Admiral-Piett/goaws/app/utils" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +func ConfirmSubscriptionV1(req *http.Request) (int, interfaces.AbstractResponseBody) { + requestBody := models.NewConfirmSubscriptionRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - ConfirmSubscriptionV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", false) + } + topicArn := requestBody.TopicArn + confirmToken := requestBody.Token + var pendingConfirm pendingConfirm + + if pending, ok := TOPIC_DATA[topicArn]; !ok { + return utils.CreateErrorResponseV1("SubscriptionNotFound", false) + } else { + pendingConfirm = *pending + } + + if pendingConfirm.token != confirmToken { + return utils.CreateErrorResponseV1("SubscriptionNotFound", false) + } + respStruct := models.ConfirmSubscriptionResponse{ + Xmlns: models.BASE_XMLNS, + Result: models.ConfirmSubscriptionResult{SubscriptionArn: pendingConfirm.subArn}, + Metadata: app.ResponseMetadata{RequestId: uuid.NewString()}, + } + return http.StatusOK, respStruct +} diff --git a/app/gosns/confirm_subscription_test.go b/app/gosns/confirm_subscription_test.go new file mode 100644 index 00000000..f1b687ef --- /dev/null +++ b/app/gosns/confirm_subscription_test.go @@ -0,0 +1,125 @@ +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 TestConfirmSubscriptionV1_Success(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + TOPIC_DATA = make(map[string]*pendingConfirm) + }() + + topicArn := "test-topic-arn" + confirmToken := "test-token" + subscriptionArn := "test-sub-arn" + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.ConfirmSubscriptionRequest) + *v = models.ConfirmSubscriptionRequest{ + TopicArn: topicArn, + Token: confirmToken, + } + return true + } + // set pending subscription + TOPIC_DATA[topicArn] = &pendingConfirm{ + subArn: subscriptionArn, + token: confirmToken, + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := ConfirmSubscriptionV1(r) + + result := response.GetResult().(models.ConfirmSubscriptionResult) + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, subscriptionArn, result.SubscriptionArn) +} + +func TestConfirmSubscriptionV1_NotFoundSubscription(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "NoQueuesOrTopics") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + TOPIC_DATA = make(map[string]*pendingConfirm) + }() + + topicArn := "test-topic-arn" + confirmToken := "test-token" + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.ConfirmSubscriptionRequest) + *v = models.ConfirmSubscriptionRequest{ + TopicArn: topicArn, + Token: confirmToken, + } + return true + } + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := ConfirmSubscriptionV1(r) + result := response.GetResult().(models.ErrorResult) + assert.Equal(t, http.StatusNotFound, code) + assert.Contains(t, result.Message, "The specified subscription does not exist for this wsdl version.") +} + +func TestConfirmSubscriptionV1_MismatchToken(t *testing.T) { + + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + TOPIC_DATA = make(map[string]*pendingConfirm) + }() + + topicArn := "test-topic-arn" + confirmToken := "test-token" + subscriptionArn := "test-sub-arn" + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + v := resultingStruct.(*models.ConfirmSubscriptionRequest) + *v = models.ConfirmSubscriptionRequest{ + TopicArn: topicArn, + Token: "dummy", + } + return true + } + + // set dummy subscription + TOPIC_DATA[topicArn] = &pendingConfirm{ + subArn: subscriptionArn, + token: confirmToken, + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, response := ConfirmSubscriptionV1(r) + result := response.GetResult().(models.ErrorResult) + assert.Equal(t, http.StatusNotFound, code) + assert.Contains(t, result.Message, "The specified subscription does not exist for this wsdl version.") +} + +func TestConfirmSubscriptionV1_TransformerError(t *testing.T) { + conf.LoadYamlConfig("../conf/mock-data/mock-config.yaml", "BaseUnitTests") + defer func() { + test.ResetApp() + utils.REQUEST_TRANSFORMER = utils.TransformRequest + TOPIC_DATA = make(map[string]*pendingConfirm) + }() + + utils.REQUEST_TRANSFORMER = func(resultingStruct interfaces.AbstractRequestBody, req *http.Request, emptyRequestValid bool) (success bool) { + return false + } + + _, r := test.GenerateRequestInfo("POST", "/", nil, true) + code, _ := ConfirmSubscriptionV1(r) + assert.Equal(t, http.StatusBadRequest, code) +} diff --git a/app/gosns/gosns.go b/app/gosns/gosns.go index e920248c..02976bd3 100644 --- a/app/gosns/gosns.go +++ b/app/gosns/gosns.go @@ -8,8 +8,6 @@ import ( "net/http" "time" - "github.com/google/uuid" - "github.com/Admiral-Piett/goaws/app/models" "bytes" @@ -129,20 +127,6 @@ func formatSignature(msg *app.SNSMessage) (formated string, err error) { return } -func ConfirmSubscription(w http.ResponseWriter, req *http.Request) { - topicArn := req.Form.Get("TopicArn") - confirmToken := req.Form.Get("Token") - pendingConfirm := TOPIC_DATA[topicArn] - if pendingConfirm.token == confirmToken { - respStruct := models.ConfirmSubscriptionResponse{"http://queue.amazonaws.com/doc/2012-11-05/", models.SubscribeResult{SubscriptionArn: pendingConfirm.subArn}, app.ResponseMetadata{RequestId: uuid.NewString()}} - - SendResponseBack(w, req, respStruct, "application/xml") - } else { - createErrorResponse(w, req, "SubArnNotFound") - } - -} - // NOTE: The use case for this is to use GoAWS to call some external system with the message payload. Essentially // it is a localized subscription to some non-AWS endpoint. func callEndpoint(endpoint string, subArn string, msg app.SNSMessage, raw bool) error { diff --git a/app/models/responses.go b/app/models/responses.go index 68dc76d9..04d7b327 100644 --- a/app/models/responses.go +++ b/app/models/responses.go @@ -359,9 +359,21 @@ func (r SubscribeResponse) GetRequestId() string { /*** ConfirmSubscriptionResponse ***/ type ConfirmSubscriptionResponse struct { - Xmlns string `xml:"xmlns,attr"` - Result SubscribeResult `xml:"ConfirmSubscriptionResult"` - Metadata app.ResponseMetadata `xml:"ResponseMetadata"` + Xmlns string `xml:"xmlns,attr"` + Result ConfirmSubscriptionResult `xml:"ConfirmSubscriptionResult"` + Metadata app.ResponseMetadata `xml:"ResponseMetadata"` +} + +type ConfirmSubscriptionResult struct { + SubscriptionArn string `xml:"SubscriptionArn"` +} + +func (r ConfirmSubscriptionResponse) GetResult() interface{} { + return r.Result +} + +func (r ConfirmSubscriptionResponse) GetRequestId() string { + return r.Metadata.RequestId } /*** Delete Subscription ***/ diff --git a/app/models/sns.go b/app/models/sns.go index 60f7f42d..910192ba 100644 --- a/app/models/sns.go +++ b/app/models/sns.go @@ -299,3 +299,17 @@ type ListSubscriptionsByTopicRequest struct { } func (r *ListSubscriptionsByTopicRequest) SetAttributesFromForm(values url.Values) {} + +// Confirm Subscription V1 + +func NewConfirmSubscriptionRequest() *ConfirmSubscriptionRequest { + return &ConfirmSubscriptionRequest{} +} + +type ConfirmSubscriptionRequest struct { + AuthenticateOnUnsubscribe bool `json:"AuthenticateOnUnsubscribe" schema:"AuthenticateOnUnsubscribe"` // not implemented + TopicArn string `json:"TopicArn" schema:"TopicArn"` + Token string `json:"Token" schema:"Token"` +} + +func (r *ConfirmSubscriptionRequest) SetAttributesFromForm(values url.Values) {} diff --git a/app/router/router.go b/app/router/router.go index 1d560e96..66e184ef 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -89,11 +89,9 @@ var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractR "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, "SetSubscriptionAttributes": sns.SetSubscriptionAttributesV1, "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopicV1, -} -var routingTable = map[string]http.HandlerFunc{ // SNS Internal - "ConfirmSubscription": sns.ConfirmSubscription, + "ConfirmSubscription": sns.ConfirmSubscriptionV1, } func health(w http.ResponseWriter, req *http.Request) { @@ -115,15 +113,9 @@ func actionHandler(w http.ResponseWriter, req *http.Request) { encodeResponse(w, req, statusCode, responseBody) return } - fn, ok := routingTable[action] - if !ok { - log.Println("Bad Request - Action:", action) - w.WriteHeader(http.StatusBadRequest) - io.WriteString(w, "Bad Request") - return - } - - http.HandlerFunc(fn).ServeHTTP(w, req) + log.Println("Bad Request - Action:", action) + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "Bad Request") } func pemHandler(w http.ResponseWriter, req *http.Request) { diff --git a/app/router/router_test.go b/app/router/router_test.go index c209ee27..0a2a3fb5 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -17,8 +17,6 @@ import ( "github.com/Admiral-Piett/goaws/app/interfaces" - sns "github.com/Admiral-Piett/goaws/app/gosns" - sqs "github.com/Admiral-Piett/goaws/app/gosqs" "github.com/stretchr/testify/assert" @@ -251,61 +249,3 @@ func TestActionHandler_v1_xml(t *testing.T) { xml.Unmarshal(w.Body.Bytes(), &tmp) assert.Equal(t, mocks.BaseResponse{Message: "response-body"}, tmp) } - -func TestActionHandler_v0_xml(t *testing.T) { - defer func() { - routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - // SQS - "CreateQueue": sqs.CreateQueueV1, - "ListQueues": sqs.ListQueuesV1, - "GetQueueAttributes": sqs.GetQueueAttributesV1, - "SetQueueAttributes": sqs.SetQueueAttributesV1, - "SendMessage": sqs.SendMessageV1, - "ReceiveMessage": sqs.ReceiveMessageV1, - "DeleteMessage": sqs.DeleteMessageV1, - "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, - "GetQueueUrl": sqs.GetQueueUrlV1, - "PurgeQueue": sqs.PurgeQueueV1, - "DeleteQueue": sqs.DeleteQueueV1, - "SendMessageBatch": sqs.SendMessageBatchV1, - "DeleteMessageBatch": sqs.DeleteMessageBatchV1, - - // SNS - "Subscribe": sns.SubscribeV1, - "Unsubscribe": sns.UnsubscribeV1, - "Publish": sns.PublishV1, - "ListTopics": sns.ListTopicsV1, - "CreateTopic": sns.CreateTopicV1, - "DeleteTopic": sns.DeleteTopicV1, - "ListSubscriptions": sns.ListSubscriptionsV1, - "GetSubscriptionAttributes": sns.GetSubscriptionAttributesV1, - "SetSubscriptionAttributes": sns.SetSubscriptionAttributesV1, - "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopicV1, - } - - routingTable = map[string]http.HandlerFunc{ - // SNS Internal - "ConfirmSubscription": sns.ConfirmSubscription, - } - }() - - mockCalled := false - mockFunction := func(w http.ResponseWriter, req *http.Request) { - mockCalled = true - w.WriteHeader(http.StatusOK) - } - routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){} - routingTable = map[string]http.HandlerFunc{ - "CreateQueue": mockFunction, - } - - w, r := test.GenerateRequestInfo("POST", "/url", nil, false) - form := url.Values{} - form.Add("Action", "CreateQueue") - r.PostForm = form - - actionHandler(w, r) - - assert.True(t, mockCalled) - assert.Equal(t, http.StatusOK, w.Code) -}