tests: added mock data sources to go backend

This commit is contained in:
Jean Jacques Avril 2025-01-04 20:28:02 +00:00
parent ba15a542b9
commit 48aae18736
No known key found for this signature in database
13 changed files with 567 additions and 9 deletions

View File

@ -95,6 +95,7 @@ All dependencies are bundled with the **devcontainer**. For manual setup, ensure
code . code .
``` ```
--- ---
## 🖥️ Running ActaTempus ## 🖥️ Running ActaTempus
@ -123,7 +124,7 @@ dart run bin/backend_dart.dart # Starts on port 8080
--- ---
## 🗄️ Database Management ### 🗄️ Database Management
ActaTempus uses **Prisma ORM** for database schema management and code generation across both backends. ActaTempus uses **Prisma ORM** for database schema management and code generation across both backends.
Before the backend can connect you need to start the PostgresSQL server. To make things easier you can launch Before the backend can connect you need to start the PostgresSQL server. To make things easier you can launch
@ -135,7 +136,7 @@ docker compose up
### Deploy the Schema #### Deploy the Schema
You only need to apply the schema with one of the following commands, as both result in the same schema. You only need to apply the schema with one of the following commands, as both result in the same schema.
In case if you want to change the corresponding server edit the ```.env```-file within the backend projects. In case if you want to change the corresponding server edit the ```.env```-file within the backend projects.
@ -151,7 +152,7 @@ cd backend-go
go run github.com/steebchen/prisma-client-go db push # within backend-go go run github.com/steebchen/prisma-client-go db push # within backend-go
``` ```
### Prisma Studio (UI) #### Prisma Studio (UI)
Prisma Studio is WebUI that improves development with Databases as it allows looking right into the data as well as well as altering it. Prisma Studio is WebUI that improves development with Databases as it allows looking right into the data as well as well as altering it.
```bash ```bash
@ -163,18 +164,33 @@ bunx prisma studio
To generate ORM Code for the specifig backend run the following commands. To generate ORM Code for the specifig backend run the following commands.
This is usually necessary after changes are made to the projects ``schema.prisma``-file. This is usually necessary after changes are made to the projects ``schema.prisma``-file.
#### Dart ##### Dart
```bash ```bash
cd backend-dart cd backend-dart
bunx prisma generate bunx prisma generate
``` ```
#### Go ##### Go
```bash ```bash
cd backend-go cd backend-go
go run github.com/steebchen/prisma-client-go generate go run github.com/steebchen/prisma-client-go generate
``` ```
---
## ⚠️ Testing
##### Dart
```bash
cd backend-dart
dart test
```
##### Go
```bash
cd backend-go
go test ./... -v
```
--- ---
## 🔧 Known Issues ## 🔧 Known Issues

View File

