Skip to content

Commit

Permalink
add unit test for users service
Browse files Browse the repository at this point in the history
  • Loading branch information
negrel committed Jan 15, 2024
1 parent 36049ba commit baa218c
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 39 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
.config/genenv.local.sh

node_modules/

# gomock
*_mock_test.go
6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,8 @@ lint/fix:
$(MAKE) -C ./tests lint/fix

.PHONY: codegen
codegen: ./cmd/server/wire_gen.go

./cmd/server/wire_gen.go: $(wildcard ./cmd/server/*.go)
wire ./...
codegen:
go generate ./...

$(GENENV_FILE):
@echo "$(GENENV_FILE) doesn't exist, generating one..."
Expand Down
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
default = pkgs.mkShell {
buildInputs = with pkgs; [
go
mockgen
gopls
golangci-lint
wire
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ require (
github.com/labstack/echo/v4 v4.11.4
github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.4
go.uber.org/mock v0.4.0
)

require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/google/subcommands v1.0.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/tools v0.10.0 // indirect
)

require (
Expand All @@ -34,7 +38,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lib/pq v1.10.9
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
Expand Down Expand Up @@ -99,13 +100,16 @@ github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7Fw
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
Expand Down Expand Up @@ -140,6 +144,7 @@ golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
Expand Down
7 changes: 7 additions & 0 deletions internal/services/users/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package users

type CreateCmd struct {
UserName UserName
Email Email
Password Password
}
15 changes: 0 additions & 15 deletions internal/services/users/models.go

This file was deleted.

24 changes: 14 additions & 10 deletions internal/services/users/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,43 @@ package users

import (
"context"
"errors"
"fmt"

"github.com/prismelabs/prismeanalytics/internal/models"
"github.com/prismelabs/prismeanalytics/internal/secret"
"golang.org/x/crypto/bcrypt"
)

var (
ErrUserNotFound = errors.New("user not found")
)

// Service define user management service.
type Service interface {
CreateUser(context.Context, CreateCmd) (models.UserId, error)
CreateUser(context.Context, CreateCmd) (UserId, error)
}

// ProvideService define a wire provider for user Service.
func ProvideService(store Store) Service {
return service{store}
func ProvideService(s store) Service {
return service{s}
}

type service struct {
store Store
store store
}

// CreateUser implements Service.
func (s service) CreateUser(ctx context.Context, cmd CreateCmd) (models.UserId, error) {
uid := models.NewUserId()
func (s service) CreateUser(ctx context.Context, cmd CreateCmd) (UserId, error) {
uid := NewUserId()

hashedPassword, err := bcrypt.GenerateFromPassword([]byte(cmd.Password.ExposeSecret()), bcrypt.DefaultCost)
if err != nil {
return models.UserId{}, fmt.Errorf("failed to hash password: %w", err)
return UserId{}, fmt.Errorf("failed to hash password: %w", err)
}

err = s.store.InsertUser(ctx, uid, cmd.UserName, cmd.Email, PasswordHash(secret.New(hashedPassword)))
err = s.store.InsertUser(ctx, uid, cmd.UserName, cmd.Email, secret.New(hashedPassword))
if err != nil {
return models.UserId{}, fmt.Errorf("failed to insert user in store: %w", err)
return UserId{}, fmt.Errorf("failed to insert user in store: %w", err)
}

// TODO: sent verification email and implement email verification.
Expand Down
72 changes: 72 additions & 0 deletions internal/services/users/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package users

import (
"context"
"testing"

"github.com/prismelabs/prismeanalytics/internal/secret"
"github.com/prismelabs/prismeanalytics/internal/testutils"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)

func TestService(t *testing.T) {
t.Run("CreateUser", func(t *testing.T) {
ctx := context.Background()

t.Run("UserAlreadyExists", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := NewMockStore(ctrl)

service := ProvideService(store)

username := testutils.Must(NewUserName)("foo")
email := testutils.Must(NewEmail)("foo@example.com")

store.EXPECT().InsertUser(
ctx,
gomock.Any(), // user id
username,
email,
gomock.Any(), // password
).Return(ErrUserAlreadyExists)

userId, err := service.CreateUser(ctx, CreateCmd{
UserName: username,
Email: email,
Password: testutils.Must(NewPassword)(secret.New("p4ssW0rd!")),
})
require.Error(t, err)
require.ErrorIs(t, err, ErrUserAlreadyExists)
require.Equal(t, UserId{}, userId)
})

t.Run("EmailNotUsed", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := NewMockStore(ctrl)

service := ProvideService(store)

username := testutils.Must(NewUserName)("foo")
email := testutils.Must(NewEmail)("foo@example.com")

store.EXPECT().InsertUser(
ctx,
gomock.Any(), // user id
username,
email,
gomock.Any(), // password
).Return(nil)

userId, err := service.CreateUser(ctx, CreateCmd{
UserName: username,
Email: email,
Password: testutils.Must(NewPassword)(secret.New("p4ssW0rd!")),
})
require.NoError(t, err)
require.NotEqual(t, UserId{}, userId)
})
})
}
29 changes: 20 additions & 9 deletions internal/services/users/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@ package users
import (
"context"
"database/sql"
"errors"

"github.com/prismelabs/prismeanalytics/internal/models"
"github.com/lib/pq"
"github.com/prismelabs/prismeanalytics/internal/postgres"
"github.com/prismelabs/prismeanalytics/internal/secret"
)

// Store define a user store.
type Store interface {
InsertUser(context.Context, models.UserId, models.UserName, models.Email, PasswordHash) error
var (
ErrUserAlreadyExists = errors.New("user already exists")
)

// store define a user store.
//
//go:generate mockgen -source store.go -destination store_mock_test.go -package users -mock_names store=MockStore store
type store interface {
InsertUser(context.Context, UserId, UserName, Email, secret.Secret[[]byte]) error
}

// ProvideStore define a wire provider for user store.
func ProvideStore(pg postgres.Pg) Store {
return store{pg.DB}
func ProvideStore(pg postgres.Pg) store {
return pgStore{pg.DB}
}

type store struct {
type pgStore struct {
db *sql.DB
}

// InsertUser implements Store.
func (s store) InsertUser(ctx context.Context, userId models.UserId, userName models.UserName, email models.Email, passwordHash PasswordHash) error {
_, err := s.db.ExecContext(
func (pgs pgStore) InsertUser(ctx context.Context, userId UserId, userName UserName, email Email, passwordHash secret.Secret[[]byte]) error {
_, err := pgs.db.ExecContext(
ctx,
"INSERT INTO users VALUES ($1, $2, $3, $4, NOW())",
userId,
Expand All @@ -34,6 +41,10 @@ func (s store) InsertUser(ctx context.Context, userId models.UserId, userName mo
secret.Secret[[]byte](passwordHash).ExposeSecret(),
)
if err != nil {
var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Code.Name() == "unique_violation" {
return ErrUserAlreadyExists
}
return err
}

Expand Down
11 changes: 11 additions & 0 deletions internal/services/users/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package users

import "time"

type User struct {
Id UserId
Email Email
Password Password
Name UserName
CreatedAt time.Time
}
12 changes: 12 additions & 0 deletions internal/testutils/must.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package testutils

func Must[I, R any](fn func(I) (R, error)) func(I) R {
return func(value I) R {
result, err := fn(value)
if err != nil {
panic(err)
}

return result
}
}

0 comments on commit baa218c

Please sign in to comment.