From 529c48160f8740f8a4c84a615ea2d88b3e82aa48 Mon Sep 17 00:00:00 2001 From: uprightsleepy Date: Fri, 7 Mar 2025 22:49:43 -0600 Subject: [PATCH 1/4] updated dependencies --- go.mod | 6 ++++-- go.sum | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c35995d..491c352 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,9 @@ require ( github.com/aws/aws-lambda-go v1.47.0 github.com/aws/aws-sdk-go-v2 v1.36.3 github.com/aws/aws-sdk-go-v2/config v1.29.9 - github.com/aws/aws-sdk-go-v2/service/rdsdata v1.28.1 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.1 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.1 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 + github.com/golang/mock v1.6.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 ) @@ -19,6 +20,7 @@ require ( github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect diff --git a/go.sum b/go.sum index 8ecf8c0..d9fe82b 100644 --- a/go.sum +++ b/go.sum @@ -14,14 +14,16 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0io github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.1 h1:DEys4E5Q2p735j56lteNVyByIBDAlMrO5VIEd9RC0/4= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.1/go.mod h1:yYaWRnVSPyAmexW5t7G3TcuYoalYfT+xQwzWsvtUQ7M= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15 h1:M1R1rud7HzDrfCdlBQ7NjnRsDNEhXO/vGhuD189Ggmk= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15/go.mod h1:uvFKBSq9yMPV4LGAi7N4awn4tLY+hKE35f8THes2mzQ= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/rdsdata v1.28.1 h1:E8NhIO2v519YEOWPNaFigCyrwgF0Z8E0nRWlYqhRTOc= -github.com/aws/aws-sdk-go-v2/service/rdsdata v1.28.1/go.mod h1:ah2CXasxl8doBpmLB5w4d3I1GDM8ykZpvdM9ac2Fq2Y= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.1 h1:+FDQfaijddP+aeT1BcT4ic8nZZc4hYUQVDL51CeCvb8= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.1/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 h1:vlYXbindmagyVA3RS2SPd47eKZ00GZZQcr+etTviHtc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q= github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= @@ -33,6 +35,8 @@ github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxY 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/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -43,8 +47,31 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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/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.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 6084f9ecb5c6843c9177bf2a7fbfc6bb0647e1fb Mon Sep 17 00:00:00 2001 From: uprightsleepy Date: Fri, 7 Mar 2025 22:50:10 -0600 Subject: [PATCH 2/4] removed references to RDS and went back to ddb --- config/config.go | 57 +------------ internal/db/db.go | 104 ++++++++++++++++++++++++ internal/handlers/handler.go | 42 +++++----- internal/helper/helper.go | 47 ++++++----- internal/postgres/db.go | 150 ----------------------------------- main.go | 10 ++- 6 files changed, 157 insertions(+), 253 deletions(-) create mode 100644 internal/db/db.go delete mode 100644 internal/postgres/db.go diff --git a/config/config.go b/config/config.go index 9bbcf9e..a0b8f29 100644 --- a/config/config.go +++ b/config/config.go @@ -2,10 +2,8 @@ package config import ( "context" - "encoding/json" "errors" "fmt" - "net/url" "os" "github.com/aws/aws-sdk-go-v2/aws" @@ -14,22 +12,8 @@ import ( "github.com/sirupsen/logrus" ) -type PostgresSecret struct { - Username string `json:"username"` - Password string `json:"password"` - Database string `json:"database"` - Host string `json:"host"` - Port string `json:"port"` - DBClusterARN string `json:"dbClusterArn"` - SecretARN string `json:"secretArn"` -} - type Config struct { - DBClusterARN string - SecretARN string - DatabaseName string - PostgresConnStr string - AtProtoBaseURL string + AtProtoBaseURL string } type SecretsManagerAPI interface { @@ -42,50 +26,17 @@ func LoadConfig(ctx context.Context, secretsClient SecretsManagerAPI) (*Config, return nil, aws.Config{}, fmt.Errorf("failed to load AWS config: %w", err) } - secretName := os.Getenv("POSTGRES_CONN_STR") baseURL := os.Getenv("ATPROTO_BASE_URL") - - if secretName == "" { - return nil, aws.Config{}, errors.New("POSTGRES_CONN_STR environment variable is required") - } - if baseURL == "" { return nil, aws.Config{}, errors.New("ATPROTO_BASE_URL environment variable is required") } - secretValue, err := RetrieveSecret(ctx, secretName, secretsClient) - if err != nil { - return nil, aws.Config{}, fmt.Errorf("failed to retrieve PostgreSQL secret: %w", err) - } - - var secret PostgresSecret - if err := json.Unmarshal([]byte(secretValue), &secret); err != nil { - return nil, aws.Config{}, fmt.Errorf("failed to parse PostgreSQL secret JSON: %w", err) // **Fix: Proper error** - } - - if secret.Database == "" || secret.Host == "" || secret.Username == "" || secret.Password == "" { - return nil, aws.Config{}, errors.New("parsed PostgreSQL secret is missing required fields") - } - - formattedConnStr := fmt.Sprintf( - "postgres://%s:%s@%s:%s/%s?sslmode=require", - url.QueryEscape(secret.Username), url.QueryEscape(secret.Password), - secret.Host, secret.Port, secret.Database, - ) - logrus.WithFields(logrus.Fields{ - "host": secret.Host, - "database": secret.Database, - "dbClusterArn": secret.DBClusterARN, - "secretArn": secret.SecretARN, - }).Info("Successfully loaded PostgreSQL connection details") + "atproto_base_url": baseURL, + }).Info("Successfully loaded application configuration") return &Config{ - DBClusterARN: secret.DBClusterARN, - SecretARN: secret.SecretARN, - DatabaseName: secret.Database, - PostgresConnStr: formattedConnStr, - AtProtoBaseURL: baseURL, + AtProtoBaseURL: baseURL, }, awsCfg, nil } diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..da3b2c2 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,104 @@ +package db + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/ShareFrame/user-management/internal/models" + "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/sirupsen/logrus" +) + +const ( + QueryTimeout = 3 * time.Second + TableName = "Users" +) + +type DynamoDBService interface { + CheckEmailExists(ctx context.Context, email string) (bool, error) + StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error +} + +type DynamoDBAPI interface { + PutItem(ctx context.Context, input *dynamodb.PutItemInput, opts ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) + Query(ctx context.Context, input *dynamodb.QueryInput, opts ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) +} + +type DynamoDBClient struct { + Client DynamoDBAPI +} + +func NewDynamoDBClient(client DynamoDBAPI) *DynamoDBClient { + return &DynamoDBClient{ + Client: client, + } +} + +func (d *DynamoDBClient) StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error { + ctx, cancel := context.WithTimeout(ctx, QueryTimeout) + defer cancel() + + themeJSON, err := json.Marshal(map[string]interface{}{}) + if err != nil { + return fmt.Errorf("failed to marshal theme: %w", err) + } + + item := map[string]types.AttributeValue{ + "UserId": &types.AttributeValueMemberS{Value: user.DID}, + "Did": &types.AttributeValueMemberS{Value: user.DID}, + "Email": &types.AttributeValueMemberS{Value: event.Email}, + "Handle": &types.AttributeValueMemberS{Value: user.Handle}, + "CreatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, + "ModifiedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, + "Status": &types.AttributeValueMemberS{Value: "active"}, + "Verified": &types.AttributeValueMemberBOOL{Value: false}, + "Role": &types.AttributeValueMemberS{Value: "user"}, + "DisplayName": &types.AttributeValueMemberS{Value: user.Handle}, + "ProfilePicture": &types.AttributeValueMemberS{Value: ""}, + "ProfileBanner": &types.AttributeValueMemberS{Value: ""}, + "Theme": &types.AttributeValueMemberS{Value: string(themeJSON)}, + "PrimaryColor": &types.AttributeValueMemberS{Value: "#FFFFFF"}, + "SecondaryColor": &types.AttributeValueMemberS{Value: "#000000"}, + } + + _, err = d.Client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(TableName), + Item: item, + }) + if err != nil { + logrus.Errorf("Failed to store user: %v", err) + return fmt.Errorf("failed to store user in DynamoDB: %w", err) + } + + logrus.Infof("User %s successfully stored in DynamoDB", user.Handle) + return nil +} + +func (d *DynamoDBClient) CheckEmailExists(ctx context.Context, email string) (bool, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeout) + defer cancel() + + result, err := d.Client.Query(ctx, &dynamodb.QueryInput{ + TableName: aws.String("Users"), + IndexName: aws.String("Email-index"), + KeyConditionExpression: aws.String("Email = :email"), + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":email": &types.AttributeValueMemberS{Value: email}, + }, + Limit: aws.Int32(1), + }) + + if err != nil { + logrus.WithError(err).Error("Error checking email existence") + return false, fmt.Errorf("failed to check email existence: %w", err) + } + + return len(result.Items) > 0, nil +} + + + diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go index 727838d..1c14ee9 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -9,32 +9,32 @@ import ( ATProtocol "github.com/ShareFrame/user-management/internal/atproto" "github.com/ShareFrame/user-management/internal/helper" "github.com/ShareFrame/user-management/internal/models" - "github.com/ShareFrame/user-management/internal/postgres" - "github.com/aws/aws-sdk-go-v2/service/rdsdata" + shareframeDB "github.com/ShareFrame/user-management/internal/db" "github.com/sirupsen/logrus" ) type UserHandler struct { SecretsManagerClient config.SecretsManagerAPI + DBClient shareframeDB.DynamoDBService } -func NewUserHandler(secretsClient config.SecretsManagerAPI) *UserHandler { - return &UserHandler{SecretsManagerClient: secretsClient} +func NewUserHandler(secretsClient config.SecretsManagerAPI, dbClient shareframeDB.DynamoDBService) *UserHandler { + return &UserHandler{ + SecretsManagerClient: secretsClient, + DBClient: dbClient, + } } func (h *UserHandler) Handle(ctx context.Context, event models.UserRequest) (*models.CreateUserResponse, error) { logrus.WithField("handle", event.Handle).Info("Processing create account request") - cfg, awsCfg, err := config.LoadConfig(ctx, h.SecretsManagerClient) + cfg, _, err := config.LoadConfig(ctx, h.SecretsManagerClient) if err != nil { logrus.WithError(err).Error("Failed to load application configuration") return nil, fmt.Errorf("internal error: failed to load application configuration: %w", err) } - rdsClient := rdsdata.NewFromConfig(awsCfg) - dbClient := postgres.NewPostgresDB(rdsClient, cfg.DBClusterARN, cfg.SecretARN, cfg.DatabaseName) - - updatedEvent, err := helper.ValidateAndFormatUser(ctx, event, dbClient) + updatedEvent, err := helper.ValidateAndFormatUser(ctx, event, h.DBClient) if err != nil { logrus.WithError(err).Warn("Validation error") return nil, fmt.Errorf("validation error: %w", err) @@ -64,10 +64,8 @@ func (h *UserHandler) Handle(ctx context.Context, event models.UserRequest) (*mo session, err := atProtoClient.CreateSession(utilAccountCreds.Username, utilAccountCreds.Password) if err != nil { - logrus.WithFields(logrus.Fields{ - "username": utilAccountCreds.Username, - "error": err.Error(), - }).Error("Failed to authenticate with AT Protocol") + logrus.WithFields(logrus.Fields{"username": utilAccountCreds.Username}).WithError(err). + Error("Failed to authenticate with AT Protocol") return nil, fmt.Errorf("authentication failed for user %s: %w", utilAccountCreds.Username, err) } @@ -75,7 +73,7 @@ func (h *UserHandler) Handle(ctx context.Context, event models.UserRequest) (*mo exists, err := atProtoClient.CheckUserExists(event.Handle, session.AccessJwt) if err != nil { - logrus.WithError(err).WithField("handle", event.Handle).Error("Failed to check user existence") + logrus.WithField("handle", event.Handle).WithError(err).Error("Failed to check user existence") return nil, fmt.Errorf("internal error: failed to check if user exists: %w", err) } @@ -86,22 +84,18 @@ func (h *UserHandler) Handle(ctx context.Context, event models.UserRequest) (*mo user, err := atProtoClient.RegisterUser(event.Handle, event.Email, inviteCode.Code, event.Password) if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "handle": event.Handle, - "email": event.Email, - }).Error("Failed to register user via AT Protocol") + logrus.WithFields(logrus.Fields{"handle": event.Handle, "email": event.Email}).WithError(err). + Error("Failed to register user via AT Protocol") return nil, fmt.Errorf("failed to register user: %w", err) } - if err = dbClient.StoreUser(ctx, user, updatedEvent); err != nil { - logrus.WithError(err).Error("Failed to store user in PostgreSQL") + if err = h.DBClient.StoreUser(ctx, user, updatedEvent); err != nil { + logrus.WithError(err).Error("Failed to store user in DynamoDB") return nil, fmt.Errorf("internal error: failed to store user data: %w", err) } - logrus.WithFields(logrus.Fields{ - "did": user.DID, - "handle": user.Handle, - }).Info("Successfully created and stored user") + logrus.WithFields(logrus.Fields{"did": user.DID, "handle": user.Handle}). + Info("Successfully created and stored user") return &user, nil } diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 5bb8090..74042b6 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -8,10 +8,10 @@ import ( "os" "regexp" "strings" + "time" "github.com/ShareFrame/user-management/config" "github.com/ShareFrame/user-management/internal/models" - "github.com/ShareFrame/user-management/internal/postgres" "github.com/sirupsen/logrus" ) @@ -28,7 +28,7 @@ const ( BlockedHandle = "handle is not allowed" HandleTooShort = "handle must be at least 3 characters long" HandleTooLong = "handle cannot exceed 18 characters" - QueryTimeout = 3 + QueryTimeout = 3 * time.Second ) var ( @@ -46,7 +46,12 @@ func init() { } } -func ValidateAndFormatUser(ctx context.Context, event models.UserRequest, dbClient postgres.PostgresDBService) (models.UserRequest, error) { +type DatabaseService interface { + CheckEmailExists(ctx context.Context, email string) (bool, error) + StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error +} + +func ValidateAndFormatUser(ctx context.Context, event models.UserRequest, dbClient DatabaseService) (models.UserRequest, error) { if event.Handle == "" || event.Email == "" || event.Password == "" { logrus.Warn("Validation failed: missing required fields") return models.UserRequest{}, fmt.Errorf("%v", MissingFields) @@ -70,6 +75,7 @@ func ValidateAndFormatUser(ctx context.Context, event models.UserRequest, dbClie return models.UserRequest{}, fmt.Errorf("password validation failed: %w", err) } + logrus.Infof("Calling CheckEmailExists for email: %s", event.Email) exists, err := dbClient.CheckEmailExists(ctx, event.Email) if err != nil { logrus.WithError(err).Error("Database error: failed to check email existence") @@ -84,30 +90,32 @@ func ValidateAndFormatUser(ctx context.Context, event models.UserRequest, dbClie return event, nil } - func ValidateHandle(handle string) error { if len(handle) < 3 { - return fmt.Errorf("handle must be at least 3 characters long: %v", handle) + return fmt.Errorf("%v: %v", HandleTooShort, handle) } if len(handle) > 18 { - return fmt.Errorf("handle cannot exceed 18 characters: %v", handle) + return fmt.Errorf("%v: %v", HandleTooLong, handle) } for _, blocked := range blockedUsernames { if strings.EqualFold(handle, blocked) { - return fmt.Errorf("provided handle is not allowed: %v", BlockedHandle) + return fmt.Errorf("%v: %v", BlockedHandle, handle) } } if !handleRegex.MatchString(handle) { - return fmt.Errorf("provided handle is invalid: %v", InvalidHandle) + return fmt.Errorf("%v: %v", InvalidHandle, handle) } return nil } func EnsureHandleSuffix(handle string) string { - if strings.HasSuffix(handle, PDS_Suffix) { - return handle + trimmedHandle := strings.TrimSpace(handle) + + if strings.HasSuffix(trimmedHandle, PDS_Suffix) { + return trimmedHandle } - return handle + PDS_Suffix + + return trimmedHandle + PDS_Suffix } @@ -119,17 +127,13 @@ func ValidateEmail(email string) error { } func ValidatePassword(password string) error { - if len(password) < 8 { - return fmt.Errorf(PasswordError) - } - - if !upperCaseRegex.MatchString(password) || + if len(password) < 8 || + !upperCaseRegex.MatchString(password) || !lowerCaseRegex.MatchString(password) || !digitRegex.MatchString(password) || !specialCharRegex.MatchString(password) { return fmt.Errorf(PasswordError) } - return nil } @@ -139,17 +143,12 @@ func retrieveCredentials[T any](ctx context.Context, secretEnvVar string, secret input, err := config.RetrieveSecret(ctx, secretName, secretsManagerClient) if err != nil { - logrus.WithFields(logrus.Fields{ - "secret_name": secretName, - }).WithError(err).Error("Failed to retrieve credentials from Secrets Manager") + logrus.WithFields(logrus.Fields{"secret_name": secretName}).WithError(err).Error("Failed to retrieve credentials") return creds, fmt.Errorf("error retrieving credentials from Secrets Manager (%s): %w", secretName, err) } if err := json.Unmarshal([]byte(input), &creds); err != nil { - logrus.WithFields(logrus.Fields{ - "secret_name": secretName, - "secret_value": input, - }).WithError(err).Error("Failed to unmarshal credentials") + logrus.WithFields(logrus.Fields{"secret_name": secretName, "secret_value": input}).WithError(err).Error("Failed to unmarshal credentials") return creds, fmt.Errorf("invalid credentials format: %w", err) } diff --git a/internal/postgres/db.go b/internal/postgres/db.go deleted file mode 100644 index d09ad3b..0000000 --- a/internal/postgres/db.go +++ /dev/null @@ -1,150 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "time" - - "github.com/ShareFrame/user-management/internal/models" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/rdsdata" - "github.com/aws/aws-sdk-go-v2/service/rdsdata/types" - "github.com/sirupsen/logrus" -) - -const ( - DefaultStatus = "active" - DefaultVerified = false - DefaultRole = "user" - DefaultPicture = "" - DefaultBanner = "" - DefaultTheme = "{}" - DefaultColor1 = "#FFFFFF" - DefaultColor2 = "#000000" - QueryTimeout = 3 * time.Second -) - -type PostgresDBService interface { - CheckEmailExists(ctx context.Context, email string) (bool, error) - StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error -} - -type RDSDataAPI interface { - ExecuteStatement(ctx context.Context, input *rdsdata.ExecuteStatementInput, opts ...func(*rdsdata.Options)) (*rdsdata.ExecuteStatementOutput, error) -} - -type PostgresDB struct { - Client RDSDataAPI - DBClusterARN string - SecretARN string - DatabaseName string -} - -func NewPostgresDB(client RDSDataAPI, dbClusterARN, secretARN, database string) *PostgresDB { - return &PostgresDB{ - Client: client, - DBClusterARN: dbClusterARN, - SecretARN: secretARN, - DatabaseName: database, - } -} - -func (p *PostgresDB) StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error { - ctx, cancel := context.WithTimeout(ctx, QueryTimeout) - defer cancel() - - query := ` - INSERT INTO users - (did, email, handle, created_at, modified_at, status, verified, role, display_name, profile_picture, profile_banner, theme, primary_color, secondary_color) - VALUES - (:did, :email, :handle, NOW(), NOW(), :status, :verified, :role, :display_name, :profile_picture, :profile_banner, CAST(:theme AS JSONB), :primary_color, :secondary_color)` - - params := []types.SqlParameter{ - newSQLParam("did", user.DID), - newSQLParam("email", event.Email), - newSQLParam("handle", user.Handle), - newSQLParam("status", DefaultStatus), - newSQLParam("verified", DefaultVerified), - newSQLParam("role", DefaultRole), - newSQLParam("display_name", user.Handle), - newSQLParam("profile_picture", DefaultPicture), - newSQLParam("profile_banner", DefaultBanner), - newSQLParam("theme", DefaultTheme), - newSQLParam("primary_color", DefaultColor1), - newSQLParam("secondary_color", DefaultColor2), - } - - result, err := p.Client.ExecuteStatement(ctx, &rdsdata.ExecuteStatementInput{ - ResourceArn: aws.String(p.DBClusterARN), - SecretArn: aws.String(p.SecretARN), - Database: aws.String(p.DatabaseName), - Sql: aws.String(query), - Parameters: params, - }) - - if err != nil { - logrus.WithFields(logrus.Fields{ - "email": event.Email, - "handle": user.Handle, - }).Errorf("Failed to store user: %v", err) - return fmt.Errorf("failed to store user in PostgreSQL: %w", err) - } - - if result == nil { - logrus.WithFields(logrus.Fields{ - "email": event.Email, - "handle": user.Handle, - }).Error("ExecuteStatement returned nil response") - return fmt.Errorf("failed to store user in PostgreSQL: unexpected nil response") - } - - logrus.Infof("User %s successfully stored in PostgreSQL", user.Handle) - return nil -} - - - -func (p *PostgresDB) CheckEmailExists(ctx context.Context, email string) (bool, error) { - ctx, cancel := context.WithTimeout(ctx, QueryTimeout) - defer cancel() - - query := `SELECT 1 FROM users WHERE email = :email LIMIT 1` - params := []types.SqlParameter{newSQLParam("email", email)} - - result, err := p.Client.ExecuteStatement(ctx, &rdsdata.ExecuteStatementInput{ - ResourceArn: aws.String(p.DBClusterARN), - SecretArn: aws.String(p.SecretARN), - Database: aws.String(p.DatabaseName), - Sql: aws.String(query), - Parameters: params, - }) - - if err != nil { - logrus.WithFields(logrus.Fields{ - "email": email, - }).Errorf("Error checking email existence: %v", err) - return false, fmt.Errorf("failed to check email existence: %w", err) - } - - if result == nil { - logrus.WithFields(logrus.Fields{ - "email": email, - }).Error("ExecuteStatement returned nil response") - return false, fmt.Errorf("failed to check email existence: unexpected nil response") - } - - return len(result.Records) > 0, nil -} - - -func newSQLParam(name string, value interface{}) types.SqlParameter { - switch v := value.(type) { - case string: - return types.SqlParameter{Name: aws.String(name), Value: &types.FieldMemberStringValue{Value: v}} - case bool: - return types.SqlParameter{Name: aws.String(name), Value: &types.FieldMemberBooleanValue{Value: v}} - default: - logrus.Warnf("Unsupported SQL parameter type for %s", name) - return types.SqlParameter{} - } -} diff --git a/main.go b/main.go index e5f6b9f..e185a31 100644 --- a/main.go +++ b/main.go @@ -2,22 +2,28 @@ package main import ( "context" + "log" "github.com/ShareFrame/user-management/internal/handlers" + shareframeDB "github.com/ShareFrame/user-management/internal/db" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" ) func main() { awsCfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { - panic("Failed to load AWS config: " + err.Error()) + log.Fatalf("Failed to load AWS config: %v", err) } secretsManagerClient := secretsmanager.NewFromConfig(awsCfg) + dynamoClient := dynamodb.NewFromConfig(awsCfg) - userHandler := handlers.NewUserHandler(secretsManagerClient) + dbClient := shareframeDB.NewDynamoDBClient(dynamoClient) + + userHandler := handlers.NewUserHandler(secretsManagerClient, dbClient) lambda.Start(userHandler.Handle) } From a11ef24ba3511b032c8d6bce21113f45d600d3de Mon Sep 17 00:00:00 2001 From: uprightsleepy Date: Fri, 7 Mar 2025 22:50:24 -0600 Subject: [PATCH 3/4] updated tests and added mocks --- config/config_test.go | 51 +---- internal/db/db_test.go | 161 ++++++++++++++ internal/db/mocks/mock_dynamodb.go | 129 ++++++++++++ internal/helper/helper_test.go | 303 ++++++++++++++------------- internal/helper/mocks/mock_helper.go | 65 ++++++ internal/postgres/db_test.go | 163 -------------- 6 files changed, 513 insertions(+), 359 deletions(-) create mode 100644 internal/db/db_test.go create mode 100644 internal/db/mocks/mock_dynamodb.go create mode 100644 internal/helper/mocks/mock_helper.go delete mode 100644 internal/postgres/db_test.go diff --git a/config/config_test.go b/config/config_test.go index 151c5ca..ff22ec5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,7 +2,6 @@ package config import ( "context" - "encoding/json" "errors" "os" "testing" @@ -30,60 +29,22 @@ func TestLoadConfig(t *testing.T) { mockSecretsClient := new(mockSecretsManagerClient) ctx := context.Background() - validSecret := PostgresSecret{ - Username: "user", - Password: "pass", - Database: "testdb", - Host: "localhost", - Port: "5432", - DBClusterARN: "arn:aws:rds:us-east-1:123456789012:cluster:test-cluster", - SecretARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret", - } - - validSecretJSON, _ := json.Marshal(validSecret) - tests := []struct { name string envVars map[string]string - mockSecret *secretsmanager.GetSecretValueOutput - mockSecretErr error expectedErrMsg string }{ { name: "Successful Config Load", envVars: map[string]string{ - "POSTGRES_CONN_STR": "test-secret", - "ATPROTO_BASE_URL": "https://example.com", - }, - mockSecret: &secretsmanager.GetSecretValueOutput{ - SecretString: aws.String(string(validSecretJSON)), + "ATPROTO_BASE_URL": "https://example.com", }, - mockSecretErr: nil, expectedErrMsg: "", }, { - name: "Corrupt JSON in Secret", - envVars: map[string]string{ - "POSTGRES_CONN_STR": "test-secret", - "ATPROTO_BASE_URL": "https://example.com", - }, - mockSecret: &secretsmanager.GetSecretValueOutput{ - SecretString: aws.String(`{"invalid": "json"`), - }, - mockSecretErr: nil, - expectedErrMsg: "failed to parse PostgreSQL secret JSON", - }, - { - name: "Missing Required Fields in Secret JSON", - envVars: map[string]string{ - "POSTGRES_CONN_STR": "test-secret", - "ATPROTO_BASE_URL": "https://example.com", - }, - mockSecret: &secretsmanager.GetSecretValueOutput{ - SecretString: aws.String(`{"username": "", "password": "", "database": "", "host": ""}`), - }, - mockSecretErr: nil, - expectedErrMsg: "parsed PostgreSQL secret is missing required fields", + name: "Missing ATPROTO_BASE_URL", + envVars: map[string]string{}, + expectedErrMsg: "ATPROTO_BASE_URL environment variable is required", }, } @@ -95,8 +56,6 @@ func TestLoadConfig(t *testing.T) { } mockSecretsClient.ExpectedCalls = nil - mockSecretsClient.On("GetSecretValue", mock.Anything, mock.Anything). - Return(test.mockSecret, test.mockSecretErr) _, _, err := LoadConfig(ctx, mockSecretsClient) @@ -106,8 +65,6 @@ func TestLoadConfig(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), test.expectedErrMsg) } - - mockSecretsClient.AssertExpectations(t) }) } } diff --git a/internal/db/db_test.go b/internal/db/db_test.go new file mode 100644 index 0000000..8c047c2 --- /dev/null +++ b/internal/db/db_test.go @@ -0,0 +1,161 @@ +package db + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/ShareFrame/user-management/internal/db/mocks" + "github.com/ShareFrame/user-management/internal/models" + "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/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func createTestUser() (models.CreateUserResponse, models.UserRequest) { + return models.CreateUserResponse{ + DID: "did:example:123", + Handle: "testuser", + }, models.UserRequest{ + Email: "test@example.com", + Handle: "testuser", + Password: "Password123!", + } +} + +func TestStoreUser(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDynamoDB := mocks.NewMockDynamoDBAPI(ctrl) + dynamoClient := &DynamoDBClient{Client: mockDynamoDB} + testUser, testEvent := createTestUser() + + themeJSON, _ := json.Marshal(map[string]interface{}{}) + + expectedInput := &dynamodb.PutItemInput{ + TableName: aws.String(TableName), + Item: map[string]types.AttributeValue{ + "UserId": &types.AttributeValueMemberS{Value: testUser.DID}, + "Did": &types.AttributeValueMemberS{Value: testUser.DID}, + "Email": &types.AttributeValueMemberS{Value: testEvent.Email}, + "Handle": &types.AttributeValueMemberS{Value: testUser.Handle}, + "CreatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, + "ModifiedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, + "Status": &types.AttributeValueMemberS{Value: "active"}, + "Verified": &types.AttributeValueMemberBOOL{Value: false}, + "Role": &types.AttributeValueMemberS{Value: "user"}, + "DisplayName": &types.AttributeValueMemberS{Value: testUser.Handle}, + "ProfilePicture": &types.AttributeValueMemberS{Value: ""}, + "ProfileBanner": &types.AttributeValueMemberS{Value: ""}, + "Theme": &types.AttributeValueMemberS{Value: string(themeJSON)}, + "PrimaryColor": &types.AttributeValueMemberS{Value: "#FFFFFF"}, + "SecondaryColor": &types.AttributeValueMemberS{Value: "#000000"}, + }, + } + + tests := []struct { + name string + mockSetup func() + expectError bool + }{ + { + name: "Successful StoreUser", + mockSetup: func() { + mockDynamoDB.EXPECT().PutItem(gomock.Any(), expectedInput).Return(&dynamodb.PutItemOutput{}, nil) + }, + expectError: false, + }, + { + name: "Failed StoreUser (DynamoDB error)", + mockSetup: func() { + mockDynamoDB.EXPECT().PutItem(gomock.Any(), expectedInput).Return(nil, errors.New("DynamoDB error")) + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + err := dynamoClient.StoreUser(context.TODO(), testUser, testEvent) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCheckEmailExists(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDynamoDB := mocks.NewMockDynamoDBAPI(ctrl) + dynamoClient := &DynamoDBClient{Client: mockDynamoDB} + + testEmail := "test@example.com" + + tests := []struct { + name string + mockSetup func() + expectExists bool + expectError bool + }{ + { + name: "Email exists", + mockSetup: func() { + mockDynamoDB.EXPECT().Query(gomock.Any(), &dynamodb.QueryInput{ + TableName: aws.String("Users"), + IndexName: aws.String("Email-index"), + KeyConditionExpression: aws.String("Email = :email"), + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":email": &types.AttributeValueMemberS{Value: testEmail}, + }, + Limit: aws.Int32(1), + }).Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{ + {"Email": &types.AttributeValueMemberS{Value: testEmail}}, + }}, nil) + }, + expectExists: true, + expectError: false, + }, + { + name: "Email does not exist", + mockSetup: func() { + mockDynamoDB.EXPECT().Query(gomock.Any(), gomock.Any()).Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{}}, nil) + }, + expectExists: false, + expectError: false, + }, + { + name: "DynamoDB error", + mockSetup: func() { + mockDynamoDB.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, errors.New("DynamoDB error")) + }, + expectExists: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + exists, err := dynamoClient.CheckEmailExists(context.TODO(), testEmail) + assert.Equal(t, tt.expectExists, exists) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/db/mocks/mock_dynamodb.go b/internal/db/mocks/mock_dynamodb.go new file mode 100644 index 0000000..0b438d7 --- /dev/null +++ b/internal/db/mocks/mock_dynamodb.go @@ -0,0 +1,129 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: db.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + models "github.com/ShareFrame/user-management/internal/models" + dynamodb "github.com/aws/aws-sdk-go-v2/service/dynamodb" + gomock "github.com/golang/mock/gomock" +) + +// MockDynamoDBService is a mock of DynamoDBService interface. +type MockDynamoDBService struct { + ctrl *gomock.Controller + recorder *MockDynamoDBServiceMockRecorder +} + +// MockDynamoDBServiceMockRecorder is the mock recorder for MockDynamoDBService. +type MockDynamoDBServiceMockRecorder struct { + mock *MockDynamoDBService +} + +// NewMockDynamoDBService creates a new mock instance. +func NewMockDynamoDBService(ctrl *gomock.Controller) *MockDynamoDBService { + mock := &MockDynamoDBService{ctrl: ctrl} + mock.recorder = &MockDynamoDBServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDynamoDBService) EXPECT() *MockDynamoDBServiceMockRecorder { + return m.recorder +} + +// CheckEmailExists mocks base method. +func (m *MockDynamoDBService) CheckEmailExists(ctx context.Context, email string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckEmailExists", ctx, email) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckEmailExists indicates an expected call of CheckEmailExists. +func (mr *MockDynamoDBServiceMockRecorder) CheckEmailExists(ctx, email interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckEmailExists", reflect.TypeOf((*MockDynamoDBService)(nil).CheckEmailExists), ctx, email) +} + +// StoreUser mocks base method. +func (m *MockDynamoDBService) StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreUser", ctx, user, event) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreUser indicates an expected call of StoreUser. +func (mr *MockDynamoDBServiceMockRecorder) StoreUser(ctx, user, event interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUser", reflect.TypeOf((*MockDynamoDBService)(nil).StoreUser), ctx, user, event) +} + +// MockDynamoDBAPI is a mock of DynamoDBAPI interface. +type MockDynamoDBAPI struct { + ctrl *gomock.Controller + recorder *MockDynamoDBAPIMockRecorder +} + +// MockDynamoDBAPIMockRecorder is the mock recorder for MockDynamoDBAPI. +type MockDynamoDBAPIMockRecorder struct { + mock *MockDynamoDBAPI +} + +// NewMockDynamoDBAPI creates a new mock instance. +func NewMockDynamoDBAPI(ctrl *gomock.Controller) *MockDynamoDBAPI { + mock := &MockDynamoDBAPI{ctrl: ctrl} + mock.recorder = &MockDynamoDBAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDynamoDBAPI) EXPECT() *MockDynamoDBAPIMockRecorder { + return m.recorder +} + +// PutItem mocks base method. +func (m *MockDynamoDBAPI) PutItem(ctx context.Context, input *dynamodb.PutItemInput, opts ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PutItem", varargs...) + ret0, _ := ret[0].(*dynamodb.PutItemOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutItem indicates an expected call of PutItem. +func (mr *MockDynamoDBAPIMockRecorder) PutItem(ctx, input interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutItem", reflect.TypeOf((*MockDynamoDBAPI)(nil).PutItem), varargs...) +} + +// Query mocks base method. +func (m *MockDynamoDBAPI) Query(ctx context.Context, input *dynamodb.QueryInput, opts ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Query", varargs...) + ret0, _ := ret[0].(*dynamodb.QueryOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockDynamoDBAPIMockRecorder) Query(ctx, input interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDynamoDBAPI)(nil).Query), varargs...) +} diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index af6e601..82a3054 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -2,64 +2,145 @@ package helper import ( "context" + "errors" "testing" + "github.com/ShareFrame/user-management/internal/helper/mocks" "github.com/ShareFrame/user-management/internal/models" - "github.com/ShareFrame/user-management/internal/postgres" - "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) -type mockPostgresClient struct { - mock.Mock -} +func TestValidateAndFormatUser(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() -var _ postgres.PostgresDBService = (*mockPostgresClient)(nil) + mockDB := mocks.NewMockDatabaseService(ctrl) -func (m *mockPostgresClient) CheckEmailExists(ctx context.Context, email string) (bool, error) { - args := m.Called(ctx, email) - return args.Bool(0), args.Error(1) -} + tests := []struct { + name string + input models.UserRequest + mockSetup func() + expectError bool + errorMsg string + }{ + { + name: "Valid User Input", + input: models.UserRequest{ + Handle: "validuser", + Email: "valid@example.com", + Password: "Valid@123", + }, + mockSetup: func() { + mockDB.EXPECT().CheckEmailExists(gomock.Any(), "valid@example.com").Return(false, nil).Times(1) + }, + expectError: false, + }, + { + name: "Database Error", + input: models.UserRequest{ + Handle: "validuser", + Email: "valid@example.com", + Password: "Valid@123", + }, + mockSetup: func() { + mockDB.EXPECT().CheckEmailExists(gomock.Any(), "valid@example.com").Return(false, errors.New("DB connection failed")).Times(1) + }, + expectError: true, + errorMsg: "internal error: failed to check email", + }, + { + name: "Blocked Username", + input: models.UserRequest{ + Handle: "admin", + Email: "valid@example.com", + Password: "Valid@123", + }, + mockSetup: func() { + mockDB.EXPECT().CheckEmailExists(gomock.Any(), gomock.Any()).Times(0) + }, + expectError: true, + errorMsg: BlockedHandle, + }, + { + name: "Handle Too Short", + input: models.UserRequest{ + Handle: "a", + Email: "valid@example.com", + Password: "Valid@123", + }, + mockSetup: func() { + mockDB.EXPECT().CheckEmailExists(gomock.Any(), gomock.Any()).Times(0) + }, + expectError: true, + errorMsg: HandleTooShort, + }, + { + name: "Empty Email But Valid Handle & Password", + input: models.UserRequest{ + Handle: "validuser", + Email: "", + Password: "Valid@123", + }, + mockSetup: func() { + mockDB.EXPECT().CheckEmailExists(gomock.Any(), gomock.Any()).Times(0) + }, + expectError: true, + errorMsg: "handle, email, and password are required fields", + }, + { + name: "Database Unexpected Error", + input: models.UserRequest{ + Handle: "validuser", + Email: "unexpected@example.com", + Password: "Valid@123", + }, + mockSetup: func() { + mockDB.EXPECT().CheckEmailExists(gomock.Any(), "unexpected@example.com").Return(false, errors.New("some unexpected DB error")).Times(1) + }, + expectError: true, + errorMsg: "internal error: failed to check email", + }, + + } -func (m *mockPostgresClient) StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error { - args := m.Called(ctx, user, event) - return args.Error(0) -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() -type mockSecretsManagerClient struct { - mock.Mock -} + _, err := ValidateAndFormatUser(context.TODO(), tt.input, mockDB) -func (m *mockSecretsManagerClient) GetSecretValue(ctx context.Context, - input *secretsmanager.GetSecretValueInput, opts ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - args := m.Called(ctx, input) - if args.Get(0) != nil { - return args.Get(0).(*secretsmanager.GetSecretValueOutput), args.Error(1) + if tt.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + } else { + assert.NoError(t, err) + } + }) } - return nil, args.Error(1) } -func TestValidatePassword(t *testing.T) { +func TestValidateHandle(t *testing.T) { tests := []struct { name string - password string - expectedErr string + handle string + expectError bool + errorMsg string }{ - {"Valid Password", "Strong@123", ""}, - {"Too Short", "Short1!", PasswordError}, - {"No Uppercase", "weakpassword1!", PasswordError}, - {"No Lowercase", "WEAKPASSWORD1!", PasswordError}, - {"No Digit", "NoDigits!!", PasswordError}, - {"No Special Character", "NoSpecial1", PasswordError}, + {"Valid Handle", "validuser", false, ""}, + {"Too Short", "a", true, HandleTooShort}, + {"Too Long", "averylongusernamethatexceeds18", true, HandleTooLong}, + {"Invalid Characters", "invalid user!", true, InvalidHandle}, + {"Blocked Handle", "admin", true, BlockedHandle}, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := ValidatePassword(test.password) - if test.expectedErr != "" { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateHandle(tt.handle) + + if tt.expectError { assert.Error(t, err) - assert.Equal(t, test.expectedErr, err.Error()) + assert.Contains(t, err.Error(), tt.errorMsg) } else { assert.NoError(t, err) } @@ -70,42 +151,20 @@ func TestValidatePassword(t *testing.T) { func TestEnsureHandleSuffix(t *testing.T) { tests := []struct { name string - handle string + input string expected string }{ - {"Already Has Suffix", "username.shareframe.social", "username.shareframe.social"}, - {"Missing Suffix", "username", "username.shareframe.social"}, + {"Already has suffix", "user" + PDS_Suffix, "user" + PDS_Suffix}, + {"Missing suffix", "user", "user" + PDS_Suffix}, + {"Handle has trailing space", "user ", "user" + PDS_Suffix}, + {"Handle has uppercase", "UserName", "UserName" + PDS_Suffix}, + {"Handle has both leading and trailing space", " user ", "user" + PDS_Suffix}, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - result := EnsureHandleSuffix(test.handle) - assert.Equal(t, test.expected, result) - }) - } -} - -func TestValidateHandle(t *testing.T) { - tests := []struct { - name string - handle string - expectedErr string - }{ - {"Valid Handle", "validuser", ""}, - {"Too Short", "ab", HandleTooShort}, - {"Too Long", "thisisaverylonghandle", HandleTooLong}, - {"Contains Special Characters", "invalid@handle", InvalidHandle}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := ValidateHandle(test.handle) - if test.expectedErr != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedErr) - } else { - assert.NoError(t, err) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EnsureHandleSuffix(tt.input) + assert.Equal(t, tt.expected, result) }) } } @@ -114,20 +173,21 @@ func TestValidateEmail(t *testing.T) { tests := []struct { name string email string - expectedErr string + expectError bool }{ - {"Valid Email", "user@example.com", ""}, - {"Missing @", "userexample.com", "invalid email format"}, - {"Missing domain", "user@", "invalid email format"}, - {"Invalid TLD", "user@example.c", "invalid email format"}, + {"Valid Email", "test@example.com", false}, + {"Invalid Email", "invalid-email", true}, + {"Missing Domain", "user@", true}, + {"No Username", "@example.com", true}, + {"Email with subdomain", "user@mail.example.com", false}, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := ValidateEmail(test.email) - if test.expectedErr != "" { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateEmail(tt.email) + + if tt.expectError { assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedErr) } else { assert.NoError(t, err) } @@ -135,84 +195,29 @@ func TestValidateEmail(t *testing.T) { } } -func TestValidateAndFormatUser(t *testing.T) { - ctx := context.Background() - +func TestValidatePassword(t *testing.T) { tests := []struct { - name string - user models.UserRequest - mockEmailExists bool - mockEmailErr error - expectCheckEmail bool - expectedErr string + name string + password string + expectError bool }{ - {"Valid User", models.UserRequest{Handle: "validuser", Email: "user@example.com", Password: "Valid@123"}, false, nil, true, ""}, - {"Missing Handle", models.UserRequest{Handle: "", Email: "user@example.com", Password: "Valid@123"}, false, nil, false, MissingFields}, - {"Missing Email", models.UserRequest{Handle: "validuser", Email: "", Password: "Valid@123"}, false, nil, false, MissingFields}, - {"Missing Password", models.UserRequest{Handle: "validuser", Email: "user@example.com", Password: ""}, false, nil, false, MissingFields}, - {"Invalid Handle", models.UserRequest{Handle: "inv@lid", Email: "user@example.com", Password: "Valid@123"}, false, nil, false, InvalidHandle}, - {"Invalid Email", models.UserRequest{Handle: "validuser", Email: "invalid-email", Password: "Valid@123"}, false, nil, false, "invalid email format"}, - {"Invalid Password", models.UserRequest{Handle: "validuser", Email: "user@example.com", Password: "weak"}, false, nil, false, PasswordError}, - {"Email Already Exists", models.UserRequest{Handle: "validuser", Email: "user@example.com", Password: "Valid@123"}, true, nil, true, EmailTaken}, - {"DB Check Failure", models.UserRequest{Handle: "validuser", Email: "user@example.com", Password: "Valid@123"}, false, assert.AnError, true, "internal error: failed to check email"}, + {"Valid Password", "Password1@", false}, + {"Too Short", "Pw1!", true}, + {"No Uppercase", "password1@", true}, + {"No Lowercase", "PASSWORD1@", true}, + {"No Digit", "Password!@", true}, + {"No Special Char", "Password123", true}, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - mockDB := new(mockPostgresClient) - - if test.expectCheckEmail { - mockDB.On("CheckEmailExists", ctx, test.user.Email).Return(test.mockEmailExists, test.mockEmailErr) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidatePassword(tt.password) - _, err := ValidateAndFormatUser(ctx, test.user, mockDB) - - if test.expectedErr != "" { + if tt.expectError { assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedErr) } else { assert.NoError(t, err) } - - if test.expectCheckEmail { - mockDB.AssertCalled(t, "CheckEmailExists", ctx, test.user.Email) - } else { - mockDB.AssertNotCalled(t, "CheckEmailExists", ctx, test.user.Email) - } - - mockDB.AssertExpectations(t) }) } } - -func TestRetrieveAdminCredentials(t *testing.T) { - mockSecretsManager := new(mockSecretsManagerClient) - ctx := context.Background() - - mockSecretValue := `{"PDS_JWT_SECRET":"jwtsecret","PDS_ADMIN_USERNAME":"admin","PDS_ADMIN_PASSWORD":"securepass"}` - mockSecretsManager.On("GetSecretValue", ctx, mock.Anything).Return(&secretsmanager.GetSecretValueOutput{ - SecretString: &mockSecretValue, - }, nil) - - creds, err := RetrieveAdminCredentials(ctx, mockSecretsManager) - assert.NoError(t, err) - assert.Equal(t, "jwtsecret", creds.PDSJWTSecret) - assert.Equal(t, "admin", creds.PDSAdminUsername) - assert.Equal(t, "securepass", creds.PDSAdminPassword) -} - -func TestRetrieveUtilAccountCreds(t *testing.T) { - mockSecretsManager := new(mockSecretsManagerClient) - ctx := context.Background() - - mockSecretValue := `{"username":"util-user","password":"util-pass","did":"did:example:123"}` - mockSecretsManager.On("GetSecretValue", ctx, mock.Anything).Return(&secretsmanager.GetSecretValueOutput{ - SecretString: &mockSecretValue, - }, nil) - - creds, err := RetrieveUtilAccountCreds(ctx, mockSecretsManager) - assert.NoError(t, err) - assert.Equal(t, "util-user", creds.Username) - assert.Equal(t, "util-pass", creds.Password) - assert.Equal(t, "did:example:123", creds.DID) -} diff --git a/internal/helper/mocks/mock_helper.go b/internal/helper/mocks/mock_helper.go new file mode 100644 index 0000000..871cd3f --- /dev/null +++ b/internal/helper/mocks/mock_helper.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: helper.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + models "github.com/ShareFrame/user-management/internal/models" + gomock "github.com/golang/mock/gomock" +) + +// MockDatabaseService is a mock of DatabaseService interface. +type MockDatabaseService struct { + ctrl *gomock.Controller + recorder *MockDatabaseServiceMockRecorder +} + +// MockDatabaseServiceMockRecorder is the mock recorder for MockDatabaseService. +type MockDatabaseServiceMockRecorder struct { + mock *MockDatabaseService +} + +// NewMockDatabaseService creates a new mock instance. +func NewMockDatabaseService(ctrl *gomock.Controller) *MockDatabaseService { + mock := &MockDatabaseService{ctrl: ctrl} + mock.recorder = &MockDatabaseServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDatabaseService) EXPECT() *MockDatabaseServiceMockRecorder { + return m.recorder +} + +// CheckEmailExists mocks base method. +func (m *MockDatabaseService) CheckEmailExists(ctx context.Context, email string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckEmailExists", ctx, email) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckEmailExists indicates an expected call of CheckEmailExists. +func (mr *MockDatabaseServiceMockRecorder) CheckEmailExists(ctx, email interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckEmailExists", reflect.TypeOf((*MockDatabaseService)(nil).CheckEmailExists), ctx, email) +} + +// StoreUser mocks base method. +func (m *MockDatabaseService) StoreUser(ctx context.Context, user models.CreateUserResponse, event models.UserRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreUser", ctx, user, event) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreUser indicates an expected call of StoreUser. +func (mr *MockDatabaseServiceMockRecorder) StoreUser(ctx, user, event interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUser", reflect.TypeOf((*MockDatabaseService)(nil).StoreUser), ctx, user, event) +} diff --git a/internal/postgres/db_test.go b/internal/postgres/db_test.go deleted file mode 100644 index 082509e..0000000 --- a/internal/postgres/db_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "testing" - - "github.com/ShareFrame/user-management/internal/models" - "github.com/aws/aws-sdk-go-v2/service/rdsdata" - "github.com/aws/aws-sdk-go-v2/service/rdsdata/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -type mockRDSClient struct { - mock.Mock -} - -func (m *mockRDSClient) ExecuteStatement(ctx context.Context, input *rdsdata.ExecuteStatementInput, opts ...func(*rdsdata.Options)) (*rdsdata.ExecuteStatementOutput, error) { - args := m.Called(ctx, input) - if args.Get(0) != nil { - return args.Get(0).(*rdsdata.ExecuteStatementOutput), args.Error(1) - } - return nil, args.Error(1) -} - -func TestStoreUser(t *testing.T) { - mockClient := new(mockRDSClient) - ctx := context.Background() - - user := models.CreateUserResponse{ - DID: "did:example:123", - Handle: "testuser", - } - - event := models.UserRequest{ - Email: "test@example.com", - } - - tests := []struct { - name string - mockOutput *rdsdata.ExecuteStatementOutput - mockError error - expectedErr string - }{ - { - name: "Successful User Storage", - mockOutput: &rdsdata.ExecuteStatementOutput{}, - mockError: nil, - expectedErr: "", - }, - { - name: "Database Error", - mockOutput: nil, - mockError: errors.New("DB connection failed"), - expectedErr: "failed to store user in PostgreSQL: DB connection failed", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - mockClient.ExpectedCalls = nil - - db := &PostgresDB{ - Client: mockClient, - DBClusterARN: "test-cluster", - SecretARN: "test-secret", - DatabaseName: "test-db", - } - - if test.mockError != nil { - mockClient.On("ExecuteStatement", mock.Anything, mock.Anything). - Return((*rdsdata.ExecuteStatementOutput)(nil), test.mockError) - } else { - mockClient.On("ExecuteStatement", mock.Anything, mock.Anything). - Return(test.mockOutput, nil) - } - - err := db.StoreUser(ctx, user, event) - - if test.expectedErr != "" { - assert.Error(t, err, "Expected an error but got nil") - assert.Contains(t, err.Error(), test.expectedErr) - } else { - assert.NoError(t, err) - } - - mockClient.AssertExpectations(t) - }) - } -} - - -func TestCheckEmailExists(t *testing.T) { - mockClient := new(mockRDSClient) - ctx := context.Background() - - tests := []struct { - name string - mockOutput *rdsdata.ExecuteStatementOutput - mockError error - expectedBool bool - expectedErr string - }{ - { - name: "Email Exists", - mockOutput: &rdsdata.ExecuteStatementOutput{ - Records: [][]types.Field{ - {&types.FieldMemberStringValue{Value: "1"}}, - }, - }, - mockError: nil, - expectedBool: true, - expectedErr: "", - }, - { - name: "Email Does Not Exist", - mockOutput: &rdsdata.ExecuteStatementOutput{ - Records: [][]types.Field{}, - }, - mockError: nil, - expectedBool: false, - expectedErr: "", - }, - { - name: "Database Connection Failure", - mockOutput: nil, - mockError: errors.New("DB connection failed"), - expectedBool: false, - expectedErr: "failed to check email existence: DB connection failed", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - mockClient.ExpectedCalls = nil - - db := &PostgresDB{ - Client: mockClient, - DBClusterARN: "test-cluster", - SecretARN: "test-secret", - DatabaseName: "test-db", - } - - mockClient.On("ExecuteStatement", mock.Anything, mock.Anything). - Return(test.mockOutput, test.mockError) - - result, err := db.CheckEmailExists(ctx, "test@example.com") - - assert.Equal(t, test.expectedBool, result) - - if test.expectedErr == "" { - assert.NoError(t, err) - } else { - assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedErr) - } - - mockClient.AssertExpectations(t) - }) - } -} - From 98ea8f16630855667afa69139e558f03127c13a6 Mon Sep 17 00:00:00 2001 From: uprightsleepy Date: Fri, 7 Mar 2025 22:56:34 -0600 Subject: [PATCH 4/4] updated db code to remove duplicate did storage --- internal/db/db.go | 4 +--- internal/db/db_test.go | 34 +++++++--------------------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/internal/db/db.go b/internal/db/db.go index da3b2c2..694e812 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -49,7 +49,6 @@ func (d *DynamoDBClient) StoreUser(ctx context.Context, user models.CreateUserRe item := map[string]types.AttributeValue{ "UserId": &types.AttributeValueMemberS{Value: user.DID}, - "Did": &types.AttributeValueMemberS{Value: user.DID}, "Email": &types.AttributeValueMemberS{Value: event.Email}, "Handle": &types.AttributeValueMemberS{Value: user.Handle}, "CreatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, @@ -70,14 +69,13 @@ func (d *DynamoDBClient) StoreUser(ctx context.Context, user models.CreateUserRe Item: item, }) if err != nil { - logrus.Errorf("Failed to store user: %v", err) return fmt.Errorf("failed to store user in DynamoDB: %w", err) } - logrus.Infof("User %s successfully stored in DynamoDB", user.Handle) return nil } + func (d *DynamoDBClient) CheckEmailExists(ctx context.Context, email string) (bool, error) { ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 8c047c2..6d47bb4 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -2,10 +2,8 @@ package db import ( "context" - "encoding/json" "errors" "testing" - "time" "github.com/ShareFrame/user-management/internal/db/mocks" "github.com/ShareFrame/user-management/internal/models" @@ -35,29 +33,6 @@ func TestStoreUser(t *testing.T) { dynamoClient := &DynamoDBClient{Client: mockDynamoDB} testUser, testEvent := createTestUser() - themeJSON, _ := json.Marshal(map[string]interface{}{}) - - expectedInput := &dynamodb.PutItemInput{ - TableName: aws.String(TableName), - Item: map[string]types.AttributeValue{ - "UserId": &types.AttributeValueMemberS{Value: testUser.DID}, - "Did": &types.AttributeValueMemberS{Value: testUser.DID}, - "Email": &types.AttributeValueMemberS{Value: testEvent.Email}, - "Handle": &types.AttributeValueMemberS{Value: testUser.Handle}, - "CreatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - "ModifiedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - "Status": &types.AttributeValueMemberS{Value: "active"}, - "Verified": &types.AttributeValueMemberBOOL{Value: false}, - "Role": &types.AttributeValueMemberS{Value: "user"}, - "DisplayName": &types.AttributeValueMemberS{Value: testUser.Handle}, - "ProfilePicture": &types.AttributeValueMemberS{Value: ""}, - "ProfileBanner": &types.AttributeValueMemberS{Value: ""}, - "Theme": &types.AttributeValueMemberS{Value: string(themeJSON)}, - "PrimaryColor": &types.AttributeValueMemberS{Value: "#FFFFFF"}, - "SecondaryColor": &types.AttributeValueMemberS{Value: "#000000"}, - }, - } - tests := []struct { name string mockSetup func() @@ -66,14 +41,18 @@ func TestStoreUser(t *testing.T) { { name: "Successful StoreUser", mockSetup: func() { - mockDynamoDB.EXPECT().PutItem(gomock.Any(), expectedInput).Return(&dynamodb.PutItemOutput{}, nil) + mockDynamoDB.EXPECT(). + PutItem(gomock.Any(), gomock.AssignableToTypeOf(&dynamodb.PutItemInput{})). + Return(&dynamodb.PutItemOutput{}, nil) }, expectError: false, }, { name: "Failed StoreUser (DynamoDB error)", mockSetup: func() { - mockDynamoDB.EXPECT().PutItem(gomock.Any(), expectedInput).Return(nil, errors.New("DynamoDB error")) + mockDynamoDB.EXPECT(). + PutItem(gomock.Any(), gomock.AssignableToTypeOf(&dynamodb.PutItemInput{})). + Return(nil, errors.New("DynamoDB error")) }, expectError: true, }, @@ -93,6 +72,7 @@ func TestStoreUser(t *testing.T) { } } + func TestCheckEmailExists(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()