@ -23,6 +23,7 @@ require (
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/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

View File

@ -38,6 +38,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=

View File

@ -0,0 +1,23 @@
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

@ -1,6 +1,7 @@
package data package data
import ( import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities" "actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db" "actatempus_backend/internal/infrastructure/data/db"
"context" "context"
@ -15,7 +16,7 @@ type PrismaProjectDataSource struct {
client *db.PrismaClient client *db.PrismaClient
} }
func NewPrismaProjectDataSource(client *db.PrismaClient) *PrismaProjectDataSource { func NewPrismaProjectDataSource(client *db.PrismaClient) data.ProjectDataSource {
return &PrismaProjectDataSource{client: client} return &PrismaProjectDataSource{client: client}
} }

View File

@ -1,6 +1,7 @@
package data package data
import ( import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities" "actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db" "actatempus_backend/internal/infrastructure/data/db"
"context" "context"
@ -15,7 +16,7 @@ type PrismaProjectTaskDataSource struct {
client *db.PrismaClient client *db.PrismaClient
} }
func NewPrismaProjectTaskDataSource(client *db.PrismaClient) *PrismaProjectTaskDataSource { func NewPrismaProjectTaskDataSource(client *db.PrismaClient) data.ProjectTaskDataSource {
return &PrismaProjectTaskDataSource{client: client} return &PrismaProjectTaskDataSource{client: client}
} }

View File

@ -1,6 +1,7 @@
package data package data
import ( import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities" "actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db" "actatempus_backend/internal/infrastructure/data/db"
"context" "context"
@ -15,7 +16,7 @@ type PrismaTimeEntryDataSource struct {
client *db.PrismaClient client *db.PrismaClient
} }
func NewPrismaTimeEntryDataSource(client *db.PrismaClient) *PrismaTimeEntryDataSource { func NewPrismaTimeEntryDataSource(client *db.PrismaClient) data.TimeEntryDataSource {
return &PrismaTimeEntryDataSource{client: client} return &PrismaTimeEntryDataSource{client: client}
} }

View File

@ -1,6 +1,7 @@
package data package data
import ( import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities" "actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db" "actatempus_backend/internal/infrastructure/data/db"
"actatempus_backend/internal/utils" "actatempus_backend/internal/utils"
@ -16,7 +17,7 @@ type PrismaUserDataSource struct {
client *db.PrismaClient client *db.PrismaClient
} }
func NewPrismaUserDataSource(client *db.PrismaClient) *PrismaUserDataSource { func NewPrismaUserDataSource(client *db.PrismaClient) data.UserDataSource {
return &PrismaUserDataSource{client: client} return &PrismaUserDataSource{client: client}
} }

View File

@ -0,0 +1,10 @@
package mocks
import (
"github.com/google/uuid"
)
// GenerateID generates a new UUID string.
func GenerateID() string {
return uuid.New().String()
}

View File

@ -0,0 +1,125 @@
package mocks
import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities"
impl "actatempus_backend/internal/infrastructure/data"
"context"
"fmt"
"sync"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
O "github.com/IBM/fp-go/option"
)
type MockProjectDataSource struct {
store map[string]*entities.Project
mu sync.RWMutex
}
func NewMockProjectDataSource() data.ProjectDataSource {
return &MockProjectDataSource{
store: make(map[string]*entities.Project),
}
}
// Create adds a new project to the store
func (ds *MockProjectDataSource) Create(ctx context.Context, project entities.ProjectCreate) E.Either[error, entities.Project] {
ds.mu.Lock()
defer ds.mu.Unlock()
id := GenerateID()
newProject := entities.Project{
ID: id,
Name: project.Name,
UserID: project.UserID,
Description: project.Description,
ClientID: project.ClientID,
}
ds.store[id] = &newProject
return E.Right[error](newProject)
}
// FindByID retrieves a project by its ID
func (ds *MockProjectDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.Project] {
ds.mu.RLock()
defer ds.mu.RUnlock()
return F.Pipe2(
O.FromNillable(ds.store[id]),
E.FromOption[*entities.Project](F.Constant(fmt.Errorf("project with ID %s not found", id))),
E.Chain(impl.TryDereference[entities.Project]),
)
}
// Update modifies an existing project
func (ds *MockProjectDataSource) Update(ctx context.Context, project entities.ProjectUpdate) E.Either[error, entities.Project] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[project.ID]
if !found {
return E.Left[entities.Project](fmt.Errorf("project with ID %s not found", project.ID))
}
if project.Name != nil {
existing.Name = *project.Name
}
if project.Description != nil {
existing.Description = project.Description
}
if project.ClientID != nil {
existing.ClientID = project.ClientID
}
if project.UserID != nil {
existing.UserID = *project.UserID
}
return E.Right[error](*existing)
}
// Delete removes a project from the store
func (ds *MockProjectDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.Project] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[id]
if !found {
return E.Left[entities.Project](fmt.Errorf("project with ID %s not found", id))
}
delete(ds.store, id)
return E.Right[error](*existing)
}
// FindAll retrieves all projects
func (ds *MockProjectDataSource) FindAll(ctx context.Context) E.Either[error, []entities.Project] {
ds.mu.RLock()
defer ds.mu.RUnlock()
projects := make([]entities.Project, 0, len(ds.store))
for _, project := range ds.store {
projects = append(projects, *project)
}
return E.Right[error](projects)
}
// FindByUserID retrieves all projects for a specific user
func (ds *MockProjectDataSource) FindByUserID(ctx context.Context, userID string) E.Either[error, []entities.Project] {
ds.mu.RLock()
defer ds.mu.RUnlock()
projects := make([]entities.Project, 0)
for _, project := range ds.store {
if project.UserID == userID {
projects = append(projects, *project)
}
}
return E.Right[error](projects)
}

View File

