mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-14 03:54:47 +00:00
Support for password resets
This commit is contained in:
74
users/handler/create.go
Normal file
74
users/handler/create.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Create a user
|
||||
func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
|
||||
// validate the request
|
||||
if len(req.FirstName) == 0 {
|
||||
return ErrMissingFirstName
|
||||
}
|
||||
if len(req.LastName) == 0 {
|
||||
return ErrMissingLastName
|
||||
}
|
||||
if len(req.Email) == 0 {
|
||||
return ErrMissingEmail
|
||||
}
|
||||
if !isEmailValid(req.Email) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
|
||||
// hash and salt the password using bcrypt
|
||||
phash, err := hashAndSalt(req.Password)
|
||||
if err != nil {
|
||||
logger.Errorf("Error hasing and salting password: %v", err)
|
||||
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// write the user to the database
|
||||
user := &User{
|
||||
ID: uuid.New().String(),
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Email: strings.ToLower(req.Email),
|
||||
Password: phash,
|
||||
}
|
||||
err = u.DB.Create(user).Error
|
||||
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
|
||||
return ErrDuplicateEmail
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// generate a token for the user
|
||||
token := Token{
|
||||
UserID: user.ID,
|
||||
Key: uuid.New().String(),
|
||||
ExpiresAt: u.Time().Add(time.Hour * 24 * 7),
|
||||
}
|
||||
if err := tx.Create(&token).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.User = user.Serialize()
|
||||
rsp.Token = token.Key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
129
users/handler/create_test.go
Normal file
129
users/handler/create_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
tt := []struct {
|
||||
Name string
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Password string
|
||||
Error error
|
||||
}{
|
||||
{
|
||||
Name: "MissingFirstName",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "password",
|
||||
Error: handler.ErrMissingFirstName,
|
||||
},
|
||||
{
|
||||
Name: "MissingLastName",
|
||||
FirstName: "John",
|
||||
Email: "john@doe.com",
|
||||
Password: "password",
|
||||
Error: handler.ErrMissingLastName,
|
||||
},
|
||||
{
|
||||
Name: "MissingEmail",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Password: "password",
|
||||
Error: handler.ErrMissingEmail,
|
||||
},
|
||||
{
|
||||
Name: "InvalidEmail",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Password: "password",
|
||||
Email: "foo.foo.foo",
|
||||
Error: handler.ErrInvalidEmail,
|
||||
},
|
||||
{
|
||||
Name: "InvalidPassword",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "pwd",
|
||||
Error: handler.ErrInvalidPassword,
|
||||
},
|
||||
}
|
||||
|
||||
// test the validations
|
||||
h := testHandler(t)
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
err := h.Create(context.TODO(), &pb.CreateRequest{
|
||||
FirstName: tc.FirstName,
|
||||
LastName: tc.LastName,
|
||||
Email: tc.Email,
|
||||
Password: tc.Password,
|
||||
}, &pb.CreateResponse{})
|
||||
assert.Equal(t, tc.Error, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
var rsp pb.CreateResponse
|
||||
req := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
u := rsp.User
|
||||
if u == nil {
|
||||
t.Fatalf("No user returned")
|
||||
}
|
||||
assert.NotEmpty(t, u.Id)
|
||||
assert.Equal(t, req.FirstName, u.FirstName)
|
||||
assert.Equal(t, req.LastName, u.LastName)
|
||||
assert.Equal(t, req.Email, u.Email)
|
||||
assert.NotEmpty(t, rsp.Token)
|
||||
})
|
||||
|
||||
t.Run("DuplicateEmail", func(t *testing.T) {
|
||||
var rsp pb.CreateResponse
|
||||
req := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
assert.Equal(t, handler.ErrDuplicateEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("DifferentEmail", func(t *testing.T) {
|
||||
var rsp pb.CreateResponse
|
||||
req := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
u := rsp.User
|
||||
if u == nil {
|
||||
t.Fatalf("No user returned")
|
||||
}
|
||||
assert.NotEmpty(t, u.Id)
|
||||
assert.Equal(t, req.FirstName, u.FirstName)
|
||||
assert.Equal(t, req.LastName, u.LastName)
|
||||
assert.Equal(t, req.Email, u.Email)
|
||||
})
|
||||
}
|
||||
34
users/handler/delete.go
Normal file
34
users/handler/delete.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Delete a user
|
||||
func (u *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
|
||||
// validate the request
|
||||
if len(req.Id) == 0 {
|
||||
return ErrMissingID
|
||||
}
|
||||
|
||||
// delete the users tokens
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Delete(&Token{}, &Token{UserID: req.Id}).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// delete from the database
|
||||
if err := tx.Delete(&User{}, &User{ID: req.Id}).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
56
users/handler/delete_test.go
Normal file
56
users/handler/delete_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingID", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var cRsp pb.CreateResponse
|
||||
cReq := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{
|
||||
Id: cRsp.User.Id,
|
||||
}, &pb.DeleteResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check it was actually deleted
|
||||
var rsp pb.ReadResponse
|
||||
err = h.Read(context.TODO(), &pb.ReadRequest{
|
||||
Ids: []string{cRsp.User.Id},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, rsp.Users[cRsp.User.Id])
|
||||
})
|
||||
|
||||
t.Run("Retry", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{
|
||||
Id: cRsp.User.Id,
|
||||
}, &pb.DeleteResponse{})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
@@ -21,6 +17,7 @@ var (
|
||||
ErrDuplicateEmail = errors.BadRequest("DUPLICATE_EMAIL", "A user with this email address already exists")
|
||||
ErrInvalidEmail = errors.BadRequest("INVALID_EMAIL", "The email provided is invalid")
|
||||
ErrInvalidPassword = errors.BadRequest("INVALID_PASSWORD", "Password must be at least 8 characters long")
|
||||
ErrMissingEmails = errors.BadRequest("MISSING_EMAILS", "One or more emails are required")
|
||||
ErrMissingIDs = errors.BadRequest("MISSING_IDS", "One or more ids are required")
|
||||
ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID")
|
||||
ErrMissingToken = errors.BadRequest("MISSING_TOKEN", "Missing token")
|
||||
@@ -65,288 +62,6 @@ type Users struct {
|
||||
Time func() time.Time
|
||||
}
|
||||
|
||||
// Create a user
|
||||
func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
|
||||
// validate the request
|
||||
if len(req.FirstName) == 0 {
|
||||
return ErrMissingFirstName
|
||||
}
|
||||
if len(req.LastName) == 0 {
|
||||
return ErrMissingLastName
|
||||
}
|
||||
if len(req.Email) == 0 {
|
||||
return ErrMissingEmail
|
||||
}
|
||||
if !isEmailValid(req.Email) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
|
||||
// hash and salt the password using bcrypt
|
||||
phash, err := hashAndSalt(req.Password)
|
||||
if err != nil {
|
||||
logger.Errorf("Error hasing and salting password: %v", err)
|
||||
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// write the user to the database
|
||||
user := &User{
|
||||
ID: uuid.New().String(),
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Email: strings.ToLower(req.Email),
|
||||
Password: phash,
|
||||
}
|
||||
err = u.DB.Create(user).Error
|
||||
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
|
||||
return ErrDuplicateEmail
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// generate a token for the user
|
||||
token := Token{
|
||||
UserID: user.ID,
|
||||
Key: uuid.New().String(),
|
||||
ExpiresAt: u.Time().Add(time.Hour * 24 * 7),
|
||||
}
|
||||
if err := tx.Create(&token).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.User = user.Serialize()
|
||||
rsp.Token = token.Key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Read users using ID
|
||||
func (u *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
|
||||
// validate the request
|
||||
if len(req.Ids) == 0 {
|
||||
return ErrMissingIDs
|
||||
}
|
||||
|
||||
// query the database
|
||||
var users []User
|
||||
if err := u.DB.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Users = make(map[string]*pb.User, len(users))
|
||||
for _, u := range users {
|
||||
rsp.Users[u.ID] = u.Serialize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update a user
|
||||
func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
|
||||
// validate the request
|
||||
if len(req.Id) == 0 {
|
||||
return ErrMissingID
|
||||
}
|
||||
if req.FirstName != nil && len(req.FirstName.Value) == 0 {
|
||||
return ErrMissingFirstName
|
||||
}
|
||||
if req.LastName != nil && len(req.LastName.Value) == 0 {
|
||||
return ErrMissingLastName
|
||||
}
|
||||
if req.Email != nil && len(req.Email.Value) == 0 {
|
||||
return ErrMissingEmail
|
||||
}
|
||||
if req.Email != nil && !isEmailValid(req.Email.Value) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := u.DB.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// assign the updated values
|
||||
if req.FirstName != nil {
|
||||
user.FirstName = req.FirstName.Value
|
||||
}
|
||||
if req.LastName != nil {
|
||||
user.LastName = req.LastName.Value
|
||||
}
|
||||
if req.Email != nil {
|
||||
user.Email = strings.ToLower(req.Email.Value)
|
||||
}
|
||||
|
||||
// write the user to the database
|
||||
err := u.DB.Save(user).Error
|
||||
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
|
||||
return ErrDuplicateEmail
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the user
|
||||
rsp.User = user.Serialize()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a user
|
||||
func (u *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
|
||||
// validate the request
|
||||
if len(req.Id) == 0 {
|
||||
return ErrMissingID
|
||||
}
|
||||
|
||||
// delete the users tokens
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Delete(&Token{}, &Token{UserID: req.Id}).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// delete from the database
|
||||
if err := tx.Delete(&User{}, &User{ID: req.Id}).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// List all users
|
||||
func (u *Users) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
|
||||
// query the database
|
||||
var users []User
|
||||
if err := u.DB.Model(&User{}).Find(&users).Error; err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Users = make([]*pb.User, len(users))
|
||||
for i, u := range users {
|
||||
rsp.Users[i] = u.Serialize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login using email and password returns the users profile and a token
|
||||
func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error {
|
||||
// validate the request
|
||||
if len(req.Email) == 0 {
|
||||
return ErrMissingEmail
|
||||
}
|
||||
if len(req.Password) == 0 {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := tx.Where(&User{Email: req.Email}).First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// compare the passwords
|
||||
if !passwordsMatch(user.Password, req.Password) {
|
||||
return ErrIncorrectPassword
|
||||
}
|
||||
|
||||
// generate a token for the user
|
||||
token := Token{
|
||||
UserID: user.ID,
|
||||
Key: uuid.New().String(),
|
||||
ExpiresAt: u.Time().Add(tokenTTL),
|
||||
}
|
||||
if err := tx.Create(&token).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Token = token.Key
|
||||
rsp.User = user.Serialize()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Logout expires all tokens for the user
|
||||
func (u *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error {
|
||||
// validate the request
|
||||
if len(req.Id) == 0 {
|
||||
return ErrMissingID
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := tx.Where(&User{ID: req.Id}).Preload("Tokens").First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// delete the tokens
|
||||
if err := tx.Delete(user.Tokens).Error; err != nil {
|
||||
logger.Errorf("Error deleting from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Validate a token, each time a token is validated it extends its lifetime for another week
|
||||
func (u *Users) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error {
|
||||
// validate the request
|
||||
if len(req.Token) == 0 {
|
||||
return ErrMissingToken
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// lookup the token
|
||||
var token Token
|
||||
if err := tx.Where(&Token{Key: req.Token}).Preload("User").First(&token).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrInvalidToken
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// ensure the token is valid
|
||||
if u.Time().After(token.ExpiresAt) {
|
||||
return ErrTokenExpired
|
||||
}
|
||||
|
||||
// extend the token for another lifetime
|
||||
token.ExpiresAt = u.Time().Add(tokenTTL)
|
||||
if err := tx.Save(&token).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.User = token.User.Serialize()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// isEmailValid checks if the email provided passes the required structure and length.
|
||||
func isEmailValid(e string) bool {
|
||||
if len(e) < 3 && len(e) > 254 {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
@@ -35,627 +32,6 @@ func testHandler(t *testing.T) *handler.Users {
|
||||
return &handler.Users{DB: db, Time: time.Now}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
tt := []struct {
|
||||
Name string
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Password string
|
||||
Error error
|
||||
}{
|
||||
{
|
||||
Name: "MissingFirstName",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "password",
|
||||
Error: handler.ErrMissingFirstName,
|
||||
},
|
||||
{
|
||||
Name: "MissingLastName",
|
||||
FirstName: "John",
|
||||
Email: "john@doe.com",
|
||||
Password: "password",
|
||||
Error: handler.ErrMissingLastName,
|
||||
},
|
||||
{
|
||||
Name: "MissingEmail",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Password: "password",
|
||||
Error: handler.ErrMissingEmail,
|
||||
},
|
||||
{
|
||||
Name: "InvalidEmail",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Password: "password",
|
||||
Email: "foo.foo.foo",
|
||||
Error: handler.ErrInvalidEmail,
|
||||
},
|
||||
{
|
||||
Name: "InvalidPassword",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "pwd",
|
||||
Error: handler.ErrInvalidPassword,
|
||||
},
|
||||
}
|
||||
|
||||
// test the validations
|
||||
h := testHandler(t)
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
err := h.Create(context.TODO(), &pb.CreateRequest{
|
||||
FirstName: tc.FirstName,
|
||||
LastName: tc.LastName,
|
||||
Email: tc.Email,
|
||||
Password: tc.Password,
|
||||
}, &pb.CreateResponse{})
|
||||
assert.Equal(t, tc.Error, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
var rsp pb.CreateResponse
|
||||
req := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
u := rsp.User
|
||||
if u == nil {
|
||||
t.Fatalf("No user returned")
|
||||
}
|
||||
assert.NotEmpty(t, u.Id)
|
||||
assert.Equal(t, req.FirstName, u.FirstName)
|
||||
assert.Equal(t, req.LastName, u.LastName)
|
||||
assert.Equal(t, req.Email, u.Email)
|
||||
assert.NotEmpty(t, rsp.Token)
|
||||
})
|
||||
|
||||
t.Run("DuplicateEmail", func(t *testing.T) {
|
||||
var rsp pb.CreateResponse
|
||||
req := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
assert.Equal(t, handler.ErrDuplicateEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("DifferentEmail", func(t *testing.T) {
|
||||
var rsp pb.CreateResponse
|
||||
req := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
u := rsp.User
|
||||
if u == nil {
|
||||
t.Fatalf("No user returned")
|
||||
}
|
||||
assert.NotEmpty(t, u.Id)
|
||||
assert.Equal(t, req.FirstName, u.FirstName)
|
||||
assert.Equal(t, req.LastName, u.LastName)
|
||||
assert.Equal(t, req.Email, u.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingIDs", func(t *testing.T) {
|
||||
var rsp pb.ReadResponse
|
||||
err := h.Read(context.TODO(), &pb.ReadRequest{}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingIDs, err)
|
||||
assert.Nil(t, rsp.Users)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var rsp pb.ReadResponse
|
||||
err := h.Read(context.TODO(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp)
|
||||
assert.Nil(t, err)
|
||||
if rsp.Users == nil {
|
||||
t.Fatal("Expected the users object to not be nil")
|
||||
}
|
||||
assert.Nil(t, rsp.Users["foo"])
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var rsp1 pb.CreateResponse
|
||||
req1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req1, &rsp1)
|
||||
assert.NoError(t, err)
|
||||
if rsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var rsp2 pb.CreateResponse
|
||||
req2 := pb.CreateRequest{
|
||||
FirstName: "Apple",
|
||||
LastName: "Tree",
|
||||
Email: "apple@tree.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &req2, &rsp2)
|
||||
assert.NoError(t, err)
|
||||
if rsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
// test the read
|
||||
var rsp pb.ReadResponse
|
||||
err = h.Read(context.TODO(), &pb.ReadRequest{
|
||||
Ids: []string{rsp1.User.Id, rsp2.User.Id},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if rsp.Users == nil {
|
||||
t.Fatal("Users not returned")
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, rsp.Users[rsp1.User.Id])
|
||||
assert.NotNil(t, rsp.Users[rsp2.User.Id])
|
||||
|
||||
// check the users match
|
||||
if u := rsp.Users[rsp1.User.Id]; u != nil {
|
||||
assert.Equal(t, rsp1.User.Id, u.Id)
|
||||
assert.Equal(t, rsp1.User.FirstName, u.FirstName)
|
||||
assert.Equal(t, rsp1.User.LastName, u.LastName)
|
||||
assert.Equal(t, rsp1.User.Email, u.Email)
|
||||
}
|
||||
if u := rsp.Users[rsp2.User.Id]; u != nil {
|
||||
assert.Equal(t, rsp2.User.Id, u.Id)
|
||||
assert.Equal(t, rsp2.User.FirstName, u.FirstName)
|
||||
assert.Equal(t, rsp2.User.LastName, u.LastName)
|
||||
assert.Equal(t, rsp2.User.Email, u.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingID", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{Id: "foo"}, &rsp)
|
||||
assert.Equal(t, handler.ErrNotFound, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var cRsp1 pb.CreateResponse
|
||||
cReq1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var cRsp2 pb.CreateResponse
|
||||
cReq2 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("BlankFirstName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingFirstName, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("BlankLastName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingLastName, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("BlankLastName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingLastName, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("BlankEmail", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("InvalidEmail", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrInvalidEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("EmailAlreadyExists", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrDuplicateEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
uReq := pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id,
|
||||
Email: &wrapperspb.StringValue{Value: "foobar@gmail.com"},
|
||||
FirstName: &wrapperspb.StringValue{Value: "Foo"},
|
||||
LastName: &wrapperspb.StringValue{Value: "Bar"},
|
||||
}
|
||||
var uRsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &uReq, &uRsp)
|
||||
assert.NoError(t, err)
|
||||
if uRsp.User == nil {
|
||||
t.Error("No user returned")
|
||||
return
|
||||
}
|
||||
assert.Equal(t, cRsp1.User.Id, uRsp.User.Id)
|
||||
assert.Equal(t, uReq.Email.Value, uRsp.User.Email)
|
||||
assert.Equal(t, uReq.FirstName.Value, uRsp.User.FirstName)
|
||||
assert.Equal(t, uReq.LastName.Value, uRsp.User.LastName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingID", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var cRsp pb.CreateResponse
|
||||
cReq := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{
|
||||
Id: cRsp.User.Id,
|
||||
}, &pb.DeleteResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check it was actually deleted
|
||||
var rsp pb.ReadResponse
|
||||
err = h.Read(context.TODO(), &pb.ReadRequest{
|
||||
Ids: []string{cRsp.User.Id},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, rsp.Users[cRsp.User.Id])
|
||||
})
|
||||
|
||||
t.Run("Retry", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{
|
||||
Id: cRsp.User.Id,
|
||||
}, &pb.DeleteResponse{})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
// create some mock data
|
||||
var cRsp1 pb.CreateResponse
|
||||
cReq1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var cRsp2 pb.CreateResponse
|
||||
cReq2 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var rsp pb.ListResponse
|
||||
err = h.List(context.TODO(), &pb.ListRequest{}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
if rsp.Users == nil {
|
||||
t.Error("No users returned")
|
||||
return
|
||||
}
|
||||
|
||||
var u1Found, u2Found bool
|
||||
for _, u := range rsp.Users {
|
||||
switch u.Id {
|
||||
case cRsp1.User.Id:
|
||||
assertUsersMatch(t, cRsp1.User, u)
|
||||
u1Found = true
|
||||
case cRsp2.User.Id:
|
||||
assertUsersMatch(t, cRsp2.User, u)
|
||||
u2Found = true
|
||||
default:
|
||||
t.Fatal("Unexpected user returned")
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.True(t, u1Found)
|
||||
assert.True(t, u2Found)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
// create some mock data
|
||||
var cRsp pb.CreateResponse
|
||||
cReq := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
Name string
|
||||
Email string
|
||||
Password string
|
||||
Error error
|
||||
User *pb.User
|
||||
}{
|
||||
{
|
||||
Name: "MissingEmail",
|
||||
Password: "passwordabc",
|
||||
Error: handler.ErrMissingEmail,
|
||||
},
|
||||
{
|
||||
Name: "MissingPassword",
|
||||
Email: "john@doe.com",
|
||||
Error: handler.ErrInvalidPassword,
|
||||
},
|
||||
{
|
||||
Name: "UserNotFound",
|
||||
Email: "foo@bar.com",
|
||||
Password: "passwordabc",
|
||||
Error: handler.ErrNotFound,
|
||||
},
|
||||
{
|
||||
Name: "IncorrectPassword",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabcdef",
|
||||
Error: handler.ErrIncorrectPassword,
|
||||
},
|
||||
{
|
||||
Name: "Valid",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
User: cRsp.User,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
var rsp pb.LoginResponse
|
||||
err := h.Login(context.TODO(), &pb.LoginRequest{
|
||||
Email: tc.Email, Password: tc.Password,
|
||||
}, &rsp)
|
||||
assert.Equal(t, tc.Error, err)
|
||||
|
||||
if tc.User != nil {
|
||||
assertUsersMatch(t, tc.User, rsp.User)
|
||||
assert.NotEmpty(t, rsp.Token)
|
||||
} else {
|
||||
assert.Nil(t, tc.User)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingUserID", func(t *testing.T) {
|
||||
err := h.Logout(context.TODO(), &pb.LogoutRequest{}, &pb.LogoutResponse{})
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
})
|
||||
|
||||
t.Run("UserNotFound", func(t *testing.T) {
|
||||
err := h.Logout(context.TODO(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{})
|
||||
assert.Equal(t, handler.ErrNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
// create some mock data
|
||||
var cRsp pb.CreateResponse
|
||||
cReq := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Logout(context.TODO(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = h.Validate(context.TODO(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
// create some mock data
|
||||
var cRsp1 pb.CreateResponse
|
||||
cReq1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var cRsp2 pb.CreateResponse
|
||||
cReq2 := pb.CreateRequest{
|
||||
FirstName: "Barry",
|
||||
LastName: "Doe",
|
||||
Email: "barry@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
Name string
|
||||
Token string
|
||||
Time func() time.Time
|
||||
Error error
|
||||
User *pb.User
|
||||
}{
|
||||
{
|
||||
Name: "MissingToken",
|
||||
Error: handler.ErrMissingToken,
|
||||
},
|
||||
{
|
||||
Name: "InvalidToken",
|
||||
Error: handler.ErrInvalidToken,
|
||||
Token: uuid.New().String(),
|
||||
},
|
||||
{
|
||||
Name: "ExpiredToken",
|
||||
Error: handler.ErrTokenExpired,
|
||||
Token: cRsp1.Token,
|
||||
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
|
||||
},
|
||||
{
|
||||
Name: "ValidToken",
|
||||
User: cRsp2.User,
|
||||
Token: cRsp2.Token,
|
||||
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 3) },
|
||||
},
|
||||
{
|
||||
Name: "RefreshedToken",
|
||||
User: cRsp2.User,
|
||||
Token: cRsp2.Token,
|
||||
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if tc.Time == nil {
|
||||
h.Time = time.Now
|
||||
} else {
|
||||
h.Time = tc.Time
|
||||
}
|
||||
|
||||
var rsp pb.ValidateResponse
|
||||
err := h.Validate(context.TODO(), &pb.ValidateRequest{Token: tc.Token}, &rsp)
|
||||
assert.Equal(t, tc.Error, err)
|
||||
|
||||
if tc.User != nil {
|
||||
assertUsersMatch(t, tc.User, rsp.User)
|
||||
} else {
|
||||
assert.Nil(t, tc.User)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertUsersMatch(t *testing.T, exp, act *pb.User) {
|
||||
if act == nil {
|
||||
t.Error("No user returned")
|
||||
|
||||
26
users/handler/list.go
Normal file
26
users/handler/list.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
)
|
||||
|
||||
// List all users
|
||||
func (u *Users) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
|
||||
// query the database
|
||||
var users []User
|
||||
if err := u.DB.Model(&User{}).Find(&users).Error; err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Users = make([]*pb.User, len(users))
|
||||
for i, u := range users {
|
||||
rsp.Users[i] = u.Serialize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
67
users/handler/list_test.go
Normal file
67
users/handler/list_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
// create some mock data
|
||||
var cRsp1 pb.CreateResponse
|
||||
cReq1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var cRsp2 pb.CreateResponse
|
||||
cReq2 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var rsp pb.ListResponse
|
||||
err = h.List(context.TODO(), &pb.ListRequest{}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
if rsp.Users == nil {
|
||||
t.Error("No users returned")
|
||||
return
|
||||
}
|
||||
|
||||
var u1Found, u2Found bool
|
||||
for _, u := range rsp.Users {
|
||||
switch u.Id {
|
||||
case cRsp1.User.Id:
|
||||
assertUsersMatch(t, cRsp1.User, u)
|
||||
u1Found = true
|
||||
case cRsp2.User.Id:
|
||||
assertUsersMatch(t, cRsp2.User, u)
|
||||
u2Found = true
|
||||
default:
|
||||
t.Fatal("Unexpected user returned")
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.True(t, u1Found)
|
||||
assert.True(t, u2Found)
|
||||
}
|
||||
54
users/handler/login.go
Normal file
54
users/handler/login.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Login using email and password returns the users profile and a token
|
||||
func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error {
|
||||
// validate the request
|
||||
if len(req.Email) == 0 {
|
||||
return ErrMissingEmail
|
||||
}
|
||||
if len(req.Password) == 0 {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := tx.Where(&User{Email: req.Email}).First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// compare the passwords
|
||||
if !passwordsMatch(user.Password, req.Password) {
|
||||
return ErrIncorrectPassword
|
||||
}
|
||||
|
||||
// generate a token for the user
|
||||
token := Token{
|
||||
UserID: user.ID,
|
||||
Key: uuid.New().String(),
|
||||
ExpiresAt: u.Time().Add(tokenTTL),
|
||||
}
|
||||
if err := tx.Create(&token).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Token = token.Key
|
||||
rsp.User = user.Serialize()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
83
users/handler/login_test.go
Normal file
83
users/handler/login_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
// create some mock data
|
||||
var cRsp pb.CreateResponse
|
||||
cReq := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
Name string
|
||||
Email string
|
||||
Password string
|
||||
Error error
|
||||
User *pb.User
|
||||
}{
|
||||
{
|
||||
Name: "MissingEmail",
|
||||
Password: "passwordabc",
|
||||
Error: handler.ErrMissingEmail,
|
||||
},
|
||||
{
|
||||
Name: "MissingPassword",
|
||||
Email: "john@doe.com",
|
||||
Error: handler.ErrInvalidPassword,
|
||||
},
|
||||
{
|
||||
Name: "UserNotFound",
|
||||
Email: "foo@bar.com",
|
||||
Password: "passwordabc",
|
||||
Error: handler.ErrNotFound,
|
||||
},
|
||||
{
|
||||
Name: "IncorrectPassword",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabcdef",
|
||||
Error: handler.ErrIncorrectPassword,
|
||||
},
|
||||
{
|
||||
Name: "Valid",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
User: cRsp.User,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
var rsp pb.LoginResponse
|
||||
err := h.Login(context.TODO(), &pb.LoginRequest{
|
||||
Email: tc.Email, Password: tc.Password,
|
||||
}, &rsp)
|
||||
assert.Equal(t, tc.Error, err)
|
||||
|
||||
if tc.User != nil {
|
||||
assertUsersMatch(t, tc.User, rsp.User)
|
||||
assert.NotEmpty(t, rsp.Token)
|
||||
} else {
|
||||
assert.Nil(t, tc.User)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
37
users/handler/logout.go
Normal file
37
users/handler/logout.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Logout expires all tokens for the user
|
||||
func (u *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error {
|
||||
// validate the request
|
||||
if len(req.Id) == 0 {
|
||||
return ErrMissingID
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := tx.Where(&User{ID: req.Id}).Preload("Tokens").First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// delete the tokens
|
||||
if err := tx.Delete(user.Tokens).Error; err != nil {
|
||||
logger.Errorf("Error deleting from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
48
users/handler/logout_test.go
Normal file
48
users/handler/logout_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingUserID", func(t *testing.T) {
|
||||
err := h.Logout(context.TODO(), &pb.LogoutRequest{}, &pb.LogoutResponse{})
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
})
|
||||
|
||||
t.Run("UserNotFound", func(t *testing.T) {
|
||||
err := h.Logout(context.TODO(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{})
|
||||
assert.Equal(t, handler.ErrNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
// create some mock data
|
||||
var cRsp pb.CreateResponse
|
||||
cReq := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Logout(context.TODO(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = h.Validate(context.TODO(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
31
users/handler/read.go
Normal file
31
users/handler/read.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
)
|
||||
|
||||
// Read users using ID
|
||||
func (u *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
|
||||
// validate the request
|
||||
if len(req.Ids) == 0 {
|
||||
return ErrMissingIDs
|
||||
}
|
||||
|
||||
// query the database
|
||||
var users []User
|
||||
if err := u.DB.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Users = make(map[string]*pb.User, len(users))
|
||||
for _, u := range users {
|
||||
rsp.Users[u.ID] = u.Serialize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
36
users/handler/read_by_email.go
Normal file
36
users/handler/read_by_email.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
)
|
||||
|
||||
// Read users using email
|
||||
func (u *Users) ReadByEmail(ctx context.Context, req *pb.ReadByEmailRequest, rsp *pb.ReadByEmailResponse) error {
|
||||
// validate the request
|
||||
if len(req.Emails) == 0 {
|
||||
return ErrMissingEmails
|
||||
}
|
||||
emails := make([]string, len(req.Emails))
|
||||
for i, e := range req.Emails {
|
||||
emails[i] = strings.ToLower(e)
|
||||
}
|
||||
|
||||
// query the database
|
||||
var users []User
|
||||
if err := u.DB.Model(&User{}).Where("lower(email) IN (?)", emails).Find(&users).Error; err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.Users = make(map[string]*pb.User, len(users))
|
||||
for _, u := range users {
|
||||
rsp.Users[u.Email] = u.Serialize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
89
users/handler/read_by_email_test.go
Normal file
89
users/handler/read_by_email_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReadByEmail(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingEmails", func(t *testing.T) {
|
||||
var rsp pb.ReadByEmailResponse
|
||||
err := h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingEmails, err)
|
||||
assert.Nil(t, rsp.Users)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var rsp pb.ReadByEmailResponse
|
||||
err := h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{Emails: []string{"foo"}}, &rsp)
|
||||
assert.Nil(t, err)
|
||||
if rsp.Users == nil {
|
||||
t.Fatal("Expected the users object to not be nil")
|
||||
}
|
||||
assert.Nil(t, rsp.Users["foo"])
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var rsp1 pb.CreateResponse
|
||||
req1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req1, &rsp1)
|
||||
assert.NoError(t, err)
|
||||
if rsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var rsp2 pb.CreateResponse
|
||||
req2 := pb.CreateRequest{
|
||||
FirstName: "Apple",
|
||||
LastName: "Tree",
|
||||
Email: "apple@tree.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &req2, &rsp2)
|
||||
assert.NoError(t, err)
|
||||
if rsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
// test the read
|
||||
var rsp pb.ReadByEmailResponse
|
||||
err = h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{
|
||||
Emails: []string{rsp1.User.Email, strings.ToUpper(rsp2.User.Email)},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if rsp.Users == nil {
|
||||
t.Fatal("Users not returned")
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, rsp.Users[rsp1.User.Email])
|
||||
assert.NotNil(t, rsp.Users[rsp2.User.Email])
|
||||
|
||||
// check the users match
|
||||
if u := rsp.Users[rsp1.User.Email]; u != nil {
|
||||
assert.Equal(t, rsp1.User.Id, u.Id)
|
||||
assert.Equal(t, rsp1.User.FirstName, u.FirstName)
|
||||
assert.Equal(t, rsp1.User.LastName, u.LastName)
|
||||
assert.Equal(t, rsp1.User.Email, u.Email)
|
||||
}
|
||||
if u := rsp.Users[rsp2.User.Email]; u != nil {
|
||||
assert.Equal(t, rsp2.User.Id, u.Id)
|
||||
assert.Equal(t, rsp2.User.FirstName, u.FirstName)
|
||||
assert.Equal(t, rsp2.User.LastName, u.LastName)
|
||||
assert.Equal(t, rsp2.User.Email, u.Email)
|
||||
}
|
||||
}
|
||||
88
users/handler/read_test.go
Normal file
88
users/handler/read_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingIDs", func(t *testing.T) {
|
||||
var rsp pb.ReadResponse
|
||||
err := h.Read(context.TODO(), &pb.ReadRequest{}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingIDs, err)
|
||||
assert.Nil(t, rsp.Users)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var rsp pb.ReadResponse
|
||||
err := h.Read(context.TODO(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp)
|
||||
assert.Nil(t, err)
|
||||
if rsp.Users == nil {
|
||||
t.Fatal("Expected the users object to not be nil")
|
||||
}
|
||||
assert.Nil(t, rsp.Users["foo"])
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var rsp1 pb.CreateResponse
|
||||
req1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req1, &rsp1)
|
||||
assert.NoError(t, err)
|
||||
if rsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var rsp2 pb.CreateResponse
|
||||
req2 := pb.CreateRequest{
|
||||
FirstName: "Apple",
|
||||
LastName: "Tree",
|
||||
Email: "apple@tree.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &req2, &rsp2)
|
||||
assert.NoError(t, err)
|
||||
if rsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
// test the read
|
||||
var rsp pb.ReadResponse
|
||||
err = h.Read(context.TODO(), &pb.ReadRequest{
|
||||
Ids: []string{rsp1.User.Id, rsp2.User.Id},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if rsp.Users == nil {
|
||||
t.Fatal("Users not returned")
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, rsp.Users[rsp1.User.Id])
|
||||
assert.NotNil(t, rsp.Users[rsp2.User.Id])
|
||||
|
||||
// check the users match
|
||||
if u := rsp.Users[rsp1.User.Id]; u != nil {
|
||||
assert.Equal(t, rsp1.User.Id, u.Id)
|
||||
assert.Equal(t, rsp1.User.FirstName, u.FirstName)
|
||||
assert.Equal(t, rsp1.User.LastName, u.LastName)
|
||||
assert.Equal(t, rsp1.User.Email, u.Email)
|
||||
}
|
||||
if u := rsp.Users[rsp2.User.Id]; u != nil {
|
||||
assert.Equal(t, rsp2.User.Id, u.Id)
|
||||
assert.Equal(t, rsp2.User.FirstName, u.FirstName)
|
||||
assert.Equal(t, rsp2.User.LastName, u.LastName)
|
||||
assert.Equal(t, rsp2.User.Email, u.Email)
|
||||
}
|
||||
}
|
||||
75
users/handler/update.go
Normal file
75
users/handler/update.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Update a user
|
||||
func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
|
||||
// validate the request
|
||||
if len(req.Id) == 0 {
|
||||
return ErrMissingID
|
||||
}
|
||||
if req.FirstName != nil && len(req.FirstName.Value) == 0 {
|
||||
return ErrMissingFirstName
|
||||
}
|
||||
if req.LastName != nil && len(req.LastName.Value) == 0 {
|
||||
return ErrMissingLastName
|
||||
}
|
||||
if req.Email != nil && len(req.Email.Value) == 0 {
|
||||
return ErrMissingEmail
|
||||
}
|
||||
if req.Email != nil && !isEmailValid(req.Email.Value) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
if req.Password != nil && len(req.Password.Value) < 8 {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := u.DB.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// assign the updated values
|
||||
if req.FirstName != nil {
|
||||
user.FirstName = req.FirstName.Value
|
||||
}
|
||||
if req.LastName != nil {
|
||||
user.LastName = req.LastName.Value
|
||||
}
|
||||
if req.Email != nil {
|
||||
user.Email = strings.ToLower(req.Email.Value)
|
||||
}
|
||||
if req.Password != nil {
|
||||
p, err := hashAndSalt(req.Password.Value)
|
||||
if err != nil {
|
||||
logger.Errorf("Error hasing and salting password: %v", err)
|
||||
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
|
||||
}
|
||||
user.Password = p
|
||||
}
|
||||
|
||||
// write the user to the database
|
||||
err := u.DB.Save(user).Error
|
||||
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
|
||||
return ErrDuplicateEmail
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the user
|
||||
rsp.User = user.Serialize()
|
||||
return nil
|
||||
}
|
||||
148
users/handler/update_test.go
Normal file
148
users/handler/update_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingID", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{Id: "foo"}, &rsp)
|
||||
assert.Equal(t, handler.ErrNotFound, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
// create some mock data
|
||||
var cRsp1 pb.CreateResponse
|
||||
cReq1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var cRsp2 pb.CreateResponse
|
||||
cReq2 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("BlankFirstName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingFirstName, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("BlankLastName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingLastName, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("BlankLastName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingLastName, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("BlankEmail", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("InvalidEmail", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrInvalidEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("EmailAlreadyExists", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrDuplicateEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
uReq := pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id,
|
||||
Email: &wrapperspb.StringValue{Value: "foobar@gmail.com"},
|
||||
FirstName: &wrapperspb.StringValue{Value: "Foo"},
|
||||
LastName: &wrapperspb.StringValue{Value: "Bar"},
|
||||
}
|
||||
var uRsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &uReq, &uRsp)
|
||||
assert.NoError(t, err)
|
||||
if uRsp.User == nil {
|
||||
t.Error("No user returned")
|
||||
return
|
||||
}
|
||||
assert.Equal(t, cRsp1.User.Id, uRsp.User.Id)
|
||||
assert.Equal(t, uReq.Email.Value, uRsp.User.Email)
|
||||
assert.Equal(t, uReq.FirstName.Value, uRsp.User.FirstName)
|
||||
assert.Equal(t, uReq.LastName.Value, uRsp.User.LastName)
|
||||
})
|
||||
|
||||
t.Run("UpdatePassword", func(t *testing.T) {
|
||||
uReq := pb.UpdateRequest{
|
||||
Id: cRsp2.User.Id,
|
||||
Password: &wrapperspb.StringValue{Value: "helloworld"},
|
||||
}
|
||||
err := h.Update(context.TODO(), &uReq, &pb.UpdateResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
lReq := pb.LoginRequest{
|
||||
Email: cRsp2.User.Email,
|
||||
Password: "helloworld",
|
||||
}
|
||||
err = h.Login(context.TODO(), &lReq, &pb.LoginResponse{})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
45
users/handler/validate.go
Normal file
45
users/handler/validate.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Validate a token, each time a token is validated it extends its lifetime for another week
|
||||
func (u *Users) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error {
|
||||
// validate the request
|
||||
if len(req.Token) == 0 {
|
||||
return ErrMissingToken
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// lookup the token
|
||||
var token Token
|
||||
if err := tx.Where(&Token{Key: req.Token}).Preload("User").First(&token).Error; err == gorm.ErrRecordNotFound {
|
||||
return ErrInvalidToken
|
||||
} else if err != nil {
|
||||
logger.Errorf("Error reading from the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// ensure the token is valid
|
||||
if u.Time().After(token.ExpiresAt) {
|
||||
return ErrTokenExpired
|
||||
}
|
||||
|
||||
// extend the token for another lifetime
|
||||
token.ExpiresAt = u.Time().Add(tokenTTL)
|
||||
if err := tx.Save(&token).Error; err != nil {
|
||||
logger.Errorf("Error writing to the database: %v", err)
|
||||
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
|
||||
}
|
||||
|
||||
// serialize the response
|
||||
rsp.User = token.User.Serialize()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
101
users/handler/validate_test.go
Normal file
101
users/handler/validate_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
// create some mock data
|
||||
var cRsp1 pb.CreateResponse
|
||||
cReq1 := pb.CreateRequest{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
var cRsp2 pb.CreateResponse
|
||||
cReq2 := pb.CreateRequest{
|
||||
FirstName: "Barry",
|
||||
LastName: "Doe",
|
||||
Email: "barry@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
Name string
|
||||
Token string
|
||||
Time func() time.Time
|
||||
Error error
|
||||
User *pb.User
|
||||
}{
|
||||
{
|
||||
Name: "MissingToken",
|
||||
Error: handler.ErrMissingToken,
|
||||
},
|
||||
{
|
||||
Name: "InvalidToken",
|
||||
Error: handler.ErrInvalidToken,
|
||||
Token: uuid.New().String(),
|
||||
},
|
||||
{
|
||||
Name: "ExpiredToken",
|
||||
Error: handler.ErrTokenExpired,
|
||||
Token: cRsp1.Token,
|
||||
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
|
||||
},
|
||||
{
|
||||
Name: "ValidToken",
|
||||
User: cRsp2.User,
|
||||
Token: cRsp2.Token,
|
||||
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 3) },
|
||||
},
|
||||
{
|
||||
Name: "RefreshedToken",
|
||||
User: cRsp2.User,
|
||||
Token: cRsp2.Token,
|
||||
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if tc.Time == nil {
|
||||
h.Time = time.Now
|
||||
} else {
|
||||
h.Time = tc.Time
|
||||
}
|
||||
|
||||
var rsp pb.ValidateResponse
|
||||
err := h.Validate(context.TODO(), &pb.ValidateRequest{Token: tc.Token}, &rsp)
|
||||
assert.Equal(t, tc.Error, err)
|
||||
|
||||
if tc.User != nil {
|
||||
assertUsersMatch(t, tc.User, rsp.User)
|
||||
} else {
|
||||
assert.Nil(t, tc.User)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user