From 89d95f33800ce4884e494ee9215bf3d9c1082009 Mon Sep 17 00:00:00 2001 From: harryzcy Date: Sat, 27 May 2023 19:25:48 +0800 Subject: [PATCH] Refactor thread related code (#269) --- api/emails/create/main.go | 7 +- api/emails/delete/delete.go | 7 +- api/emails/get/main.go | 9 +- api/emails/getContent/main.go | 7 +- api/emails/getRaw/main.go | 7 +- api/emails/list/main.go | 7 +- api/emails/read/main.go | 7 +- api/emails/save/main.go | 7 +- api/emails/send/main.go | 7 +- api/emails/trash/trash.go | 7 +- api/emails/untrash/untrash.go | 7 +- api/threads/get/main.go | 10 +- functions/emailReceive/main.go | 11 +- functions/emailRestore/main.go | 15 +- integration/sepup_test.go | 16 +- integration/thread_test.go | 25 +-- internal/datasource/storage/dynamodb.go | 8 +- internal/datasource/storage/dynamodb_test.go | 5 +- internal/datasource/storage/s3.go | 14 +- internal/datasource/storage/s3_test.go | 13 +- internal/datasource/storage/sqs.go | 10 +- internal/datasource/storage/sqs_test.go | 11 +- internal/email/create.go | 23 +- internal/email/create_test.go | 9 +- internal/email/delete.go | 3 +- internal/email/delete_test.go | 5 +- internal/email/email.go | 10 +- internal/email/email_test.go | 2 +- internal/email/get.go | 15 +- internal/email/get_test.go | 9 +- internal/email/list_query.go | 5 +- internal/email/list_query_test.go | 9 +- internal/email/read.go | 3 +- internal/email/save.go | 5 +- internal/email/save_test.go | 8 +- internal/email/send.go | 9 +- internal/email/send_test.go | 3 +- internal/email/trash.go | 3 +- internal/email/untrash.go | 3 +- internal/env/env.go | 14 ++ internal/{email => thread}/thread.go | 83 ++++---- internal/{email => thread}/thread_test.go | 210 +++++++++---------- internal/util/idutil/idutil.go | 11 + internal/util/mockutil/mockutil.go | 32 +++ 44 files changed, 339 insertions(+), 352 deletions(-) create mode 100644 internal/env/env.go rename internal/{email => thread}/thread.go (84%) rename internal/{email => thread}/thread_test.go (55%) create mode 100644 internal/util/idutil/idutil.go create mode 100644 internal/util/mockutil/mockutil.go diff --git a/api/emails/create/main.go b/api/emails/create/main.go index 80b367c5..808a0383 100644 --- a/api/emails/create/main.go +++ b/api/emails/create/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -15,12 +14,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/sesv2" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - type createClient struct { dynamodbSvc *dynamodb.Client sesv2Svd *sesv2.Client @@ -59,7 +56,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(400, "invalid input"), nil diff --git a/api/emails/delete/delete.go b/api/emails/delete/delete.go index 275059bd..41b7c34c 100644 --- a/api/emails/delete/delete.go +++ b/api/emails/delete/delete.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -14,12 +13,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - type deleteClient struct { cfg aws.Config } @@ -38,7 +35,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/get/main.go b/api/emails/get/main.go index 16d6c964..9b6a3d29 100644 --- a/api/emails/get/main.go +++ b/api/emails/get/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -13,19 +12,17 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil @@ -38,7 +35,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R return apiutil.NewErrorResponse(http.StatusBadRequest, "bad request: invalid messageID"), nil } - result, err := email.Get(ctx, dynamodb.NewFromConfig(cfg), messageID) + result, err := email.GetAndRead(ctx, dynamodb.NewFromConfig(cfg), messageID) if err != nil { if err == email.ErrNotFound { fmt.Println("email not found") diff --git a/api/emails/getContent/main.go b/api/emails/getContent/main.go index 7d8fe482..26076c4b 100644 --- a/api/emails/getContent/main.go +++ b/api/emails/getContent/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "os" "strings" "time" @@ -13,19 +12,17 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/getRaw/main.go b/api/emails/getRaw/main.go index 56de0626..ed469887 100644 --- a/api/emails/getRaw/main.go +++ b/api/emails/getRaw/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "os" "strings" "time" @@ -14,19 +13,17 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/harryzcy/mailbox/internal/datasource/storage" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/list/main.go b/api/emails/list/main.go index 0c3c5160..23929889 100644 --- a/api/emails/list/main.go +++ b/api/emails/list/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "strconv" "time" @@ -14,19 +13,17 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/read/main.go b/api/emails/read/main.go index 860e3f1e..caf024af 100644 --- a/api/emails/read/main.go +++ b/api/emails/read/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "os" "strings" "time" @@ -13,19 +12,17 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/save/main.go b/api/emails/save/main.go index 4dd4aff4..5d03d9d4 100644 --- a/api/emails/save/main.go +++ b/api/emails/save/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -15,12 +14,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/sesv2" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - type saveClient struct { dynamodbSvc *dynamodb.Client sesv2Svc *sesv2.Client @@ -55,7 +52,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/send/main.go b/api/emails/send/main.go index 991c43df..f60d5c03 100644 --- a/api/emails/send/main.go +++ b/api/emails/send/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -15,12 +14,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/sesv2" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - type sendClient struct { dynamodbSvc *dynamodb.Client sesv2Svc *sesv2.Client @@ -54,7 +51,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R messageID := req.PathParameters["messageID"] fmt.Printf("request params: [messagesID] %s\n", messageID) - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/trash/trash.go b/api/emails/trash/trash.go index 9cb33d55..1773a302 100644 --- a/api/emails/trash/trash.go +++ b/api/emails/trash/trash.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -12,19 +11,17 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/emails/untrash/untrash.go b/api/emails/untrash/untrash.go index 82b330e2..7d40a845 100644 --- a/api/emails/untrash/untrash.go +++ b/api/emails/untrash/untrash.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -12,19 +11,17 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil diff --git a/api/threads/get/main.go b/api/threads/get/main.go index 2bb5b9bd..9df1f668 100644 --- a/api/threads/get/main.go +++ b/api/threads/get/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "time" "github.com/aws/aws-lambda-go/events" @@ -13,19 +12,18 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" + "github.com/harryzcy/mailbox/internal/thread" "github.com/harryzcy/mailbox/internal/util/apiutil" ) -// AWS Region -var region = os.Getenv("REGION") - func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() fmt.Println("request received") - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { fmt.Printf("unable to load SDK config, %v\n", err) return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil @@ -38,7 +36,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R return apiutil.NewErrorResponse(http.StatusBadRequest, "bad request: invalid threadID"), nil } - result, err := email.GetThreadWithEmails(ctx, dynamodb.NewFromConfig(cfg), threadID) + result, err := thread.GetThreadWithEmails(ctx, dynamodb.NewFromConfig(cfg), threadID) if err != nil { if err == email.ErrNotFound { fmt.Println("thread not found") diff --git a/functions/emailReceive/main.go b/functions/emailReceive/main.go index 4603bdeb..722b0523 100644 --- a/functions/emailReceive/main.go +++ b/functions/emailReceive/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "strings" "time" @@ -17,13 +16,11 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/harryzcy/mailbox/internal/datasource/storage" - "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" + "github.com/harryzcy/mailbox/internal/thread" "github.com/harryzcy/mailbox/internal/util/format" ) -// AWS Region -var region = os.Getenv("REGION") - func main() { lambda.Start(handler) } @@ -43,7 +40,7 @@ func receiveEmail(ctx context.Context, ses events.SimpleEmailService) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { log.Fatalf("unable to load SDK config, %v", err) } @@ -98,7 +95,7 @@ func receiveEmail(ctx context.Context, ses events.SimpleEmailService) { log.Printf("subject: %v", ses.Mail.CommonHeaders.Subject) - email.StoreEmail(ctx, dynamodb.NewFromConfig(cfg), &email.StoreEmailInput{ + thread.StoreEmail(ctx, dynamodb.NewFromConfig(cfg), &thread.StoreEmailInput{ Item: item, InReplyTo: inReplyTo, References: references, diff --git a/functions/emailRestore/main.go b/functions/emailRestore/main.go index 5038188f..82d5da27 100644 --- a/functions/emailRestore/main.go +++ b/functions/emailRestore/main.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "log" - "os" "regexp" "strings" "time" @@ -25,14 +24,10 @@ import ( "github.com/jhillyerd/enmime" "github.com/harryzcy/mailbox/internal/datasource/storage" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" ) -// AWS Region -var region = os.Getenv("REGION") -var tableName = os.Getenv("TABLE_NAME") -var s3Bucket = os.Getenv("S3_BUCKET") - func main() { lambda.Start(handler) } @@ -43,7 +38,7 @@ type client struct { } func handler(ctx context.Context, sqsEvent events.SQSEvent) (events.SQSEventResponse, error) { - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(env.Region)) if err != nil { log.Fatalf("unable to load SDK config, %v", err) } @@ -76,7 +71,7 @@ func restoreEmail(ctx context.Context, cli *client, messageID string) error { item := make(map[string]dynamodbTypes.AttributeValue) getResp, err := cli.dynamoDBClient.GetItem(ctx, &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: messageID}, }, @@ -90,7 +85,7 @@ func restoreEmail(ctx context.Context, cli *client, messageID string) error { } object, err := cli.s3Client.GetObject(ctx, &s3.GetObjectInput{ - Bucket: &s3Bucket, + Bucket: &env.S3Bucket, Key: &messageID, }) fmt.Println("got object from s3, err:", err) @@ -137,7 +132,7 @@ func restoreEmail(ctx context.Context, cli *client, messageID string) error { item["Inlines"] = storage.ParseFiles(envelope.Inlines).ToAttributeValue() resp, err := cli.dynamoDBClient.PutItem(ctx, &dynamodb.PutItemInput{ - TableName: &tableName, + TableName: &env.TableName, ConditionExpression: aws.String("attribute_not_exists(MessageID)"), Item: item, }) diff --git a/integration/sepup_test.go b/integration/sepup_test.go index c50a431c..aab4094e 100644 --- a/integration/sepup_test.go +++ b/integration/sepup_test.go @@ -3,7 +3,6 @@ package integration import ( "context" "log" - "os" "testing" "github.com/aws/aws-sdk-go-v2/aws" @@ -11,11 +10,10 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" ) var ( - tableName = os.Getenv("DYNAMODB_TABLE") - client *dynamodb.Client ) @@ -57,7 +55,7 @@ func tableExists(d *dynamodb.Client) bool { log.Fatal("ListTables failed", err) } for _, n := range tables.TableNames { - if n == tableName { + if n == env.TableName { return true } } @@ -66,7 +64,7 @@ func tableExists(d *dynamodb.Client) bool { func createTableIfNotExists(d *dynamodb.Client) { if tableExists(d) { - log.Printf("table=%v already exists\n", tableName) + log.Printf("table=%v already exists\n", env.TableName) return } _, err := d.CreateTable(context.TODO(), &dynamodb.CreateTableInput{ @@ -131,25 +129,25 @@ func createTableIfNotExists(d *dynamodb.Client) { }, }, }, - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), BillingMode: types.BillingModePayPerRequest, }) if err != nil { log.Fatal("CreateTable failed", err) } - log.Printf("created table=%v\n", tableName) + log.Printf("created table=%v\n", env.TableName) } func deleteAllItems() { resp, err := client.Scan(context.TODO(), &dynamodb.ScanInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), }) if err != nil { log.Fatal("Scan failed", err) } for _, item := range resp.Items { _, err := client.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{"MessageID": item["MessageID"]}, }) if err != nil { diff --git a/integration/thread_test.go b/integration/thread_test.go index ab907ae2..6e312d06 100644 --- a/integration/thread_test.go +++ b/integration/thread_test.go @@ -7,7 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" + "github.com/harryzcy/mailbox/internal/thread" "github.com/stretchr/testify/assert" ) @@ -18,7 +19,7 @@ func TestStoreEmails(t *testing.T) { func testEmptyTable(t *testing.T) int { resp, err := client.Scan(context.TODO(), &dynamodb.ScanInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), }) assert.NoError(t, err) assert.Equal(t, 0, len(resp.Items)) @@ -33,7 +34,7 @@ func testStoreEmails_NoThread(t *testing.T) { } // first email - email.StoreEmail(context.TODO(), client, &email.StoreEmailInput{ + thread.StoreEmail(context.TODO(), client, &thread.StoreEmailInput{ Item: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: "1"}, "OriginalMessageID": &types.AttributeValueMemberS{Value: "1@example.com"}, @@ -43,7 +44,7 @@ func testStoreEmails_NoThread(t *testing.T) { TimeReceived: "2023-02-01T00:00:00Z", }) // second email, no In-Reply-To or References - email.StoreEmail(context.TODO(), client, &email.StoreEmailInput{ + thread.StoreEmail(context.TODO(), client, &thread.StoreEmailInput{ Item: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: "2"}, "OriginalMessageID": &types.AttributeValueMemberS{Value: "2@example.com"}, @@ -53,7 +54,7 @@ func testStoreEmails_NoThread(t *testing.T) { TimeReceived: "2023-02-01T00:00:00Z", }) // third email, with In-Reply-To and References, but they don't exist - email.StoreEmail(context.TODO(), client, &email.StoreEmailInput{ + thread.StoreEmail(context.TODO(), client, &thread.StoreEmailInput{ Item: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: "3"}, "OriginalMessageID": &types.AttributeValueMemberS{Value: "3@example.com"}, @@ -75,7 +76,7 @@ func testStoreEmails_BasicThread(t *testing.T) { return } - email.StoreEmail(context.TODO(), client, &email.StoreEmailInput{ + thread.StoreEmail(context.TODO(), client, &thread.StoreEmailInput{ Item: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: "1"}, "OriginalMessageID": &types.AttributeValueMemberS{Value: "1@example.com"}, @@ -89,7 +90,7 @@ func testStoreEmails_BasicThread(t *testing.T) { testItemNoAttribute(t, "1", "IsThreadLatest") // no thread yet // should create a new thread - email.StoreEmail(context.TODO(), client, &email.StoreEmailInput{ + thread.StoreEmail(context.TODO(), client, &thread.StoreEmailInput{ Item: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: "2"}, "OriginalMessageID": &types.AttributeValueMemberS{Value: "2@example.com"}, @@ -106,7 +107,7 @@ func testStoreEmails_BasicThread(t *testing.T) { testItemHasAttribute(t, "2", "IsThreadLatest", &types.AttributeValueMemberBOOL{Value: true}) // should add to the same thread - email.StoreEmail(context.TODO(), client, &email.StoreEmailInput{ + thread.StoreEmail(context.TODO(), client, &thread.StoreEmailInput{ Item: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: "3"}, "OriginalMessageID": &types.AttributeValueMemberS{Value: "3@example.com"}, @@ -127,7 +128,7 @@ func testStoreEmails_BasicThread(t *testing.T) { testItemHasAttribute(t, "3", "IsThreadLatest", &types.AttributeValueMemberBOOL{Value: true}) resp, err := client.Scan(context.TODO(), &dynamodb.ScanInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), }) assert.NoError(t, err) @@ -163,7 +164,7 @@ func testStoreEmails_BasicThread(t *testing.T) { func testItemExists(t *testing.T, messageID string) { resp, err := client.GetItem(context.TODO(), &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, @@ -174,7 +175,7 @@ func testItemExists(t *testing.T, messageID string) { func testItemNoAttribute(t *testing.T, messageID, attribute string) { resp, err := client.GetItem(context.TODO(), &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, @@ -186,7 +187,7 @@ func testItemNoAttribute(t *testing.T, messageID, attribute string) { func testItemHasAttribute(t *testing.T, messageID, attribute string, value types.AttributeValue) { resp, err := client.GetItem(context.TODO(), &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, diff --git a/internal/datasource/storage/dynamodb.go b/internal/datasource/storage/dynamodb.go index 3b8b3854..e9222588 100644 --- a/internal/datasource/storage/dynamodb.go +++ b/internal/datasource/storage/dynamodb.go @@ -2,14 +2,10 @@ package storage import ( "context" - "os" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" -) - -var ( - tableName = os.Getenv("DYNAMODB_TABLE") + "github.com/harryzcy/mailbox/internal/env" ) // DynamoDBStorage is an interface that defines required DynamoDB functions @@ -30,7 +26,7 @@ type DynamoDBPutItemAPI interface { // StoreInDynamoDB stores data in DynamoDB func (s dynamodbStorage) Store(ctx context.Context, api DynamoDBPutItemAPI, item map[string]types.AttributeValue) error { _, err := api.PutItem(ctx, &dynamodb.PutItemInput{ - TableName: &tableName, + TableName: &env.TableName, Item: item, }) if err != nil { diff --git a/internal/datasource/storage/dynamodb_test.go b/internal/datasource/storage/dynamodb_test.go index 3e026da6..14dbab75 100644 --- a/internal/datasource/storage/dynamodb_test.go +++ b/internal/datasource/storage/dynamodb_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/stretchr/testify/assert" ) @@ -18,7 +19,7 @@ func (m mockPutItemAPI) PutItem(ctx context.Context, params *dynamodb.PutItemInp } func TestDynamoDB_Store(t *testing.T) { - tableName = "example-table" + env.TableName = "example-table" tests := []struct { client func(t *testing.T, item map[string]types.AttributeValue) DynamoDBPutItemAPI item map[string]types.AttributeValue @@ -29,7 +30,7 @@ func TestDynamoDB_Store(t *testing.T) { return mockPutItemAPI( func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { t.Helper() - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) assert.Equal(t, item, params.Item) return &dynamodb.PutItemOutput{}, nil }) diff --git a/internal/datasource/storage/s3.go b/internal/datasource/storage/s3.go index f6fd2bcf..f43ad166 100644 --- a/internal/datasource/storage/s3.go +++ b/internal/datasource/storage/s3.go @@ -3,17 +3,13 @@ package storage import ( "context" "io" - "os" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/types" "github.com/jhillyerd/enmime" ) -var ( - s3Bucket = os.Getenv("S3_BUCKET") -) - type GetEmailResult struct { Text string HTML string @@ -45,7 +41,7 @@ type S3GetObjectAPI interface { // GetEmail retrieves an email from s3 bucket func (s s3Storage) GetEmail(ctx context.Context, api S3GetObjectAPI, messageID string) (*GetEmailResult, error) { object, err := api.GetObject(ctx, &s3.GetObjectInput{ - Bucket: &s3Bucket, + Bucket: &env.S3Bucket, Key: &messageID, }) if err != nil { @@ -68,7 +64,7 @@ func (s s3Storage) GetEmail(ctx context.Context, api S3GetObjectAPI, messageID s // GetEmailRaw retrieves raw MIME email from s3 bucket func (s s3Storage) GetEmailRaw(ctx context.Context, api S3GetObjectAPI, messageID string) ([]byte, error) { object, err := api.GetObject(ctx, &s3.GetObjectInput{ - Bucket: &s3Bucket, + Bucket: &env.S3Bucket, Key: &messageID, }) if err != nil { @@ -88,7 +84,7 @@ type GetEmailContentResult struct { // GetEmailContent retrieved the attachment of inline of an email from s3 bucket func (s s3Storage) GetEmailContent(ctx context.Context, api S3GetObjectAPI, messageID, disposition, contentID string) (*GetEmailContentResult, error) { object, err := api.GetObject(ctx, &s3.GetObjectInput{ - Bucket: &s3Bucket, + Bucket: &env.S3Bucket, Key: &messageID, }) if err != nil { @@ -133,7 +129,7 @@ type S3DeleteObjectAPI interface { // DeleteEmail deletes an email from S3 bucket func (s s3Storage) DeleteEmail(ctx context.Context, api S3DeleteObjectAPI, messageID string) error { _, err := api.DeleteObject(ctx, &s3.DeleteObjectInput{ - Bucket: &s3Bucket, + Bucket: &env.S3Bucket, Key: &messageID, }) if err != nil { diff --git a/internal/datasource/storage/s3_test.go b/internal/datasource/storage/s3_test.go index 44411f3c..d0ea1894 100644 --- a/internal/datasource/storage/s3_test.go +++ b/internal/datasource/storage/s3_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/harryzcy/mailbox/internal/env" "github.com/jhillyerd/enmime" "github.com/stretchr/testify/assert" ) @@ -20,7 +21,7 @@ func (m mockGetObjectAPI) GetObject(ctx context.Context, params *s3.GetObjectInp } func TestS3_GetEmail(t *testing.T) { - s3Bucket = "test_bucket" + env.S3Bucket = "test_bucket" cases := []struct { client func(t *testing.T) S3GetObjectAPI @@ -35,7 +36,7 @@ func TestS3_GetEmail(t *testing.T) { return mockGetObjectAPI(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { t.Helper() assert.NotNil(t, params.Bucket, "expect bucket to not be nil") - assert.Equal(t, s3Bucket, *params.Bucket) + assert.Equal(t, env.S3Bucket, *params.Bucket) assert.NotNil(t, params.Key, "expect key to not be nil") assert.Equal(t, "exampleMessageID", *params.Key) @@ -93,7 +94,7 @@ func TestS3_GetEmail(t *testing.T) { } func TestS3_GetEmailRaw(t *testing.T) { - s3Bucket = "test_bucket" + env.S3Bucket = "test_bucket" cases := []struct { client func(t *testing.T) S3GetObjectAPI @@ -106,7 +107,7 @@ func TestS3_GetEmailRaw(t *testing.T) { return mockGetObjectAPI(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { t.Helper() assert.NotNil(t, params.Bucket, "expect bucket to not be nil") - assert.Equal(t, s3Bucket, *params.Bucket) + assert.Equal(t, env.S3Bucket, *params.Bucket) assert.NotNil(t, params.Key, "expect key to not be nil") assert.Equal(t, "exampleMessageID", *params.Key) @@ -149,7 +150,7 @@ func (m mockDeleteObjectAPI) DeleteObject(ctx context.Context, params *s3.Delete } func TestS3_DeleteEmail(t *testing.T) { - s3Bucket = "test_bucket" + env.S3Bucket = "test_bucket" tests := []struct { client func(t *testing.T) S3DeleteObjectAPI messageID string @@ -160,7 +161,7 @@ func TestS3_DeleteEmail(t *testing.T) { return mockDeleteObjectAPI(func(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) { t.Helper() assert.NotNil(t, params.Bucket, "expect bucket to not be nil") - assert.Equal(t, s3Bucket, *params.Bucket) + assert.Equal(t, env.S3Bucket, *params.Bucket) assert.NotNil(t, params.Key, "expect key to not be nil") assert.Equal(t, "exampleMessageID", *params.Key) diff --git a/internal/datasource/storage/sqs.go b/internal/datasource/storage/sqs.go index eaffe2cf..24a6d621 100644 --- a/internal/datasource/storage/sqs.go +++ b/internal/datasource/storage/sqs.go @@ -4,15 +4,11 @@ import ( "context" "encoding/json" "fmt" - "os" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sqs/types" -) - -var ( - queueName = os.Getenv("SQS_QUEUE") + "github.com/harryzcy/mailbox/internal/env" ) // SQSStorage references all SQS related functions @@ -43,7 +39,7 @@ type EmailReceipt struct { } func (s sqsStorage) Enabled() bool { - return queueName != "" + return env.QueueName != "" } // SendEmailHandle sends an email receipt to SQS. @@ -67,7 +63,7 @@ type EmailNotification struct { // SendEmailNotification notifies about a change of state of an email, categorized by event. func (s sqsStorage) SendEmailNotification(ctx context.Context, api SQSSendMessageAPI, input EmailNotification) error { result, err := api.GetQueueUrl(ctx, &sqs.GetQueueUrlInput{ - QueueName: &queueName, + QueueName: &env.QueueName, }) if err != nil { fmt.Println("Failed to get queue url") diff --git a/internal/datasource/storage/sqs_test.go b/internal/datasource/storage/sqs_test.go index f13ab788..52fc9e0e 100644 --- a/internal/datasource/storage/sqs_test.go +++ b/internal/datasource/storage/sqs_test.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sqs/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/stretchr/testify/assert" ) @@ -26,15 +27,15 @@ func (m mockSQSSendMessageAPI) SendMessage(ctx context.Context, params *sqs.Send } func TestSQSEnabled(t *testing.T) { - queueName = "test-queue-TestSQSEnabled" + env.QueueName = "test-queue-TestSQSEnabled" assert.True(t, SQS.Enabled()) - queueName = "" + env.QueueName = "" assert.False(t, SQS.Enabled()) } func TestSQSSendMessageAPI(t *testing.T) { - queueName = "test-queue-TestSQSSendMessageAPI" + env.QueueName = "test-queue-TestSQSSendMessageAPI" tests := []struct { client func(t *testing.T) SQSSendMessageAPI input EmailReceipt @@ -73,7 +74,7 @@ func TestSQSSendMessageAPI(t *testing.T) { } func TestSendEmailNotification(t *testing.T) { - queueName = "test-queue-TestSendEmailNotification" + env.QueueName = "test-queue-TestSendEmailNotification" tests := []struct { client func(t *testing.T) SQSSendMessageAPI input EmailNotification @@ -84,7 +85,7 @@ func TestSendEmailNotification(t *testing.T) { return mockSQSSendMessageAPI{ mockGetQueueUrl: func(ctx context.Context, params *sqs.GetQueueUrlInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueUrlOutput, error) { t.Helper() - assert.Equal(t, queueName, *params.QueueName) + assert.Equal(t, env.QueueName, *params.QueueName) return &sqs.GetQueueUrlOutput{ QueueUrl: aws.String("https://queue.url"), diff --git a/internal/email/create.go b/internal/email/create.go index b8468e85..2c731b79 100644 --- a/internal/email/create.go +++ b/internal/email/create.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "os" "strings" "time" @@ -12,12 +11,12 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/google/uuid" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" "github.com/harryzcy/mailbox/internal/util/htmlutil" + "github.com/harryzcy/mailbox/internal/util/idutil" ) -var region = os.Getenv("REGION") - // CreateInput represents the input of create method type CreateInput struct { EmailInput @@ -108,13 +107,13 @@ func Create(ctx context.Context, api CreateAndSendEmailAPI, input CreateInput) ( TransactItems: []types.TransactWriteItem{ { Put: &types.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: item, }, }, { Update: &types.Update{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": item["ThreadID"], }, @@ -138,7 +137,7 @@ func Create(ctx context.Context, api CreateAndSendEmailAPI, input CreateInput) ( // 1) put the email, // 2) create a new thread with DraftID, // 3) add ThreadID to the previous email - threadID = generateThreadID() + threadID = idutil.GenerateThreadID() item["ThreadID"] = &types.AttributeValueMemberS{Value: threadID} t := time.Now().UTC() @@ -164,19 +163,19 @@ func Create(ctx context.Context, api CreateAndSendEmailAPI, input CreateInput) ( TransactItems: []types.TransactWriteItem{ { Put: &types.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: item, }, }, { Put: &types.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: thread, }, }, { Update: &types.Update{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: info.CreatingEmailID}, }, @@ -203,7 +202,7 @@ func Create(ctx context.Context, api CreateAndSendEmailAPI, input CreateInput) ( } else { // is not part of the thread, so we can just put the email _, err = api.PutItem(ctx, &dynamodb.PutItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: item, }) if err != nil { @@ -278,7 +277,7 @@ type ThreadInfo struct { func getThreadInfo(ctx context.Context, api CreateAndSendEmailAPI, replyEmailID string) (*ThreadInfo, error) { fmt.Println("getting email to reply to") - email, err := get(ctx, api, replyEmailID) + email, err := Get(ctx, api, replyEmailID) if err != nil { return nil, err } @@ -286,7 +285,7 @@ func getThreadInfo(ctx context.Context, api CreateAndSendEmailAPI, replyEmailID if email.Type == EmailTypeInbox { replyToMessageID = email.OriginalMessageID } else if email.Type == EmailTypeSent { - replyToMessageID = fmt.Sprintf("%s@%s.amazonses.com", email.MessageID, region) + replyToMessageID = fmt.Sprintf("%s@%s.amazonses.com", email.MessageID, env.Region) } else { return nil, errors.New("invalid email type") } diff --git a/internal/email/create_test.go b/internal/email/create_test.go index f93c30e3..8d76e9a7 100644 --- a/internal/email/create_test.go +++ b/internal/email/create_test.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/sesv2" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/htmlutil" "github.com/stretchr/testify/assert" ) @@ -44,7 +45,7 @@ func TestCreate(t *testing.T) { getUpdatedTime = func() time.Time { return time.Date(2022, 3, 16, 16, 55, 45, 0, time.UTC) } defer func() { getUpdatedTime = oldGetUpdatedTime }() - tableName = "table-for-create" + env.TableName = "table-for-create" tests := []struct { client func(t *testing.T) CreateAndSendEmailAPI input CreateInput @@ -58,7 +59,7 @@ func TestCreate(t *testing.T) { mockPutItem: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { t.Helper() - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) messageID := params.Item["MessageID"].(*types.AttributeValueMemberS).Value assert.Len(t, messageID, 6+32) @@ -223,7 +224,7 @@ func TestCreate(t *testing.T) { for _, item := range params.TransactItems { if item.Delete != nil { assert.Nil(t, item.Put) - assert.Equal(t, tableName, *item.Delete.TableName) + assert.Equal(t, env.TableName, *item.Delete.TableName) messageID := item.Delete.Key["MessageID"].(*types.AttributeValueMemberS).Value assert.Len(t, messageID, 6+32) @@ -231,7 +232,7 @@ func TestCreate(t *testing.T) { } if item.Put != nil { assert.Nil(t, item.Delete) - assert.Equal(t, tableName, *item.Put.TableName) + assert.Equal(t, env.TableName, *item.Put.TableName) messageID := item.Put.Item["MessageID"].(*types.AttributeValueMemberS).Value assert.Equal(t, "sent-message-id", messageID) diff --git a/internal/email/delete.go b/internal/email/delete.go index 8c0c6494..764fb541 100644 --- a/internal/email/delete.go +++ b/internal/email/delete.go @@ -10,13 +10,14 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/harryzcy/mailbox/internal/datasource/storage" + "github.com/harryzcy/mailbox/internal/env" ) // Delete deletes an trashed email from DynamoDB and S3. // This action won't be successful if it's not trashed. func Delete(ctx context.Context, api DeleteItemAPI, messageID string) error { _, err := api.DeleteItem(ctx, &dynamodb.DeleteItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, diff --git a/internal/email/delete_test.go b/internal/email/delete_test.go index 6f53a7a1..07c66296 100644 --- a/internal/email/delete_test.go +++ b/internal/email/delete_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/harryzcy/mailbox/internal/env" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -26,7 +27,7 @@ func (m mockDeleteItemAPI) DeleteObject(ctx context.Context, params *s3.DeleteOb } func TestDelete(t *testing.T) { - tableName = "table-for-delete" + env.TableName = "table-for-delete" tests := []struct { client func(t *testing.T) DeleteItemAPI messageID string @@ -38,7 +39,7 @@ func TestDelete(t *testing.T) { mockDeleteItem: func(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { t.Helper() - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) assert.Len(t, params.Key, 1) assert.IsType(t, params.Key["MessageID"], &types.AttributeValueMemberS{}) assert.Equal(t, diff --git a/internal/email/email.go b/internal/email/email.go index e8ebb482..f49af3e5 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -2,20 +2,12 @@ package email import ( "fmt" - "os" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/harryzcy/mailbox/internal/util/format" ) -var ( - // tableName represents DynamoDB table name - tableName = os.Getenv("DYNAMODB_TABLE") - // gsiIndexName represents DynamoDB's GSI name - gsiIndexName = os.Getenv("DYNAMODB_TIME_INDEX") -) - // The constants representing email types const ( // EmailTypeInbox represents an inbox email @@ -76,7 +68,7 @@ func (gsi GSIIndex) ToTimeIndex() (*TimeIndex, error) { return index, nil } -func unmarshalGSI(item map[string]types.AttributeValue) (emailType, emailTime string, err error) { +func UnmarshalGSI(item map[string]types.AttributeValue) (emailType, emailTime string, err error) { var typeYearMonth string var dt string // date-time err = attributevalue.Unmarshal(item["TypeYearMonth"], &typeYearMonth) diff --git a/internal/email/email_test.go b/internal/email/email_test.go index ba90e92e..f53bbc82 100644 --- a/internal/email/email_test.go +++ b/internal/email/email_test.go @@ -75,7 +75,7 @@ func TestUnmarshalGSI(t *testing.T) { for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { - emailType, timeReceived, err := unmarshalGSI(test.items) + emailType, timeReceived, err := UnmarshalGSI(test.items) assert.Equal(t, test.expectedEmailType, emailType) assert.Equal(t, test.expectedTimeReceived, timeReceived) if test.expectedTargetErr == nil { diff --git a/internal/email/get.go b/internal/email/get.go index b87c7779..ed02b1b5 100644 --- a/internal/email/get.go +++ b/internal/email/get.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb" dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/types" ) @@ -59,8 +60,8 @@ type EmailVerdict struct { } // Get returns the email and marks it as read -func Get(ctx context.Context, api GetEmailAPI, messageID string) (*GetResult, error) { - result, err := get(ctx, api, messageID) +func GetAndRead(ctx context.Context, api GetEmailAPI, messageID string) (*GetResult, error) { + result, err := Get(ctx, api, messageID) if err != nil { return nil, err } @@ -78,9 +79,9 @@ func Get(ctx context.Context, api GetEmailAPI, messageID string) (*GetResult, er } // get returns the email -func get(ctx context.Context, api GetItemAPI, messageID string) (*GetResult, error) { +func Get(ctx context.Context, api GetItemAPI, messageID string) (*GetResult, error) { resp, err := api.GetItem(ctx, &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: messageID}, }, @@ -105,7 +106,7 @@ func get(ctx context.Context, api GetItemAPI, messageID string) (*GetResult, err } } - result, err := parseGetResult(resp.Item) + result, err := ParseGetResult(resp.Item) if err != nil { return nil, err } @@ -114,7 +115,7 @@ func get(ctx context.Context, api GetItemAPI, messageID string) (*GetResult, err return result, nil } -func parseGetResult(attributeValues map[string]dynamodbTypes.AttributeValue) (*GetResult, error) { +func ParseGetResult(attributeValues map[string]dynamodbTypes.AttributeValue) (*GetResult, error) { result := new(GetResult) err := attributevalue.UnmarshalMap(attributeValues, result) if err != nil { @@ -122,7 +123,7 @@ func parseGetResult(attributeValues map[string]dynamodbTypes.AttributeValue) (*G } var emailTime string - result.Type, emailTime, err = unmarshalGSI(attributeValues) + result.Type, emailTime, err = UnmarshalGSI(attributeValues) if err != nil { return nil, err } diff --git a/internal/email/get_test.go b/internal/email/get_test.go index 9a13f990..148d9c3e 100644 --- a/internal/email/get_test.go +++ b/internal/email/get_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/stretchr/testify/assert" ) @@ -17,8 +18,8 @@ func (m mockGetItemAPI) GetItem(ctx context.Context, params *dynamodb.GetItemInp return m(ctx, params, optFns...) } -func Test_get(t *testing.T) { - tableName = "table-for-get" +func TestGet(t *testing.T) { + env.TableName = "table-for-get" tests := []struct { client func(t *testing.T) GetItemAPI messageID string @@ -30,7 +31,7 @@ func Test_get(t *testing.T) { return mockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { t.Helper() assert.NotNil(t, params.TableName) - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) assert.Len(t, params.Key, 1) assert.IsType(t, params.Key["MessageID"], &types.AttributeValueMemberS{}) @@ -143,7 +144,7 @@ func Test_get(t *testing.T) { for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { ctx := context.TODO() - result, err := get(ctx, test.client(t), test.messageID) + result, err := Get(ctx, test.client(t), test.messageID) assert.Equal(t, test.expected, result) assert.Equal(t, test.expectedErr, err) }) diff --git a/internal/email/list_query.go b/internal/email/list_query.go index fc9783bb..c632f3e2 100644 --- a/internal/email/list_query.go +++ b/internal/email/list_query.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" ) // listQueryInput represents the inputs for listByYearMonth function @@ -45,8 +46,8 @@ func listByYearMonth(ctx context.Context, api QueryAPI, input listQueryInput) (l } queryInput := &dynamodb.QueryInput{ - TableName: &tableName, - IndexName: &gsiIndexName, + TableName: &env.TableName, + IndexName: &env.GsiIndexName, ExclusiveStartKey: input.lastEvaluatedKey, KeyConditionExpression: aws.String("#tym = :val"), ExpressionAttributeValues: map[string]types.AttributeValue{ diff --git a/internal/email/list_query_test.go b/internal/email/list_query_test.go index 6d5c8626..afcb5b21 100644 --- a/internal/email/list_query_test.go +++ b/internal/email/list_query_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" "github.com/stretchr/testify/assert" ) @@ -19,8 +20,8 @@ func (m mockQueryAPI) Query(ctx context.Context, params *dynamodb.QueryInput, op } func TestByYearMonth(t *testing.T) { - tableName = "list-by-year-month-table-name" - gsiIndexName = "gsi-index-name" + env.TableName = "list-by-year-month-table-name" + env.GsiIndexName = "gsi-index-name" tests := []struct { client func(t *testing.T) QueryAPI unmarshalListOfMaps func(l []map[string]types.AttributeValue, out interface{}) error @@ -33,8 +34,8 @@ func TestByYearMonth(t *testing.T) { return mockQueryAPI(func(ctx context.Context, params *dynamodb.QueryInput, optFns ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) { t.Helper() - assert.Equal(t, tableName, *params.TableName) - assert.Equal(t, gsiIndexName, *params.IndexName) + assert.Equal(t, env.TableName, *params.TableName) + assert.Equal(t, env.GsiIndexName, *params.IndexName) assert.Equal(t, map[string]types.AttributeValue{ "foo": &types.AttributeValueMemberS{Value: "bar"}, }, params.ExclusiveStartKey) diff --git a/internal/email/read.go b/internal/email/read.go index 2c5920b1..88a03478 100644 --- a/internal/email/read.go +++ b/internal/email/read.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" ) const ( @@ -18,7 +19,7 @@ const ( // Read marks an email as read or unread func Read(ctx context.Context, api UpdateItemAPI, messageID, action string) error { input := &dynamodb.UpdateItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, diff --git a/internal/email/save.go b/internal/email/save.go index c5e536f7..bb65d8c0 100644 --- a/internal/email/save.go +++ b/internal/email/save.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" ) @@ -62,7 +63,7 @@ func Save(ctx context.Context, api SaveAndSendEmailAPI, input SaveInput) (*SaveR // but rather they are initialized when creating the draft email. // So we need to get the original values from DynamoDB, and keep them in the item. resp, err := api.GetItem(ctx, &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: input.MessageID}, }, @@ -85,7 +86,7 @@ func Save(ctx context.Context, api SaveAndSendEmailAPI, input SaveInput) (*SaveR } _, err = api.PutItem(ctx, &dynamodb.PutItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: item, ConditionExpression: aws.String("MessageID = :messageID"), ExpressionAttributeValues: map[string]types.AttributeValue{ diff --git a/internal/email/save_test.go b/internal/email/save_test.go index cd38a375..994a28b3 100644 --- a/internal/email/save_test.go +++ b/internal/email/save_test.go @@ -10,7 +10,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/sesv2" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/htmlutil" + "github.com/harryzcy/mailbox/internal/util/mockutil" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -23,7 +25,7 @@ var ( type mockSaveEmailAPI struct { mockGetItem mockGetItemAPI mockPutItem func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) - mockTransactWriteItem mockTransactWriteItemAPI + mockTransactWriteItem mockutil.MockTransactWriteItemAPI mockSendEmail func(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) } @@ -52,7 +54,7 @@ func TestSave(t *testing.T) { getUpdatedTime = func() time.Time { return time.Date(2022, 3, 16, 16, 55, 45, 0, time.UTC) } defer func() { getUpdatedTime = oldGetUpdatedTime }() - tableName = "table-for-save" + env.TableName = "table-for-save" tests := []struct { client func(t *testing.T) SaveAndSendEmailAPI input SaveInput @@ -71,7 +73,7 @@ func TestSave(t *testing.T) { mockPutItem: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { t.Helper() - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) messageID := params.Item["MessageID"].(*types.AttributeValueMemberS).Value assert.Equal(t, "draft-example", messageID) diff --git a/internal/email/send.go b/internal/email/send.go index 5ef853af..48bf8a97 100644 --- a/internal/email/send.go +++ b/internal/email/send.go @@ -13,6 +13,7 @@ import ( dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/sesv2" sestypes "github.com/aws/aws-sdk-go-v2/service/sesv2/types" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" "github.com/jhillyerd/enmime" ) @@ -27,7 +28,7 @@ func Send(ctx context.Context, api GetAndSendEmailAPI, messageID string) (*SendR return nil, ErrEmailIsNotDraft } - resp, err := get(ctx, api, messageID) + resp, err := Get(ctx, api, messageID) if err != nil { return nil, err } @@ -142,7 +143,7 @@ func markEmailAsSent(ctx context.Context, api SendEmailAPI, oldMessageID string, TransactItems: []dynamodbTypes.TransactWriteItem{ { Delete: &dynamodbTypes.Delete{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: oldMessageID}, }, @@ -150,7 +151,7 @@ func markEmailAsSent(ctx context.Context, api SendEmailAPI, oldMessageID string, }, { Put: &dynamodbTypes.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: item, }, }, @@ -163,7 +164,7 @@ func markEmailAsSent(ctx context.Context, api SendEmailAPI, oldMessageID string, fmt.Println("include thread update") input.TransactItems = append(input.TransactItems, dynamodbTypes.TransactWriteItem{ Update: &dynamodbTypes.Update{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: email.ThreadID}, }, diff --git a/internal/email/send_test.go b/internal/email/send_test.go index eda274e2..6bfbecda 100644 --- a/internal/email/send_test.go +++ b/internal/email/send_test.go @@ -11,12 +11,13 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/sesv2" + "github.com/harryzcy/mailbox/internal/util/mockutil" "github.com/stretchr/testify/assert" ) type mockSendEmailAPI struct { mockGetItem mockGetItemAPI - mockTransactWriteItem mockTransactWriteItemAPI + mockTransactWriteItem mockutil.MockTransactWriteItemAPI mockSendEmail func(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) } diff --git a/internal/email/trash.go b/internal/email/trash.go index 764ced9a..cce06e42 100644 --- a/internal/email/trash.go +++ b/internal/email/trash.go @@ -9,12 +9,13 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" ) // Trash marks an email as trashed func Trash(ctx context.Context, api UpdateItemAPI, messageID string) error { _, err := api.UpdateItem(ctx, &dynamodb.UpdateItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, diff --git a/internal/email/untrash.go b/internal/email/untrash.go index 604135aa..db88cc4e 100644 --- a/internal/email/untrash.go +++ b/internal/email/untrash.go @@ -8,12 +8,13 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/harryzcy/mailbox/internal/env" ) // Untrash marks an trashed email as not trashed func Untrash(ctx context.Context, api UpdateItemAPI, messageID string) error { _, err := api.UpdateItem(ctx, &dynamodb.UpdateItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]types.AttributeValue{ "MessageID": &types.AttributeValueMemberS{Value: messageID}, }, diff --git a/internal/env/env.go b/internal/env/env.go new file mode 100644 index 00000000..1aa70c2d --- /dev/null +++ b/internal/env/env.go @@ -0,0 +1,14 @@ +package env + +import "os" + +var ( + // AWS Region + Region = os.Getenv("REGION") + + TableName = os.Getenv("DYNAMODB_TABLE") + GsiOriginalIndexName = os.Getenv("DYNAMODB_ORIGINAL_INDEX") + GsiIndexName = os.Getenv("DYNAMODB_TIME_INDEX") + S3Bucket = os.Getenv("S3_BUCKET") + QueueName = os.Getenv("SQS_QUEUE") +) diff --git a/internal/email/thread.go b/internal/thread/thread.go similarity index 84% rename from internal/email/thread.go rename to internal/thread/thread.go index 6bef969a..9383c5f1 100644 --- a/internal/email/thread.go +++ b/internal/thread/thread.go @@ -1,11 +1,10 @@ -package email +package thread import ( "context" "errors" "fmt" "log" - "os" "strings" "time" @@ -13,13 +12,11 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb" dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/google/uuid" "github.com/harryzcy/mailbox/internal/datasource/storage" + "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" -) - -var ( - gsiOriginalIndexName = os.Getenv("DYNAMODB_ORIGINAL_INDEX") + "github.com/harryzcy/mailbox/internal/util/idutil" ) type Thread struct { @@ -30,33 +27,33 @@ type Thread struct { DraftID string `json:"draftID,omitempty"` TimeUpdated string `json:"timeUpdated"` // The time the last email is received or sent - Emails []GetResult `json:"emails,omitempty"` - Draft *GetResult `json:"draft,omitempty"` + Emails []email.GetResult `json:"emails,omitempty"` + Draft *email.GetResult `json:"draft,omitempty"` } -func GetThread(ctx context.Context, api GetItemAPI, messageID string) (*Thread, error) { +func GetThread(ctx context.Context, api email.GetItemAPI, messageID string) (*Thread, error) { resp, err := api.GetItem(ctx, &dynamodb.GetItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: messageID}, }, }) if err != nil { if apiErr := new(dynamodbTypes.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { - return nil, ErrTooManyRequests + return nil, email.ErrTooManyRequests } return nil, err } if len(resp.Item) == 0 { - return nil, ErrNotFound + return nil, email.ErrNotFound } - emailType, _, err := unmarshalGSI(resp.Item) + emailType, _, err := email.UnmarshalGSI(resp.Item) if err != nil { return nil, err } if emailType != "thread" { - return nil, ErrNotFound + return nil, email.ErrNotFound } result := &Thread{ @@ -70,7 +67,7 @@ func GetThread(ctx context.Context, api GetItemAPI, messageID string) (*Thread, return result, nil } -func GetThreadWithEmails(ctx context.Context, api GetThreadWithEmailsAPI, messageID string) (*Thread, error) { +func GetThreadWithEmails(ctx context.Context, api email.GetThreadWithEmailsAPI, messageID string) (*Thread, error) { thread, err := GetThread(ctx, api, messageID) if err != nil { return nil, err @@ -90,14 +87,14 @@ func GetThreadWithEmails(ctx context.Context, api GetThreadWithEmailsAPI, messag resp, err := api.BatchGetItem(ctx, &dynamodb.BatchGetItemInput{ RequestItems: map[string]dynamodbTypes.KeysAndAttributes{ - tableName: { + env.TableName: { Keys: keys, }, }, }) if err != nil { if apiErr := new(dynamodbTypes.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { - return nil, ErrTooManyRequests + return nil, email.ErrTooManyRequests } return nil, err } @@ -107,10 +104,10 @@ func GetThreadWithEmails(ctx context.Context, api GetThreadWithEmailsAPI, messag orderMap[emailID] = i } - thread.Emails = make([]GetResult, len(thread.EmailIDs)) + thread.Emails = make([]email.GetResult, len(thread.EmailIDs)) - for _, item := range resp.Responses[tableName] { - email, err := parseGetResult(item) + for _, item := range resp.Responses[env.TableName] { + email, err := email.ParseGetResult(item) if err != nil { return nil, err } @@ -144,7 +141,7 @@ type DetermineThreadOutput struct { // DetermineThread determines which thread an incoming email belongs to. // If a thread already exists, the ThreadID is returned and Exists is true. // If a thread does not exist and a new thread should be created, the ThreadID is randomly generated and ShouldCreate is true. -func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *DetermineThreadInput) (*DetermineThreadOutput, error) { +func DetermineThread(ctx context.Context, api email.QueryAndGetItemAPI, input *DetermineThreadInput) (*DetermineThreadOutput, error) { fmt.Println("Determining thread...") originalMessageID := "" if len(input.InReplyTo) > 0 { @@ -158,7 +155,7 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi return &DetermineThreadOutput{}, nil } - sesDomain := region + ".amazonses.com" + sesDomain := env.Region + ".amazonses.com" var possibleSentID string if strings.HasSuffix(originalMessageID, "@"+sesDomain+">") { fmt.Println("incoming email is replying to a SES email") @@ -168,14 +165,14 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi possibleSentID = strings.TrimPrefix(possibleSentID, "<") } - var previousEmail *GetResult + var previousEmail *email.GetResult var err error isSentEmail := false if possibleSentID != "" { // Check if the messageID is a sent email first fmt.Println("checking possible sent email") - previousEmail, err = get(ctx, api, possibleSentID) - if err != nil && !errors.Is(err, ErrNotFound) { + previousEmail, err = email.Get(ctx, api, possibleSentID) + if err != nil && !errors.Is(err, email.ErrNotFound) { return nil, err } isSentEmail = true @@ -186,8 +183,8 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi fmt.Println("checking original messageID") var resp *dynamodb.QueryOutput resp, err = api.Query(ctx, &dynamodb.QueryInput{ - TableName: aws.String(tableName), - IndexName: aws.String(gsiOriginalIndexName), + TableName: aws.String(env.TableName), + IndexName: aws.String(env.GsiOriginalIndexName), KeyConditionExpression: aws.String("OriginalMessageID = :originalMessageID"), ExpressionAttributeValues: map[string]dynamodbTypes.AttributeValue{ ":originalMessageID": &dynamodbTypes.AttributeValueMemberS{Value: originalMessageID}, @@ -195,7 +192,7 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi }) if err != nil { if apiErr := new(dynamodbTypes.ProvisionedThroughputExceededException); errors.As(err, &apiErr) { - return nil, ErrTooManyRequests + return nil, email.ErrTooManyRequests } return nil, err } @@ -205,9 +202,9 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi } searchMessageID := resp.Items[0]["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value - previousEmail, err = get(ctx, api, searchMessageID) + previousEmail, err = email.Get(ctx, api, searchMessageID) if err != nil { - if errors.Is(err, ErrNotFound) { + if errors.Is(err, email.ErrNotFound) { return &DetermineThreadOutput{}, nil } return nil, err @@ -217,7 +214,7 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi if previousEmail.ThreadID == "" { // There's no thread for previousEmail, so we need to create a new thread fmt.Println("determining thread finished: new thread should be created") - threadID := generateThreadID() + threadID := idutil.GenerateThreadID() output := &DetermineThreadOutput{ ThreadID: threadID, ShouldCreate: true, @@ -253,10 +250,6 @@ func DetermineThread(ctx context.Context, api QueryAndGetItemAPI, input *Determi }, nil } -func generateThreadID() string { - return strings.ReplaceAll(uuid.NewString(), "-", "") -} - type StoreEmailWithExistingThreadInput struct { ThreadID string Email map[string]dynamodbTypes.AttributeValue @@ -265,21 +258,21 @@ type StoreEmailWithExistingThreadInput struct { } // StoreEmailWithExistingThread stores the email and updates the thread. -func StoreEmailWithExistingThread(ctx context.Context, api TransactWriteItemsAPI, input *StoreEmailWithExistingThreadInput) error { +func StoreEmailWithExistingThread(ctx context.Context, api email.TransactWriteItemsAPI, input *StoreEmailWithExistingThreadInput) error { input.Email["IsThreadLatest"] = &dynamodbTypes.AttributeValueMemberBOOL{Value: true} _, err := api.TransactWriteItems(ctx, &dynamodb.TransactWriteItemsInput{ TransactItems: []dynamodbTypes.TransactWriteItem{ { // Store new email Put: &dynamodbTypes.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: input.Email, }, }, { // Update the thread Update: &dynamodbTypes.Update{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: input.ThreadID}, }, @@ -297,7 +290,7 @@ func StoreEmailWithExistingThread(ctx context.Context, api TransactWriteItemsAPI { // Remove IsThreadLatest from the previous email Update: &dynamodbTypes.Update{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: input.PreviousMessageID}, }, @@ -323,7 +316,7 @@ type StoreEmailWithNewThreadInput struct { } // StoreEmailWithNewThread stores the email, creates a new thread, and add ThreadID to previous email -func StoreEmailWithNewThread(ctx context.Context, api TransactWriteItemsAPI, input *StoreEmailWithNewThreadInput) error { +func StoreEmailWithNewThread(ctx context.Context, api email.TransactWriteItemsAPI, input *StoreEmailWithNewThreadInput) error { t, err := time.Parse(time.RFC3339, input.CreatingTime) if err != nil { return err @@ -352,7 +345,7 @@ func StoreEmailWithNewThread(ctx context.Context, api TransactWriteItemsAPI, inp { // Set ThreadID to previous email Update: &dynamodbTypes.Update{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Key: map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: input.CreatingEmailID}, }, @@ -368,14 +361,14 @@ func StoreEmailWithNewThread(ctx context.Context, api TransactWriteItemsAPI, inp { // Store the new email Put: &dynamodbTypes.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: input.Email, }, }, { // Create the new thread Put: &dynamodbTypes.Put{ - TableName: aws.String(tableName), + TableName: aws.String(env.TableName), Item: thread, }, }, @@ -395,7 +388,7 @@ type StoreEmailInput struct { } // StoreEmail attempts to store the email. If error occurs, it will be logged and the function will return. -func StoreEmail(ctx context.Context, api StoreEmailAPI, input *StoreEmailInput) { +func StoreEmail(ctx context.Context, api email.StoreEmailAPI, input *StoreEmailInput) { output, err := DetermineThread(ctx, api, &DetermineThreadInput{ InReplyTo: input.InReplyTo, References: input.References, diff --git a/internal/email/thread_test.go b/internal/thread/thread_test.go similarity index 55% rename from internal/email/thread_test.go rename to internal/thread/thread_test.go index 7a4a7db6..e88d87ac 100644 --- a/internal/email/thread_test.go +++ b/internal/thread/thread_test.go @@ -1,4 +1,4 @@ -package email +package thread import ( "context" @@ -7,47 +7,50 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/smithy-go/middleware" + "github.com/harryzcy/mailbox/internal/email" + "github.com/harryzcy/mailbox/internal/env" "github.com/harryzcy/mailbox/internal/util/format" + "github.com/harryzcy/mailbox/internal/util/idutil" + "github.com/harryzcy/mailbox/internal/util/mockutil" "github.com/stretchr/testify/assert" ) func TestGetThread(t *testing.T) { - tableName = "table-for-get-thread" + env.TableName = "table-for-get-thread" tests := []struct { - client func(t *testing.T) GetItemAPI + client func(t *testing.T) email.GetItemAPI messageID string expected *Thread expectedErr error }{ { - client: func(t *testing.T) GetItemAPI { - return mockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + client: func(t *testing.T) email.GetItemAPI { + return mockutil.MockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { t.Helper() assert.NotNil(t, params.TableName) - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) assert.Len(t, params.Key, 1) - assert.IsType(t, params.Key["MessageID"], &types.AttributeValueMemberS{}) + assert.IsType(t, params.Key["MessageID"], &dynamodbTypes.AttributeValueMemberS{}) assert.Equal(t, - params.Key["MessageID"].(*types.AttributeValueMemberS).Value, + params.Key["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value, "exampleMessageID", ) return &dynamodb.GetItemOutput{ - Item: map[string]types.AttributeValue{ + Item: map[string]dynamodbTypes.AttributeValue{ "MessageID": params.Key["MessageID"], - "TypeYearMonth": &types.AttributeValueMemberS{Value: "thread#2023-02"}, - "Subject": &types.AttributeValueMemberS{Value: "subject"}, - "EmailIDs": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberS{Value: "id-1"}, - &types.AttributeValueMemberS{Value: "id-2"}, + "TypeYearMonth": &dynamodbTypes.AttributeValueMemberS{Value: "thread#2023-02"}, + "Subject": &dynamodbTypes.AttributeValueMemberS{Value: "subject"}, + "EmailIDs": &dynamodbTypes.AttributeValueMemberL{ + Value: []dynamodbTypes.AttributeValue{ + &dynamodbTypes.AttributeValueMemberS{Value: "id-1"}, + &dynamodbTypes.AttributeValueMemberS{Value: "id-2"}, }, }, - "TimeUpdated": &types.AttributeValueMemberS{Value: "2022-03-12T01:01:01Z"}, + "TimeUpdated": &dynamodbTypes.AttributeValueMemberS{Value: "2022-03-12T01:01:01Z"}, }, }, nil }) @@ -62,27 +65,27 @@ func TestGetThread(t *testing.T) { }, }, { - client: func(t *testing.T) GetItemAPI { - return mockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + client: func(t *testing.T) email.GetItemAPI { + return mockutil.MockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{ - Item: map[string]types.AttributeValue{ + Item: map[string]dynamodbTypes.AttributeValue{ "MessageID": params.Key["MessageID"], - "TypeYearMonth": &types.AttributeValueMemberS{Value: "inbox#2023-02"}, + "TypeYearMonth": &dynamodbTypes.AttributeValueMemberS{Value: "inbox#2023-02"}, }, }, nil }) }, messageID: "exampleMessageID", expected: nil, - expectedErr: ErrNotFound, + expectedErr: email.ErrNotFound, }, { - client: func(t *testing.T) GetItemAPI { - return mockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + client: func(t *testing.T) email.GetItemAPI { + return mockutil.MockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{ - Item: map[string]types.AttributeValue{ + Item: map[string]dynamodbTypes.AttributeValue{ "MessageID": params.Key["MessageID"], - "TypeYearMonth": &types.AttributeValueMemberS{Value: "invalid#2023-02"}, + "TypeYearMonth": &dynamodbTypes.AttributeValueMemberS{Value: "invalid#2023-02"}, }, }, nil }) @@ -92,8 +95,8 @@ func TestGetThread(t *testing.T) { expectedErr: format.ErrInvalidEmailType, }, { - client: func(t *testing.T) GetItemAPI { - return mockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + client: func(t *testing.T) email.GetItemAPI { + return mockutil.MockGetItemAPI(func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{ Item: nil, }, nil @@ -101,7 +104,7 @@ func TestGetThread(t *testing.T) { }, messageID: "exampleMessageID", expected: nil, - expectedErr: ErrNotFound, + expectedErr: email.ErrNotFound, }, } @@ -115,89 +118,76 @@ func TestGetThread(t *testing.T) { } } -type mockGetThreadWithEmailsAPI struct { - mockGetItem mockGetItemAPI - mockBatchGetItem func(ctx context.Context, params *dynamodb.BatchGetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchGetItemOutput, error) -} - -func (m mockGetThreadWithEmailsAPI) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { - return m.mockGetItem(ctx, params, optFns...) -} - -func (m mockGetThreadWithEmailsAPI) BatchGetItem(ctx context.Context, params *dynamodb.BatchGetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchGetItemOutput, error) { - return m.mockBatchGetItem(ctx, params, optFns...) -} - func TestGetThreadWithEmails(t *testing.T) { - tableName = "table-for-get-thread-with-emails" + env.TableName = "table-for-get-thread-with-emails" tests := []struct { - client func(t *testing.T) GetThreadWithEmailsAPI + client func(t *testing.T) email.GetThreadWithEmailsAPI messageID string expected *Thread expectedErr error }{ { - client: func(t *testing.T) GetThreadWithEmailsAPI { - return mockGetThreadWithEmailsAPI{ - mockGetItem: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + client: func(t *testing.T) email.GetThreadWithEmailsAPI { + return mockutil.MockGetThreadWithEmailsAPI{ + MockGetItem: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { t.Helper() assert.NotNil(t, params.TableName) - assert.Equal(t, tableName, *params.TableName) + assert.Equal(t, env.TableName, *params.TableName) assert.Len(t, params.Key, 1) - assert.IsType(t, params.Key["MessageID"], &types.AttributeValueMemberS{}) + assert.IsType(t, params.Key["MessageID"], &dynamodbTypes.AttributeValueMemberS{}) assert.Equal(t, - params.Key["MessageID"].(*types.AttributeValueMemberS).Value, + params.Key["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value, "exampleMessageID", ) return &dynamodb.GetItemOutput{ Item: map[string]dynamodbTypes.AttributeValue{ "MessageID": params.Key["MessageID"], - "TypeYearMonth": &types.AttributeValueMemberS{Value: "thread#2023-02"}, - "Subject": &types.AttributeValueMemberS{Value: "subject"}, - "EmailIDs": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberS{Value: "id-1"}, - &types.AttributeValueMemberS{Value: "id-2"}, + "TypeYearMonth": &dynamodbTypes.AttributeValueMemberS{Value: "thread#2023-02"}, + "Subject": &dynamodbTypes.AttributeValueMemberS{Value: "subject"}, + "EmailIDs": &dynamodbTypes.AttributeValueMemberL{ + Value: []dynamodbTypes.AttributeValue{ + &dynamodbTypes.AttributeValueMemberS{Value: "id-1"}, + &dynamodbTypes.AttributeValueMemberS{Value: "id-2"}, }, }, - "TimeUpdated": &types.AttributeValueMemberS{Value: "2023-02-18T01:01:01Z"}, + "TimeUpdated": &dynamodbTypes.AttributeValueMemberS{Value: "2023-02-18T01:01:01Z"}, }, }, nil }, - mockBatchGetItem: func(ctx context.Context, params *dynamodb.BatchGetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchGetItemOutput, error) { + MockBatchGetItem: func(ctx context.Context, params *dynamodb.BatchGetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchGetItemOutput, error) { t.Helper() assert.NotNil(t, params.RequestItems) assert.Len(t, params.RequestItems, 1) - assert.Len(t, params.RequestItems[tableName].Keys, 2) + assert.Len(t, params.RequestItems[env.TableName].Keys, 2) assert.Equal(t, - params.RequestItems[tableName].Keys[0]["MessageID"].(*types.AttributeValueMemberS).Value, + params.RequestItems[env.TableName].Keys[0]["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value, "id-1", ) assert.Equal(t, - params.RequestItems[tableName].Keys[1]["MessageID"].(*types.AttributeValueMemberS).Value, + params.RequestItems[env.TableName].Keys[1]["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value, "id-2", ) return &dynamodb.BatchGetItemOutput{ Responses: map[string][]map[string]dynamodbTypes.AttributeValue{ - tableName: { + env.TableName: { { - "MessageID": &types.AttributeValueMemberS{Value: "id-1"}, - "TypeYearMonth": &types.AttributeValueMemberS{Value: "inbox#2023-02"}, - "DateTime": &types.AttributeValueMemberS{Value: "18-01:01:01"}, - "Subject": &types.AttributeValueMemberS{Value: "subject"}, - "From": &types.AttributeValueMemberSS{Value: []string{"example@example.com"}}, - "To": &types.AttributeValueMemberSS{Value: []string{"example@example.com"}}, + "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: "id-1"}, + "TypeYearMonth": &dynamodbTypes.AttributeValueMemberS{Value: "inbox#2023-02"}, + "DateTime": &dynamodbTypes.AttributeValueMemberS{Value: "18-01:01:01"}, + "Subject": &dynamodbTypes.AttributeValueMemberS{Value: "subject"}, + "From": &dynamodbTypes.AttributeValueMemberSS{Value: []string{"example@example.com"}}, + "To": &dynamodbTypes.AttributeValueMemberSS{Value: []string{"example@example.com"}}, }, { - "MessageID": &types.AttributeValueMemberS{Value: "id-2"}, - "TypeYearMonth": &types.AttributeValueMemberS{Value: "inbox#2023-02"}, - "DateTime": &types.AttributeValueMemberS{Value: "18-01:01:01"}, - "Subject": &types.AttributeValueMemberS{Value: "subject"}, - "From": &types.AttributeValueMemberSS{Value: []string{"example@example.com"}}, - "To": &types.AttributeValueMemberSS{Value: []string{"example@example.com"}}, + "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: "id-2"}, + "TypeYearMonth": &dynamodbTypes.AttributeValueMemberS{Value: "inbox#2023-02"}, + "DateTime": &dynamodbTypes.AttributeValueMemberS{Value: "18-01:01:01"}, + "Subject": &dynamodbTypes.AttributeValueMemberS{Value: "subject"}, + "From": &dynamodbTypes.AttributeValueMemberSS{Value: []string{"example@example.com"}}, + "To": &dynamodbTypes.AttributeValueMemberSS{Value: []string{"example@example.com"}}, }, }, }, @@ -212,7 +202,7 @@ func TestGetThreadWithEmails(t *testing.T) { Subject: "subject", EmailIDs: []string{"id-1", "id-2"}, TimeUpdated: "2023-02-18T01:01:01Z", - Emails: []GetResult{ + Emails: []email.GetResult{ { MessageID: "id-1", Type: "inbox", @@ -235,16 +225,16 @@ func TestGetThreadWithEmails(t *testing.T) { }, }, { - client: func(t *testing.T) GetThreadWithEmailsAPI { - return mockGetThreadWithEmailsAPI{ - mockGetItem: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + client: func(t *testing.T) email.GetThreadWithEmailsAPI { + return mockutil.MockGetThreadWithEmailsAPI{ + MockGetItem: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{}, nil }, } }, messageID: "exampleMessageID", expected: nil, - expectedErr: ErrNotFound, + expectedErr: email.ErrNotFound, }, } @@ -259,22 +249,16 @@ func TestGetThreadWithEmails(t *testing.T) { } func TestGenerateThreadID(t *testing.T) { - id := generateThreadID() + id := idutil.GenerateThreadID() assert.NotEmpty(t, id) assert.Len(t, id, 36-4) // minus the 4 dashes assert.NotContains(t, id, "-") } -type mockTransactWriteItemAPI func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) - -func (m mockTransactWriteItemAPI) TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { - return m(ctx, params, optFns...) -} - func TestStoreEmailWithExistingThread(t *testing.T) { - tableName = "table-for-store-email-with-existing-thread" + env.TableName = "table-for-store-email-with-existing-thread" tests := []struct { - client func(t *testing.T) TransactWriteItemsAPI + client func(t *testing.T) email.TransactWriteItemsAPI threadID string email map[string]dynamodbTypes.AttributeValue timeReceived string @@ -282,38 +266,38 @@ func TestStoreEmailWithExistingThread(t *testing.T) { expectedErr error }{ { - client: func(t *testing.T) TransactWriteItemsAPI { - return mockTransactWriteItemAPI(func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { + client: func(t *testing.T) email.TransactWriteItemsAPI { + return mockutil.MockTransactWriteItemAPI(func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { t.Helper() for _, item := range params.TransactItems { if item.Put != nil { - assert.Equal(t, tableName, *item.Put.TableName) + assert.Equal(t, env.TableName, *item.Put.TableName) assert.Equal(t, map[string]dynamodbTypes.AttributeValue{ "MessageID": &dynamodbTypes.AttributeValueMemberS{Value: "exampleMessageID"}, "IsThreadLatest": &dynamodbTypes.AttributeValueMemberBOOL{Value: true}, }, item.Put.Item) } if item.Update != nil { - assert.Equal(t, tableName, *item.Update.TableName) - assert.IsType(t, item.Update.Key["MessageID"], &types.AttributeValueMemberS{}) + assert.Equal(t, env.TableName, *item.Update.TableName) + assert.IsType(t, item.Update.Key["MessageID"], &dynamodbTypes.AttributeValueMemberS{}) - if item.Update.Key["MessageID"].(*types.AttributeValueMemberS).Value == "exampleThreadID" { + if item.Update.Key["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value == "exampleThreadID" { assert.Equal(t, "SET #emails = list_append(#emails, :emails), #timeUpdated = :timeUpdated", *item.Update.UpdateExpression) assert.Equal(t, map[string]string{ "#emails": "EmailIDs", "#timeUpdated": "TimeUpdated", }, item.Update.ExpressionAttributeNames) - assert.Equal(t, map[string]types.AttributeValue{ - ":emails": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberS{Value: "exampleMessageID"}, + assert.Equal(t, map[string]dynamodbTypes.AttributeValue{ + ":emails": &dynamodbTypes.AttributeValueMemberL{ + Value: []dynamodbTypes.AttributeValue{ + &dynamodbTypes.AttributeValueMemberS{Value: "exampleMessageID"}, }, }, - ":timeUpdated": &types.AttributeValueMemberS{Value: "2023-02-18T01:01:01Z"}, + ":timeUpdated": &dynamodbTypes.AttributeValueMemberS{Value: "2023-02-18T01:01:01Z"}, }, item.Update.ExpressionAttributeValues) } else { - assert.Equal(t, "examplePreviousMessageID", item.Update.Key["MessageID"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "examplePreviousMessageID", item.Update.Key["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value) assert.Equal(t, "REMOVE IsThreadLatest", *item.Update.UpdateExpression) } } @@ -348,9 +332,9 @@ func TestStoreEmailWithExistingThread(t *testing.T) { } func TestStoreEmailWithNewThread(t *testing.T) { - tableName = "table-for-store-email-with-existing-thread" + env.TableName = "table-for-store-email-with-existing-thread" tests := []struct { - client func(t *testing.T) TransactWriteItemsAPI + client func(t *testing.T) email.TransactWriteItemsAPI threadID string email map[string]dynamodbTypes.AttributeValue CreatingEmailID string @@ -360,12 +344,12 @@ func TestStoreEmailWithNewThread(t *testing.T) { expectedErr error }{ { - client: func(t *testing.T) TransactWriteItemsAPI { - return mockTransactWriteItemAPI(func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { + client: func(t *testing.T) email.TransactWriteItemsAPI { + return mockutil.MockTransactWriteItemAPI(func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { t.Helper() for _, item := range params.TransactItems { if item.Put != nil { - assert.Equal(t, tableName, *item.Put.TableName) + assert.Equal(t, env.TableName, *item.Put.TableName) if item.Put.Item["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value == "exampleThreadID" { assert.Equal(t, map[string]dynamodbTypes.AttributeValue{ @@ -374,9 +358,9 @@ func TestStoreEmailWithNewThread(t *testing.T) { Value: "thread#2023-02", }, "EmailIDs": &dynamodbTypes.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberS{Value: "exampleCreatingEmailID"}, - &types.AttributeValueMemberS{Value: "exampleMessageID"}, + Value: []dynamodbTypes.AttributeValue{ + &dynamodbTypes.AttributeValueMemberS{Value: "exampleCreatingEmailID"}, + &dynamodbTypes.AttributeValueMemberS{Value: "exampleMessageID"}, }, }, "TimeUpdated": &dynamodbTypes.AttributeValueMemberS{Value: "2023-02-19T01:01:01Z"}, @@ -392,18 +376,18 @@ func TestStoreEmailWithNewThread(t *testing.T) { } } if item.Update != nil { - assert.Equal(t, tableName, *item.Update.TableName) - assert.IsType(t, item.Update.Key["MessageID"], &types.AttributeValueMemberS{}) + assert.Equal(t, env.TableName, *item.Update.TableName) + assert.IsType(t, item.Update.Key["MessageID"], &dynamodbTypes.AttributeValueMemberS{}) assert.Equal(t, - item.Update.Key["MessageID"].(*types.AttributeValueMemberS).Value, + item.Update.Key["MessageID"].(*dynamodbTypes.AttributeValueMemberS).Value, "exampleCreatingEmailID", ) assert.Equal(t, "SET #threadID = :threadID", *item.Update.UpdateExpression) assert.Equal(t, map[string]string{ "#threadID": "ThreadID", }, item.Update.ExpressionAttributeNames) - assert.Equal(t, map[string]types.AttributeValue{ - ":threadID": &types.AttributeValueMemberS{Value: "exampleThreadID"}, + assert.Equal(t, map[string]dynamodbTypes.AttributeValue{ + ":threadID": &dynamodbTypes.AttributeValueMemberS{Value: "exampleThreadID"}, }, item.Update.ExpressionAttributeValues) } } diff --git a/internal/util/idutil/idutil.go b/internal/util/idutil/idutil.go new file mode 100644 index 00000000..07d69ebb --- /dev/null +++ b/internal/util/idutil/idutil.go @@ -0,0 +1,11 @@ +package idutil + +import ( + "strings" + + "github.com/google/uuid" +) + +func GenerateThreadID() string { + return strings.ReplaceAll(uuid.NewString(), "-", "") +} diff --git a/internal/util/mockutil/mockutil.go b/internal/util/mockutil/mockutil.go new file mode 100644 index 00000000..8c2c1b5c --- /dev/null +++ b/internal/util/mockutil/mockutil.go @@ -0,0 +1,32 @@ +package mockutil + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" +) + +type MockGetItemAPI func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) + +func (m MockGetItemAPI) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return m(ctx, params, optFns...) +} + +type MockGetThreadWithEmailsAPI struct { + MockGetItem MockGetItemAPI + MockBatchGetItem func(ctx context.Context, params *dynamodb.BatchGetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchGetItemOutput, error) +} + +func (m MockGetThreadWithEmailsAPI) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return m.MockGetItem(ctx, params, optFns...) +} + +func (m MockGetThreadWithEmailsAPI) BatchGetItem(ctx context.Context, params *dynamodb.BatchGetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchGetItemOutput, error) { + return m.MockBatchGetItem(ctx, params, optFns...) +} + +type MockTransactWriteItemAPI func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) + +func (m MockTransactWriteItemAPI) TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { + return m(ctx, params, optFns...) +}