@ -0,0 +1,120 @@
package mocks
import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities"
impl "actatempus_backend/internal/infrastructure/data"
"context"
"fmt"
"sync"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
O "github.com/IBM/fp-go/option"
)
type MockProjectTaskDataSource struct {
store map[string]*entities.ProjectTask
mu sync.RWMutex
}
func NewMockProjectTaskDataSource() data.ProjectTaskDataSource {
return &MockProjectTaskDataSource{
store: make(map[string]*entities.ProjectTask),
}
}
// Create a new ProjectTask
func (ds *MockProjectTaskDataSource) Create(ctx context.Context, task entities.ProjectTaskCreate) E.Either[error, entities.ProjectTask] {
ds.mu.Lock()
defer ds.mu.Unlock()
id := GenerateID()
newTask := entities.ProjectTask{
ID: id,
Name: task.Name,
ProjectID: task.ProjectID,
Description: task.Description,
}
ds.store[id] = &newTask
return E.Right[error](newTask)
}
// Find ProjectTask by ID
func (ds *MockProjectTaskDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.ProjectTask] {
ds.mu.RLock()
defer ds.mu.RUnlock()
return F.Pipe2(
O.FromNillable(ds.store[id]),
E.FromOption[*entities.ProjectTask](F.Constant(fmt.Errorf("project task with ID %s not found", id))),
E.Chain(impl.TryDereference[entities.ProjectTask]),
)
}
// Update an existing ProjectTask
func (ds *MockProjectTaskDataSource) Update(ctx context.Context, task entities.ProjectTaskUpdate) E.Either[error, entities.ProjectTask] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[task.ID]
if !found {
return E.Left[entities.ProjectTask](fmt.Errorf("project task with ID %s not found", task.ID))
}
if task.Name != nil {
existing.Name = *task.Name
}
if task.Description != nil {
existing.Description = task.Description
}
if task.ProjectID != nil {
existing.ProjectID = *task.ProjectID
}
return E.Right[error](*existing)
}
// Delete a ProjectTask
func (ds *MockProjectTaskDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.ProjectTask] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[id]
if !found {
return E.Left[entities.ProjectTask](fmt.Errorf("project task with ID %s not found", id))
}
delete(ds.store, id)
return E.Right[error](*existing)
}
// FindAll retrieves all ProjectTasks
func (ds *MockProjectTaskDataSource) FindAll(ctx context.Context) E.Either[error, []entities.ProjectTask] {
ds.mu.RLock()
defer ds.mu.RUnlock()
tasks := make([]entities.ProjectTask, 0, len(ds.store))
for _, task := range ds.store {
tasks = append(tasks, *task)
}
return E.Right[error](tasks)
}
// FindByProjectID retrieves all ProjectTasks for a given Project
func (ds *MockProjectTaskDataSource) FindByProjectID(ctx context.Context, projectID string) E.Either[error, []entities.ProjectTask] {
ds.mu.RLock()
defer ds.mu.RUnlock()
tasks := make([]entities.ProjectTask, 0)
for _, task := range ds.store {
if task.ProjectID == projectID {
tasks = append(tasks, *task)
}
}
return E.Right[error](tasks)
}

View File

@ -0,0 +1,144 @@
package mocks
import (
impl "actatempus_backend/internal/infrastructure/data"
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities"
"context"
"fmt"
"sync"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
O "github.com/IBM/fp-go/option"
)
type MockTimeEntryDataSource struct {
store map[string]*entities.TimeEntry
mu sync.RWMutex
}
func NewMockTimeEntryDataSource() data.TimeEntryDataSource {
return &MockTimeEntryDataSource{
store: make(map[string]*entities.TimeEntry),
}
}
// Create a new TimeEntry
func (ds *MockTimeEntryDataSource) Create(ctx context.Context, entry entities.TimeEntryCreate) E.Either[error, entities.TimeEntry] {
ds.mu.Lock()
defer ds.mu.Unlock()
id := GenerateID()
newEntry := entities.TimeEntry{
ID: id,
StartTime: entry.StartTime,
EndTime: entry.EndTime,
Description: entry.Description,
UserID: entry.UserID,
ProjectID: entry.ProjectID,
}
ds.store[id] = &newEntry
return E.Right[error](newEntry)
}
// Find TimeEntry by ID
func (ds *MockTimeEntryDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.TimeEntry] {
ds.mu.RLock()
defer ds.mu.RUnlock()
return F.Pipe2(
O.FromNillable(ds.store[id]),
E.FromOption[*entities.TimeEntry](F.Constant(fmt.Errorf("time entry with ID %s not found", id))),
E.Chain(impl.TryDereference[entities.TimeEntry]),
)
}
// Update an existing TimeEntry
func (ds *MockTimeEntryDataSource) Update(ctx context.Context, entry entities.TimeEntryUpdate) E.Either[error, entities.TimeEntry] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[entry.ID]
if !found {
return E.Left[entities.TimeEntry](fmt.Errorf("time entry with ID %s not found", entry.ID))
}
if entry.StartTime != nil {
existing.StartTime = *entry.StartTime
}
if entry.EndTime != nil {
existing.EndTime = entry.EndTime
}
if entry.Description != nil {
existing.Description = entry.Description
}
if entry.UserID != nil {
existing.UserID = *entry.UserID
}
if entry.ProjectID != nil {
existing.ProjectID = *entry.ProjectID
}
return E.Right[error](*existing)
}
// Delete a TimeEntry
func (ds *MockTimeEntryDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.TimeEntry] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[id]
if !found {
return E.Left[entities.TimeEntry](fmt.Errorf("time entry with ID %s not found", id))
}
delete(ds.store, id)
return E.Right[error](*existing)
}
// FindAll retrieves all TimeEntries
func (ds *MockTimeEntryDataSource) FindAll(ctx context.Context) E.Either[error, []entities.TimeEntry] {
ds.mu.RLock()
defer ds.mu.RUnlock()
entries := make([]entities.TimeEntry, 0, len(ds.store))
for _, entry := range ds.store {
entries = append(entries, *entry)
}
return E.Right[error](entries)
}
// FindByUserID retrieves all TimeEntries by UserID
func (ds *MockTimeEntryDataSource) FindByUserID(ctx context.Context, userID string) E.Either[error, []entities.TimeEntry] {
ds.mu.RLock()
defer ds.mu.RUnlock()
entries := make([]entities.TimeEntry, 0)
for _, entry := range ds.store {
if entry.UserID == userID {
entries = append(entries, *entry)
}
}
return E.Right[error](entries)
}
// FindByProjectID retrieves all TimeEntries by ProjectID
func (ds *MockTimeEntryDataSource) FindByProjectID(ctx context.Context, projectID string) E.Either[error, []entities.TimeEntry] {
ds.mu.RLock()
defer ds.mu.RUnlock()
entries := make([]entities.TimeEntry, 0)
for _, entry := range ds.store {
if entry.ProjectID == projectID {
entries = append(entries, *entry)
}
}
return E.Right[error](entries)
}

