From 7f095f71b72263c9478eab711a15473568129c85 Mon Sep 17 00:00:00 2001 From: nadmax Date: Mon, 12 Jan 2026 12:36:52 +0100 Subject: [PATCH] reorganize repository package - Add models, postgres and mocks folders to separate implementations - Ignore mocks folder for codecov --- cmd/server/main.go | 4 +- cmd/worker/main.go | 4 +- codecov.yml | 2 + internal/api/handlers_test.go | 28 ++++++------ internal/dashboard/dashboard_test.go | 24 +++++----- internal/queue/queue_test.go | 6 +-- .../task_repository.go} | 20 +++++---- internal/repository/models/task.go | 25 +++++++++++ .../task_repository.go} | 44 +++++-------------- .../task_repository_test.go} | 2 +- internal/repository/task_repository.go | 8 ++-- internal/worker/worker_test.go | 6 +-- 12 files changed, 92 insertions(+), 81 deletions(-) create mode 100644 codecov.yml rename internal/repository/{mock_postgres.go => mocks/task_repository.go} (94%) create mode 100644 internal/repository/models/task.go rename internal/repository/{postgres.go => postgres/task_repository.go} (89%) rename internal/repository/{postgres_test.go => postgres/task_repository_test.go} (99%) diff --git a/cmd/server/main.go b/cmd/server/main.go index c5ec2ec..9bbfd7d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -12,7 +12,7 @@ import ( "github.com/nadmax/nexq/internal/api" "github.com/nadmax/nexq/internal/middleware" "github.com/nadmax/nexq/internal/queue" - "github.com/nadmax/nexq/internal/repository" + "github.com/nadmax/nexq/internal/repository/postgres" ) func main() { @@ -26,7 +26,7 @@ func main() { log.Fatal("POSTGRES_DSN is required") } - repo, err := repository.NewPostgresTaskRepository(postgresDSN) + repo, err := postgres.NewPostgresTaskRepository(postgresDSN) if err != nil { log.Fatal(err) } diff --git a/cmd/worker/main.go b/cmd/worker/main.go index b087c77..5337cd7 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -11,7 +11,7 @@ import ( "time" "github.com/nadmax/nexq/internal/queue" - "github.com/nadmax/nexq/internal/repository" + "github.com/nadmax/nexq/internal/repository/postgres" "github.com/nadmax/nexq/internal/task" "github.com/nadmax/nexq/internal/worker" "github.com/nadmax/nexq/internal/worker/handlers" @@ -28,7 +28,7 @@ func main() { log.Fatal("POSTGRES_DSN is required") } - repo, err := repository.NewPostgresTaskRepository(postgresDSN) + repo, err := postgres.NewPostgresTaskRepository(postgresDSN) if err != nil { log.Fatal(err) } diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..a3f2f33 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "**/mocks/**" diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 1064698..e91470b 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -11,7 +11,8 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/nadmax/nexq/internal/queue" - "github.com/nadmax/nexq/internal/repository" + "github.com/nadmax/nexq/internal/repository/mocks" + "github.com/nadmax/nexq/internal/repository/models" "github.com/nadmax/nexq/internal/task" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,11 +30,11 @@ func setupTestAPI(t *testing.T) (*API, *queue.Queue, *miniredis.Miniredis) { return api, q, mr } -func setupTestAPIWithMockRepo(t *testing.T) (*API, *queue.Queue, *repository.MockPostgresRepository, *miniredis.Miniredis) { +func setupTestAPIWithMockRepo(t *testing.T) (*API, *queue.Queue, *mocks.MockPostgresRepository, *miniredis.Miniredis) { mr, err := miniredis.Run() require.NoError(t, err) - mockRepo := repository.NewMockPostgresRepository() + mockRepo := mocks.NewMockPostgresRepository() q, err := queue.NewQueue(mr.Addr(), mockRepo) require.NoError(t, err) @@ -639,7 +640,7 @@ func TestHistoryStatsWithMockRepo(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.TaskStats = []repository.TaskStats{ + mockRepo.TaskStats = []models.TaskStats{ { Type: "send_email", Status: "completed", @@ -658,7 +659,7 @@ func TestHistoryStatsWithMockRepo(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) - var stats []repository.TaskStats + var stats []models.TaskStats err := json.NewDecoder(w.Body).Decode(&stats) require.NoError(t, err) assert.Len(t, stats, 1) @@ -686,7 +687,7 @@ func TestHandleRecentHistory_Success(t *testing.T) { duration1 := 250 duration2 := 150 - mockRepo.RecentTasks = []repository.RecentTask{ + mockRepo.RecentTasks = []models.RecentTask{ { TaskID: "task-1", Type: "send_email", @@ -710,7 +711,7 @@ func TestHandleRecentHistory_Success(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) - var tasks []repository.RecentTask + var tasks []models.RecentTask err := json.NewDecoder(w.Body).Decode(&tasks) require.NoError(t, err) assert.Len(t, tasks, 2) @@ -722,7 +723,7 @@ func TestHandleRecentHistory_WithLimit(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.RecentTasks = []repository.RecentTask{} + mockRepo.RecentTasks = []models.RecentTask{} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/api/history/recent?limit=50", nil) @@ -730,7 +731,6 @@ func TestHandleRecentHistory_WithLimit(t *testing.T) { api.handleRecentHistory(w, r) assert.Equal(t, http.StatusOK, w.Code) - // The limit is passed to the repository method, which uses it internally } func TestHandleRecentHistory_InvalidLimit(t *testing.T) { @@ -738,7 +738,7 @@ func TestHandleRecentHistory_InvalidLimit(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.RecentTasks = []repository.RecentTask{} + mockRepo.RecentTasks = []models.RecentTask{} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/api/history/recent?limit=invalid", nil) @@ -805,7 +805,7 @@ func TestHandleTaskHistory_Success(t *testing.T) { defer func() { _ = q.Close() }() taskID := "task-123" - mockRepo.ExecutionLog = []repository.LogExecutionCall{ + mockRepo.ExecutionLog = []mocks.LogExecutionCall{ { TaskID: taskID, AttemptNumber: 1, @@ -910,7 +910,7 @@ func TestHandleTasksByType_Success(t *testing.T) { taskType := "send_email" duration1 := 200 duration2 := 300 - mockRepo.RecentTasks = []repository.RecentTask{ + mockRepo.RecentTasks = []models.RecentTask{ { TaskID: "task-1", Type: taskType, @@ -934,7 +934,7 @@ func TestHandleTasksByType_Success(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) - var tasks []repository.RecentTask + var tasks []models.RecentTask err := json.NewDecoder(w.Body).Decode(&tasks) require.NoError(t, err) assert.Len(t, tasks, 2) @@ -946,7 +946,7 @@ func TestHandleTasksByType_WithLimit(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.RecentTasks = []repository.RecentTask{} + mockRepo.RecentTasks = []models.RecentTask{} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/api/history/type/send_email?limit=25", nil) diff --git a/internal/dashboard/dashboard_test.go b/internal/dashboard/dashboard_test.go index 0efb2dc..90aafcd 100644 --- a/internal/dashboard/dashboard_test.go +++ b/internal/dashboard/dashboard_test.go @@ -8,7 +8,8 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/nadmax/nexq/internal/queue" - "github.com/nadmax/nexq/internal/repository" + "github.com/nadmax/nexq/internal/repository/mocks" + "github.com/nadmax/nexq/internal/repository/models" "github.com/nadmax/nexq/internal/task" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,11 +27,11 @@ func setupTestDashboard(t *testing.T) (*Dashboard, *queue.Queue, *miniredis.Mini return dash, q, mr } -func setupTestDashboardWithMockRepo(t *testing.T) (*Dashboard, *queue.Queue, *repository.MockPostgresRepository, *miniredis.Miniredis) { +func setupTestDashboardWithMockRepo(t *testing.T) (*Dashboard, *queue.Queue, *mocks.MockPostgresRepository, *miniredis.Miniredis) { mr, err := miniredis.Run() require.NoError(t, err) - mockRepo := repository.NewMockPostgresRepository() + mockRepo := mocks.NewMockPostgresRepository() q, err := queue.NewQueue(mr.Addr(), mockRepo) require.NoError(t, err) @@ -491,7 +492,7 @@ func TestGetStatsWithRepository(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.TaskStats = []repository.TaskStats{ + mockRepo.TaskStats = []models.TaskStats{ { Type: "send_email", Status: "completed", @@ -539,7 +540,7 @@ func TestGetRecentTasksWithRepository(t *testing.T) { defer func() { _ = q.Close() }() now := time.Now() - mockRepo.RecentTasks = []repository.RecentTask{ + mockRepo.RecentTasks = []models.RecentTask{ { TaskID: "task-1", Type: "send_email", @@ -612,7 +613,7 @@ func TestGetStatsWithCompletedTasks(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.TaskStats = []repository.TaskStats{ + mockRepo.TaskStats = []models.TaskStats{ { Type: "send_email", Status: "completed", @@ -639,7 +640,7 @@ func TestGetRecentTasksWithVariedStatuses(t *testing.T) { now := time.Now() - mockRepo.RecentTasks = []repository.RecentTask{ + mockRepo.RecentTasks = []models.RecentTask{ { TaskID: "completed-task", Type: "test_task", @@ -685,8 +686,7 @@ func TestGetStatsPerformanceMetrics(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - // Setup performance data - mockRepo.TaskStats = []repository.TaskStats{ + mockRepo.TaskStats = []models.TaskStats{ { Type: "fast_task", Status: "completed", @@ -727,7 +727,7 @@ func TestGetRecentTasksOrdering(t *testing.T) { now := time.Now() - mockRepo.RecentTasks = []repository.RecentTask{ + mockRepo.RecentTasks = []models.RecentTask{ { TaskID: "newest", Type: "test", @@ -761,7 +761,7 @@ func TestGetStatsWithRetryMetrics(t *testing.T) { defer mr.Close() defer func() { _ = q.Close() }() - mockRepo.TaskStats = []repository.TaskStats{ + mockRepo.TaskStats = []models.TaskStats{ { Type: "reliable_task", Status: "completed", @@ -793,7 +793,7 @@ func TestGetRecentTasksWithFailureReasons(t *testing.T) { now := time.Now() - mockRepo.RecentTasks = []repository.RecentTask{ + mockRepo.RecentTasks = []models.RecentTask{ { TaskID: "failed-1", Type: "test", diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go index 1ae39ba..f0b25a1 100644 --- a/internal/queue/queue_test.go +++ b/internal/queue/queue_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/alicebob/miniredis/v2" - "github.com/nadmax/nexq/internal/repository" + "github.com/nadmax/nexq/internal/repository/mocks" "github.com/nadmax/nexq/internal/task" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,11 +21,11 @@ func setupTestQueue(t *testing.T) (*Queue, *miniredis.Miniredis) { return q, mr } -func setupTestQueueWithMockRepo(t *testing.T) (*Queue, *repository.MockPostgresRepository, *miniredis.Miniredis) { +func setupTestQueueWithMockRepo(t *testing.T) (*Queue, *mocks.MockPostgresRepository, *miniredis.Miniredis) { mr, err := miniredis.Run() require.NoError(t, err) - mockRepo := repository.NewMockPostgresRepository() + mockRepo := mocks.NewMockPostgresRepository() q, err := NewQueue(mr.Addr(), mockRepo) require.NoError(t, err) diff --git a/internal/repository/mock_postgres.go b/internal/repository/mocks/task_repository.go similarity index 94% rename from internal/repository/mock_postgres.go rename to internal/repository/mocks/task_repository.go index 302ee45..c3b3da2 100644 --- a/internal/repository/mock_postgres.go +++ b/internal/repository/mocks/task_repository.go @@ -1,10 +1,12 @@ -package repository +// Package mocks provides mock implementations of repository interfaces for use in tests. +package mocks import ( "context" "fmt" "sync" + "github.com/nadmax/nexq/internal/repository/models" "github.com/nadmax/nexq/internal/task" ) @@ -20,8 +22,8 @@ type MockPostgresRepository struct { LogExecutionCalls []LogExecutionCall Tasks map[string]*task.Task ExecutionLog []LogExecutionCall - TaskStats []TaskStats - RecentTasks []RecentTask + TaskStats []models.TaskStats + RecentTasks []models.RecentTask GetTaskError error SaveTaskError error CompleteTaskError error @@ -74,8 +76,8 @@ func NewMockPostgresRepository() *MockPostgresRepository { return &MockPostgresRepository{ Tasks: make(map[string]*task.Task), ExecutionLog: make([]LogExecutionCall, 0), - TaskStats: make([]TaskStats, 0), - RecentTasks: make([]RecentTask, 0), + TaskStats: make([]models.TaskStats, 0), + RecentTasks: make([]models.RecentTask, 0), } } @@ -233,7 +235,7 @@ func (m *MockPostgresRepository) LogExecution(ctx context.Context, taskID string return nil } -func (m *MockPostgresRepository) GetTaskStats(ctx context.Context, hours int) ([]TaskStats, error) { +func (m *MockPostgresRepository) GetTaskStats(ctx context.Context, hours int) ([]models.TaskStats, error) { m.mu.Lock() defer m.mu.Unlock() @@ -244,7 +246,7 @@ func (m *MockPostgresRepository) GetTaskStats(ctx context.Context, hours int) ([ return m.TaskStats, nil } -func (m *MockPostgresRepository) GetRecentTasks(ctx context.Context, limit int) ([]RecentTask, error) { +func (m *MockPostgresRepository) GetRecentTasks(ctx context.Context, limit int) ([]models.RecentTask, error) { m.mu.Lock() defer m.mu.Unlock() @@ -259,7 +261,7 @@ func (m *MockPostgresRepository) GetRecentTasks(ctx context.Context, limit int) return m.RecentTasks, nil } -func (m *MockPostgresRepository) GetTasksByType(ctx context.Context, taskType string, limit int) ([]RecentTask, error) { +func (m *MockPostgresRepository) GetTasksByType(ctx context.Context, taskType string, limit int) ([]models.RecentTask, error) { m.mu.Lock() defer m.mu.Unlock() @@ -267,7 +269,7 @@ func (m *MockPostgresRepository) GetTasksByType(ctx context.Context, taskType st return nil, m.GetTasksByTypeError } - var filtered []RecentTask + var filtered []models.RecentTask for _, task := range m.RecentTasks { if task.Type == taskType { filtered = append(filtered, task) diff --git a/internal/repository/models/task.go b/internal/repository/models/task.go new file mode 100644 index 0000000..49fb16e --- /dev/null +++ b/internal/repository/models/task.go @@ -0,0 +1,25 @@ +// Package models contains data structures used by the task repository layer. +package models + +import "time" + +type TaskStats struct { + Type string `json:"type"` + Status string `json:"status"` + Count int `json:"count"` + AvgDurationMs float64 `json:"avg_duration_ms"` + MaxDurationMs int `json:"max_duration_ms"` + MinDurationMs int `json:"min_duration_ms"` + AvgRetries float64 `json:"avg_retries"` +} + +type RecentTask struct { + TaskID string `json:"task_id"` + Type string `json:"type"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + DurationMs *int `json:"duration_ms,omitempty"` + RetryCount int `json:"retry_count"` + FailureReason string `json:"failure_reason,omitempty"` +} diff --git a/internal/repository/postgres.go b/internal/repository/postgres/task_repository.go similarity index 89% rename from internal/repository/postgres.go rename to internal/repository/postgres/task_repository.go index 5d5fa4f..71da52b 100644 --- a/internal/repository/postgres.go +++ b/internal/repository/postgres/task_repository.go @@ -1,5 +1,5 @@ -// Package repository provides PostgreSQL persistence for task history. -package repository +// Package postgres provides PostgreSQL-backed implementations of repository interfaces. +package postgres import ( "context" @@ -10,6 +10,7 @@ import ( "time" _ "github.com/lib/pq" + "github.com/nadmax/nexq/internal/repository/models" "github.com/nadmax/nexq/internal/task" ) @@ -17,27 +18,6 @@ type PostgresTaskRepository struct { db *sql.DB } -type TaskStats struct { - Type string `json:"type"` - Status string `json:"status"` - Count int `json:"count"` - AvgDurationMs float64 `json:"avg_duration_ms"` - MaxDurationMs int `json:"max_duration_ms"` - MinDurationMs int `json:"min_duration_ms"` - AvgRetries float64 `json:"avg_retries"` -} - -type RecentTask struct { - TaskID string `json:"task_id"` - Type string `json:"type"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - CompletedAt *time.Time `json:"completed_at,omitempty"` - DurationMs *int `json:"duration_ms,omitempty"` - RetryCount int `json:"retry_count"` - FailureReason string `json:"failure_reason,omitempty"` -} - func NewPostgresTaskRepository(connectionString string) (*PostgresTaskRepository, error) { db, err := sql.Open("postgres", connectionString) if err != nil { @@ -259,7 +239,7 @@ func (r *PostgresTaskRepository) LogExecution(ctx context.Context, taskID string return err } -func (r *PostgresTaskRepository) GetTaskStats(ctx context.Context, hours int) ([]TaskStats, error) { +func (r *PostgresTaskRepository) GetTaskStats(ctx context.Context, hours int) ([]models.TaskStats, error) { query := ` SELECT type, status, COUNT(*) as count, @@ -283,9 +263,9 @@ func (r *PostgresTaskRepository) GetTaskStats(ctx context.Context, hours int) ([ } }() - var stats []TaskStats + var stats []models.TaskStats for rows.Next() { - var s TaskStats + var s models.TaskStats if err := rows.Scan( &s.Type, &s.Status, @@ -304,7 +284,7 @@ func (r *PostgresTaskRepository) GetTaskStats(ctx context.Context, hours int) ([ return stats, rows.Err() } -func (r *PostgresTaskRepository) GetRecentTasks(ctx context.Context, limit int) ([]RecentTask, error) { +func (r *PostgresTaskRepository) GetRecentTasks(ctx context.Context, limit int) ([]models.RecentTask, error) { query := ` SELECT task_id, type, status, created_at, completed_at, @@ -324,9 +304,9 @@ func (r *PostgresTaskRepository) GetRecentTasks(ctx context.Context, limit int) } }() - var tasks []RecentTask + var tasks []models.RecentTask for rows.Next() { - var t RecentTask + var t models.RecentTask if err := rows.Scan( &t.TaskID, &t.Type, @@ -346,7 +326,7 @@ func (r *PostgresTaskRepository) GetRecentTasks(ctx context.Context, limit int) return tasks, rows.Err() } -func (r *PostgresTaskRepository) GetTasksByType(ctx context.Context, taskType string, limit int) ([]RecentTask, error) { +func (r *PostgresTaskRepository) GetTasksByType(ctx context.Context, taskType string, limit int) ([]models.RecentTask, error) { query := ` SELECT task_id, type, status, created_at, completed_at, @@ -367,9 +347,9 @@ func (r *PostgresTaskRepository) GetTasksByType(ctx context.Context, taskType st } }() - var tasks []RecentTask + var tasks []models.RecentTask for rows.Next() { - var t RecentTask + var t models.RecentTask if err := rows.Scan( &t.TaskID, &t.Type, diff --git a/internal/repository/postgres_test.go b/internal/repository/postgres/task_repository_test.go similarity index 99% rename from internal/repository/postgres_test.go rename to internal/repository/postgres/task_repository_test.go index 826bcb4..044e98a 100644 --- a/internal/repository/postgres_test.go +++ b/internal/repository/postgres/task_repository_test.go @@ -1,4 +1,4 @@ -package repository +package postgres import ( "context" diff --git a/internal/repository/task_repository.go b/internal/repository/task_repository.go index 1d31a44..ed19b7e 100644 --- a/internal/repository/task_repository.go +++ b/internal/repository/task_repository.go @@ -1,8 +1,10 @@ +// Package repository defines the persistence interfaces for task storage. package repository import ( "context" + "github.com/nadmax/nexq/internal/repository/models" "github.com/nadmax/nexq/internal/task" ) @@ -15,9 +17,9 @@ type TaskRepository interface { MoveTaskToDLQ(ctx context.Context, taskID string, reason string) error IncrementRetryCount(ctx context.Context, taskID string) error LogExecution(ctx context.Context, taskID string, attemptNumber int, status string, durationMs int, msgErr string, workerID string) error - GetTaskStats(ctx context.Context, hours int) ([]TaskStats, error) - GetRecentTasks(ctx context.Context, limit int) ([]RecentTask, error) - GetTasksByType(ctx context.Context, taskType string, limit int) ([]RecentTask, error) + GetTaskStats(ctx context.Context, hours int) ([]models.TaskStats, error) + GetRecentTasks(ctx context.Context, limit int) ([]models.RecentTask, error) + GetTasksByType(ctx context.Context, taskType string, limit int) ([]models.RecentTask, error) GetTaskHistory(ctx context.Context, taskID string) ([]map[string]any, error) Close() error } diff --git a/internal/worker/worker_test.go b/internal/worker/worker_test.go index dea6fbc..df10e6b 100644 --- a/internal/worker/worker_test.go +++ b/internal/worker/worker_test.go @@ -7,7 +7,7 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/nadmax/nexq/internal/queue" - "github.com/nadmax/nexq/internal/repository" + "github.com/nadmax/nexq/internal/repository/mocks" "github.com/nadmax/nexq/internal/task" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,11 +25,11 @@ func setupTestWorker(t *testing.T) (*Worker, *queue.Queue, *miniredis.Miniredis) return w, q, mr } -func setupTestWorkerWithMockRepo(t *testing.T) (*Worker, *queue.Queue, *repository.MockPostgresRepository, *miniredis.Miniredis) { +func setupTestWorkerWithMockRepo(t *testing.T) (*Worker, *queue.Queue, *mocks.MockPostgresRepository, *miniredis.Miniredis) { mr, err := miniredis.Run() require.NoError(t, err) - mockRepo := repository.NewMockPostgresRepository() + mockRepo := mocks.NewMockPostgresRepository() q, err := queue.NewQueue(mr.Addr(), mockRepo) require.NoError(t, err)