tests: implemented go user api tests

This commit is contained in:
Jean Jacques Avril 2025-01-04 21:30:29 +00:00
parent 48aae18736
commit f8933bee15
No known key found for this signature in database
11 changed files with 289 additions and 84 deletions

View File

@ -3,10 +3,14 @@ module actatempus_backend
go 1.23.2 go 1.23.2
require ( require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/steebchen/prisma-client-go v0.45.0 github.com/steebchen/prisma-client-go v0.45.0
github.com/stretchr/testify v1.10.0
) )
require ( require (
@ -14,27 +18,22 @@ require (
github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect github.com/gin-contrib/sse v1.0.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-json v0.10.4 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/arch v0.12.0 // indirect golang.org/x/arch v0.12.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.33.0 // indirect

View File

@ -9,8 +9,6 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -25,6 +23,8 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@ -74,8 +74,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
@ -112,10 +110,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI= go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI=
go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@ -144,4 +138,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,23 +0,0 @@
package services_test
import "testing"
func TestCreateUserService(t *testing.T) {
mockRepo := mocks.NewMockUserRepository()
service := services.NewUserService(mockRepo)
user := entities.UserCreate{
Email: "service@test.com",
Name: "Jane Doe",
Password: "securepassword",
}
result := service.CreateUser(context.Background(), user)
assert.True(t, result.IsRight(), "Expected service to create user")
result.Map(func(user entities.User) {
assert.Equal(t, "service@test.com", user.Email)
assert.Equal(t, "Jane Doe", user.Name)
})
}

View File

@ -0,0 +1,37 @@
package mocks
import (
"actatempus_backend/internal/domain/repository"
"context"
E "github.com/IBM/fp-go/either"
)
// MockAuthRepository is a mock implementation of AuthRepository.
type MockAuthRepository struct{}
// NewMockAuthRepository creates a new instance of MockAuthRepository.
func NewMockAuthRepository() repository.AuthRepository {
return &MockAuthRepository{}
}
// GenerateToken always returns a successful token generation.
func (m *MockAuthRepository) GenerateToken(ctx context.Context) func(userID string) E.Either[error, string] {
return func(userID string) E.Either[error, string] {
return E.Right[error]("mocked-token")
}
}
// ValidateToken always validates a token successfully.
func (m *MockAuthRepository) ValidateToken(ctx context.Context) func(token string) E.Either[error, string] {
return func(token string) E.Either[error, string] {
return E.Right[error]("mocked-user-id")
}
}
// RevokeToken always revokes a token successfully.
func (m *MockAuthRepository) RevokeToken(ctx context.Context) func(token string) E.Either[error, string] {
return func(token string) E.Either[error, string] {
return E.Right[error]("mocked-user-id")
}
}

View File

@ -15,13 +15,13 @@ import (
) )
type MockProjectDataSource struct { type MockProjectDataSource struct {
store map[string]*entities.Project Store map[string]*entities.Project
mu sync.RWMutex mu sync.RWMutex
} }
func NewMockProjectDataSource() data.ProjectDataSource { func NewMockProjectDataSource() data.ProjectDataSource {
return &MockProjectDataSource{ return &MockProjectDataSource{
store: make(map[string]*entities.Project), Store: make(map[string]*entities.Project),
} }
} }
@ -39,7 +39,7 @@ func (ds *MockProjectDataSource) Create(ctx context.Context, project entities.Pr
ClientID: project.ClientID, ClientID: project.ClientID,
} }
ds.store[id] = &newProject ds.Store[id] = &newProject
return E.Right[error](newProject) return E.Right[error](newProject)
} }
@ -50,7 +50,7 @@ func (ds *MockProjectDataSource) FindByID(ctx context.Context, id string) E.Eith
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
return F.Pipe2( return F.Pipe2(
O.FromNillable(ds.store[id]), O.FromNillable(ds.Store[id]),
E.FromOption[*entities.Project](F.Constant(fmt.Errorf("project with ID %s not found", id))), E.FromOption[*entities.Project](F.Constant(fmt.Errorf("project with ID %s not found", id))),
E.Chain(impl.TryDereference[entities.Project]), E.Chain(impl.TryDereference[entities.Project]),
) )
@ -61,7 +61,7 @@ func (ds *MockProjectDataSource) Update(ctx context.Context, project entities.Pr
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[project.ID] existing, found := ds.Store[project.ID]
if !found { if !found {
return E.Left[entities.Project](fmt.Errorf("project with ID %s not found", project.ID)) return E.Left[entities.Project](fmt.Errorf("project with ID %s not found", project.ID))
} }
@ -87,12 +87,12 @@ func (ds *MockProjectDataSource) Delete(ctx context.Context, id string) E.Either
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[id] existing, found := ds.Store[id]
if !found { if !found {
return E.Left[entities.Project](fmt.Errorf("project with ID %s not found", id)) return E.Left[entities.Project](fmt.Errorf("project with ID %s not found", id))
} }
delete(ds.store, id) delete(ds.Store, id)
return E.Right[error](*existing) return E.Right[error](*existing)
} }
@ -101,8 +101,8 @@ func (ds *MockProjectDataSource) FindAll(ctx context.Context) E.Either[error, []
ds.mu.RLock() ds.mu.RLock()
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
projects := make([]entities.Project, 0, len(ds.store)) projects := make([]entities.Project, 0, len(ds.Store))
for _, project := range ds.store { for _, project := range ds.Store {
projects = append(projects, *project) projects = append(projects, *project)
} }
@ -115,7 +115,7 @@ func (ds *MockProjectDataSource) FindByUserID(ctx context.Context, userID string
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
projects := make([]entities.Project, 0) projects := make([]entities.Project, 0)
for _, project := range ds.store { for _, project := range ds.Store {
if project.UserID == userID { if project.UserID == userID {
projects = append(projects, *project) projects = append(projects, *project)
} }
@ -123,3 +123,9 @@ func (ds *MockProjectDataSource) FindByUserID(ctx context.Context, userID string
return E.Right[error](projects) return E.Right[error](projects)
} }
func (ds *MockProjectDataSource) Clear() {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.Store = make(map[string]*entities.Project)
}

View File

@ -14,13 +14,13 @@ import (
) )
type MockProjectTaskDataSource struct { type MockProjectTaskDataSource struct {
store map[string]*entities.ProjectTask Store map[string]*entities.ProjectTask
mu sync.RWMutex mu sync.RWMutex
} }
func NewMockProjectTaskDataSource() data.ProjectTaskDataSource { func NewMockProjectTaskDataSource() data.ProjectTaskDataSource {
return &MockProjectTaskDataSource{ return &MockProjectTaskDataSource{
store: make(map[string]*entities.ProjectTask), Store: make(map[string]*entities.ProjectTask),
} }
} }
@ -37,7 +37,7 @@ func (ds *MockProjectTaskDataSource) Create(ctx context.Context, task entities.P
Description: task.Description, Description: task.Description,
} }
ds.store[id] = &newTask ds.Store[id] = &newTask
return E.Right[error](newTask) return E.Right[error](newTask)
} }
@ -48,7 +48,7 @@ func (ds *MockProjectTaskDataSource) FindByID(ctx context.Context, id string) E.
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
return F.Pipe2( return F.Pipe2(
O.FromNillable(ds.store[id]), O.FromNillable(ds.Store[id]),
E.FromOption[*entities.ProjectTask](F.Constant(fmt.Errorf("project task with ID %s not found", id))), E.FromOption[*entities.ProjectTask](F.Constant(fmt.Errorf("project task with ID %s not found", id))),
E.Chain(impl.TryDereference[entities.ProjectTask]), E.Chain(impl.TryDereference[entities.ProjectTask]),
) )
@ -59,7 +59,7 @@ func (ds *MockProjectTaskDataSource) Update(ctx context.Context, task entities.P
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[task.ID] existing, found := ds.Store[task.ID]
if !found { if !found {
return E.Left[entities.ProjectTask](fmt.Errorf("project task with ID %s not found", task.ID)) return E.Left[entities.ProjectTask](fmt.Errorf("project task with ID %s not found", task.ID))
} }
@ -82,12 +82,12 @@ func (ds *MockProjectTaskDataSource) Delete(ctx context.Context, id string) E.Ei
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[id] existing, found := ds.Store[id]
if !found { if !found {
return E.Left[entities.ProjectTask](fmt.Errorf("project task with ID %s not found", id)) return E.Left[entities.ProjectTask](fmt.Errorf("project task with ID %s not found", id))
} }
delete(ds.store, id) delete(ds.Store, id)
return E.Right[error](*existing) return E.Right[error](*existing)
} }
@ -96,8 +96,8 @@ func (ds *MockProjectTaskDataSource) FindAll(ctx context.Context) E.Either[error
ds.mu.RLock() ds.mu.RLock()
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
tasks := make([]entities.ProjectTask, 0, len(ds.store)) tasks := make([]entities.ProjectTask, 0, len(ds.Store))
for _, task := range ds.store { for _, task := range ds.Store {
tasks = append(tasks, *task) tasks = append(tasks, *task)
} }
@ -110,7 +110,7 @@ func (ds *MockProjectTaskDataSource) FindByProjectID(ctx context.Context, projec
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
tasks := make([]entities.ProjectTask, 0) tasks := make([]entities.ProjectTask, 0)
for _, task := range ds.store { for _, task := range ds.Store {
if task.ProjectID == projectID { if task.ProjectID == projectID {
tasks = append(tasks, *task) tasks = append(tasks, *task)
} }
@ -118,3 +118,9 @@ func (ds *MockProjectTaskDataSource) FindByProjectID(ctx context.Context, projec
return E.Right[error](tasks) return E.Right[error](tasks)
} }
func (ds *MockProjectTaskDataSource) Clear() {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.Store = make(map[string]*entities.ProjectTask)
}

View File

@ -3,7 +3,6 @@ package mocks
import ( import (
impl "actatempus_backend/internal/infrastructure/data" impl "actatempus_backend/internal/infrastructure/data"
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities" "actatempus_backend/internal/domain/entities"
"context" "context"
"fmt" "fmt"
@ -15,13 +14,13 @@ import (
) )
type MockTimeEntryDataSource struct { type MockTimeEntryDataSource struct {
store map[string]*entities.TimeEntry Store map[string]*entities.TimeEntry
mu sync.RWMutex mu sync.RWMutex
} }
func NewMockTimeEntryDataSource() data.TimeEntryDataSource { func NewMockTimeEntryDataSource() *MockTimeEntryDataSource {
return &MockTimeEntryDataSource{ return &MockTimeEntryDataSource{
store: make(map[string]*entities.TimeEntry), Store: make(map[string]*entities.TimeEntry),
} }
} }
@ -40,7 +39,7 @@ func (ds *MockTimeEntryDataSource) Create(ctx context.Context, entry entities.Ti
ProjectID: entry.ProjectID, ProjectID: entry.ProjectID,
} }
ds.store[id] = &newEntry ds.Store[id] = &newEntry
return E.Right[error](newEntry) return E.Right[error](newEntry)
} }
@ -51,7 +50,7 @@ func (ds *MockTimeEntryDataSource) FindByID(ctx context.Context, id string) E.Ei
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
return F.Pipe2( return F.Pipe2(
O.FromNillable(ds.store[id]), O.FromNillable(ds.Store[id]),
E.FromOption[*entities.TimeEntry](F.Constant(fmt.Errorf("time entry with ID %s not found", id))), E.FromOption[*entities.TimeEntry](F.Constant(fmt.Errorf("time entry with ID %s not found", id))),
E.Chain(impl.TryDereference[entities.TimeEntry]), E.Chain(impl.TryDereference[entities.TimeEntry]),
) )
@ -62,7 +61,7 @@ func (ds *MockTimeEntryDataSource) Update(ctx context.Context, entry entities.Ti
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[entry.ID] existing, found := ds.Store[entry.ID]
if !found { if !found {
return E.Left[entities.TimeEntry](fmt.Errorf("time entry with ID %s not found", entry.ID)) return E.Left[entities.TimeEntry](fmt.Errorf("time entry with ID %s not found", entry.ID))
} }
@ -91,12 +90,12 @@ func (ds *MockTimeEntryDataSource) Delete(ctx context.Context, id string) E.Eith
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[id] existing, found := ds.Store[id]
if !found { if !found {
return E.Left[entities.TimeEntry](fmt.Errorf("time entry with ID %s not found", id)) return E.Left[entities.TimeEntry](fmt.Errorf("time entry with ID %s not found", id))
} }
delete(ds.store, id) delete(ds.Store, id)
return E.Right[error](*existing) return E.Right[error](*existing)
} }
@ -105,8 +104,8 @@ func (ds *MockTimeEntryDataSource) FindAll(ctx context.Context) E.Either[error,
ds.mu.RLock() ds.mu.RLock()
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
entries := make([]entities.TimeEntry, 0, len(ds.store)) entries := make([]entities.TimeEntry, 0, len(ds.Store))
for _, entry := range ds.store { for _, entry := range ds.Store {
entries = append(entries, *entry) entries = append(entries, *entry)
} }
@ -119,7 +118,7 @@ func (ds *MockTimeEntryDataSource) FindByUserID(ctx context.Context, userID stri
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
entries := make([]entities.TimeEntry, 0) entries := make([]entities.TimeEntry, 0)
for _, entry := range ds.store { for _, entry := range ds.Store {
if entry.UserID == userID { if entry.UserID == userID {
entries = append(entries, *entry) entries = append(entries, *entry)
} }
@ -134,7 +133,7 @@ func (ds *MockTimeEntryDataSource) FindByProjectID(ctx context.Context, projectI
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
entries := make([]entities.TimeEntry, 0) entries := make([]entities.TimeEntry, 0)
for _, entry := range ds.store { for _, entry := range ds.Store {
if entry.ProjectID == projectID { if entry.ProjectID == projectID {
entries = append(entries, *entry) entries = append(entries, *entry)
} }
@ -142,3 +141,9 @@ func (ds *MockTimeEntryDataSource) FindByProjectID(ctx context.Context, projectI
return E.Right[error](entries) return E.Right[error](entries)
} }
func (ds *MockTimeEntryDataSource) Clear() {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.Store = make(map[string]*entities.TimeEntry)
}

View File

@ -1,7 +1,6 @@
package mocks package mocks
import ( import (
domain "actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities" "actatempus_backend/internal/domain/entities"
impl "actatempus_backend/internal/infrastructure/data" impl "actatempus_backend/internal/infrastructure/data"
"context" "context"
@ -14,13 +13,13 @@ import (
) )
type MockUserDataSource struct { type MockUserDataSource struct {
store map[string]*entities.User Store map[string]*entities.User
mu sync.RWMutex mu sync.RWMutex
} }
func NewMockUserDataSource() domain.UserDataSource { func NewMockUserDataSource() *MockUserDataSource {
return &MockUserDataSource{ return &MockUserDataSource{
store: make(map[string]*entities.User), Store: make(map[string]*entities.User),
} }
} }
@ -36,7 +35,7 @@ func (ds *MockUserDataSource) Create(ctx context.Context, user entities.UserCrea
Password: impl.GenerateSecureHash(user.Password), Password: impl.GenerateSecureHash(user.Password),
} }
ds.store[id] = &newUser ds.Store[id] = &newUser
return E.Right[error](newUser) return E.Right[error](newUser)
} }
@ -45,7 +44,7 @@ func (ds *MockUserDataSource) FindByID(ctx context.Context, id string) E.Either[
ds.mu.RLock() ds.mu.RLock()
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
return F.Pipe2( return F.Pipe2(
O.FromNillable(ds.store[id]), O.FromNillable(ds.Store[id]),
E.FromOption[*entities.User](F.Constant(fmt.Errorf("user with ID %s not found", id))), E.FromOption[*entities.User](F.Constant(fmt.Errorf("user with ID %s not found", id))),
E.Chain( E.Chain(
impl.TryDereference[entities.User], impl.TryDereference[entities.User],
@ -57,7 +56,7 @@ func (ds *MockUserDataSource) FindByEmail(ctx context.Context, email string) E.E
ds.mu.RLock() ds.mu.RLock()
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
for _, user := range ds.store { for _, user := range ds.Store {
if user.Email == email { if user.Email == email {
return E.Right[error](*user) return E.Right[error](*user)
} }
@ -69,7 +68,7 @@ func (ds *MockUserDataSource) Update(ctx context.Context, user entities.UserUpda
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[user.ID] existing, found := ds.Store[user.ID]
if !found { if !found {
return E.Left[entities.User](fmt.Errorf("user with ID %s not found", user.ID)) return E.Left[entities.User](fmt.Errorf("user with ID %s not found", user.ID))
} }
@ -91,12 +90,12 @@ func (ds *MockUserDataSource) Delete(ctx context.Context, id string) E.Either[er
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
existing, found := ds.store[id] existing, found := ds.Store[id]
if !found { if !found {
return E.Left[entities.User](fmt.Errorf("user with ID %s not found", id)) return E.Left[entities.User](fmt.Errorf("user with ID %s not found", id))
} }
delete(ds.store, id) delete(ds.Store, id)
return E.Right[error](*existing) return E.Right[error](*existing)
} }
@ -104,10 +103,16 @@ func (ds *MockUserDataSource) FindAll(ctx context.Context) E.Either[error, []ent
ds.mu.RLock() ds.mu.RLock()
defer ds.mu.RUnlock() defer ds.mu.RUnlock()
users := make([]entities.User, 0, len(ds.store)) users := make([]entities.User, 0, len(ds.Store))
for _, user := range ds.store { for _, user := range ds.Store {
users = append(users, *user) users = append(users, *user)
} }
return E.Right[error](users) return E.Right[error](users)
} }
func (ds *MockUserDataSource) Clear() {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.Store = make(map[string]*entities.User)
}

View File

@ -0,0 +1,6 @@
package services_test
// StringPtr returns a pointer to the string value passed in
func StringPtr(s string) *string {
return &s
}

View File

@ -0,0 +1,171 @@
package services_test
import (
"actatempus_backend/internal/application/repository"
"actatempus_backend/internal/application/services"
"actatempus_backend/internal/application/services/dto"
"actatempus_backend/internal/domain/entities"
"fmt"
"github.com/gin-gonic/gin"
"actatempus_backend/tests/mocks"
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
"github.com/stretchr/testify/assert"
)
func TestUserService(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
//mockAuthRepo := repository.NewInMemoryAuthRepositoryImpl("secret")
mockAuthRepo := mocks.NewMockAuthRepository()
userDS := mocks.NewMockUserDataSource()
userRepo := repository.NewUserRepository(userDS)
userService := services.NewUserService(mockAuthRepo, userRepo)
router := gin.Default()
router.RedirectTrailingSlash = false
api := router.Group("/api/users")
userService.RegisterRoutes(api)
t.Run("CreateUser - Success", func(t *testing.T) {
userCreateDTO := dto.UserCreateDTO{
Name: "John Doe",
Email: "john.doe@example.com",
Password: "password123",
}
body, _ := json.Marshal(userCreateDTO)
req := httptest.NewRequest(http.MethodPost, "/api/users/", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
var response dto.UserDTO
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, userCreateDTO.Name, response.Name)
assert.Equal(t, userCreateDTO.Email, response.Email)
})
t.Run("GetUserByID - Success", func(t *testing.T) {
// Prepopulate mock data
user := entities.UserCreate{
Name: "John Doe",
Email: "john.doe@example.com",
Password: "hashed_password",
}
newUser, _ := E.Unwrap(F.Pipe1(
user,
userRepo.Create(context.Background()),
))
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/users/%s", newUser.ID), nil)
req.Header.Set("Cookie", "session_token=fake_test_token")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
print(rec.Body.String())
assert.Equal(t, http.StatusOK, rec.Code)
var response dto.UserDTO
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, user.Name, response.Name)
assert.Equal(t, user.Email, response.Email)
})
t.Run("GetAllUsers - Success", func(t *testing.T) {
userDS.Clear()
// Prepopulate mock data
userRepo.Create(context.Background())(entities.UserCreate{
Name: "John Doe",
Email: "john.doe@example.com",
Password: "hashed_password",
})
userRepo.Create(context.Background())(entities.UserCreate{
Name: "Jane Smith",
Email: "jane.smith@example.com",
Password: "hashed_password",
})
req := httptest.NewRequest(http.MethodGet, "/api/users/", nil)
req.Header.Set("Cookie", "session_token=fake_test_token")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response []dto.UserDTO
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response, 2)
})
t.Run("UpdateUser - Success", func(t *testing.T) {
// Prepopulate mock data
newUser, _ := E.Unwrap(F.Pipe1(
entities.UserCreate{
Name: "John Doe",
Email: "john.doe@example.com",
Password: "hashed_password",
},
userRepo.Create(context.Background()),
))
userUpdateDTO := dto.UserUpdateDTO{
Name: StringPtr("John Updated"),
Email: StringPtr("john.updated@example.com"),
}
body, _ := json.Marshal(userUpdateDTO)
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/users/%s", newUser.ID), bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Cookie", "session_token=fake_test_token")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response dto.UserDTO
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, *userUpdateDTO.Name, response.Name)
assert.Equal(t, *userUpdateDTO.Email, response.Email)
})
t.Run("DeleteUser - Success", func(t *testing.T) {
// Prepopulate mock data
newUser, _ := E.Unwrap(F.Pipe1(
entities.UserCreate{
Name: "John Doe",
Email: "john.doe@example.com",
Password: "hashed_password",
},
userRepo.Create(context.Background()),
))
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/users/%s", newUser.ID), nil)
req.Header.Set("Cookie", "session_token=fake_test_token")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response dto.UserDTO
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "John Doe", response.Name)
})
}