View File

@ -0,0 +1,113 @@
package mocks
import (
domain "actatempus_backend/internal/domain/data"
"actatempus_backend/internal/domain/entities"
impl "actatempus_backend/internal/infrastructure/data"
"context"
"fmt"
"sync"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
O "github.com/IBM/fp-go/option"
)
type MockUserDataSource struct {
store map[string]*entities.User
mu sync.RWMutex
}
func NewMockUserDataSource() domain.UserDataSource {
return &MockUserDataSource{
store: make(map[string]*entities.User),
}
}
func (ds *MockUserDataSource) Create(ctx context.Context, user entities.UserCreate) E.Either[error, entities.User] {
ds.mu.Lock()
defer ds.mu.Unlock()
id := GenerateID()
newUser := entities.User{
ID: id,
Name: user.Name,
Email: user.Email,
Password: impl.GenerateSecureHash(user.Password),
}
ds.store[id] = &newUser
return E.Right[error](newUser)
}
func (ds *MockUserDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.User] {
ds.mu.RLock()
defer ds.mu.RUnlock()
return F.Pipe2(
O.FromNillable(ds.store[id]),
E.FromOption[*entities.User](F.Constant(fmt.Errorf("user with ID %s not found", id))),
E.Chain(
impl.TryDereference[entities.User],
),
)
}
func (ds *MockUserDataSource) FindByEmail(ctx context.Context, email string) E.Either[error, entities.User] {
ds.mu.RLock()
defer ds.mu.RUnlock()
for _, user := range ds.store {
if user.Email == email {
return E.Right[error](*user)
}
}
return E.Left[entities.User](fmt.Errorf("user with email %s not found", email))
}
func (ds *MockUserDataSource) Update(ctx context.Context, user entities.UserUpdate) E.Either[error, entities.User] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[user.ID]
if !found {
return E.Left[entities.User](fmt.Errorf("user with ID %s not found", user.ID))
}
if user.Name != nil {
existing.Name = *user.Name
}
if user.Email != nil {
existing.Email = *user.Email
}
if user.Password != nil {
existing.Password = impl.GenerateSecureHash(*user.Password)
}
return E.Right[error](*existing)
}
func (ds *MockUserDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.User] {
ds.mu.Lock()
defer ds.mu.Unlock()
existing, found := ds.store[id]
if !found {
return E.Left[entities.User](fmt.Errorf("user with ID %s not found", id))
}
delete(ds.store, id)
return E.Right[error](*existing)
}
func (ds *MockUserDataSource) FindAll(ctx context.Context) E.Either[error, []entities.User] {
ds.mu.RLock()
defer ds.mu.RUnlock()
users := make([]entities.User, 0, len(ds.store))
for _, user := range ds.store {
users = append(users, *user)
}
return E.Right[error](users)
}