Files
2026-03-07 04:36:52 +01:00

402 lines
11 KiB
Go

package commands
import (
"context"
"testing"
"time"
"apocapoc-api/internal/domain/entities"
"apocapoc-api/internal/domain/repositories"
"apocapoc-api/internal/domain/value_objects"
"apocapoc-api/internal/shared/errors"
"apocapoc-api/internal/shared/pagination"
)
type mockHabitRepoForBatch struct {
habits map[string]*entities.Habit
createFunc func(ctx context.Context, habit *entities.Habit) error
updateFunc func(ctx context.Context, habit *entities.Habit) error
softDeleteFunc func(ctx context.Context, id string) error
}
func (m *mockHabitRepoForBatch) FindByID(ctx context.Context, id string) (*entities.Habit, error) {
habit, ok := m.habits[id]
if !ok {
return nil, errors.ErrNotFound
}
return habit, nil
}
func (m *mockHabitRepoForBatch) Create(ctx context.Context, habit *entities.Habit) error {
if m.createFunc != nil {
return m.createFunc(ctx, habit)
}
m.habits[habit.ID] = habit
return nil
}
func (m *mockHabitRepoForBatch) Update(ctx context.Context, habit *entities.Habit) error {
if m.updateFunc != nil {
return m.updateFunc(ctx, habit)
}
m.habits[habit.ID] = habit
return nil
}
func (m *mockHabitRepoForBatch) SoftDelete(ctx context.Context, id string) error {
if m.softDeleteFunc != nil {
return m.softDeleteFunc(ctx, id)
}
delete(m.habits, id)
return nil
}
func (m *mockHabitRepoForBatch) FindByUserID(ctx context.Context, userID string) ([]*entities.Habit, error) {
return nil, nil
}
func (m *mockHabitRepoForBatch) FindActiveByUserID(ctx context.Context, userID string) ([]*entities.Habit, error) {
return nil, nil
}
func (m *mockHabitRepoForBatch) Delete(ctx context.Context, id string) error {
return nil
}
func (m *mockHabitRepoForBatch) FindActiveByUserIDWithPagination(ctx context.Context, userID string, params pagination.Params) ([]*entities.Habit, error) {
return nil, nil
}
func (m *mockHabitRepoForBatch) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
return nil, nil
}
func (m *mockHabitRepoForBatch) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
return 0, nil
}
func (m *mockHabitRepoForBatch) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
return 0, nil
}
func (m *mockHabitRepoForBatch) GetChangesSince(ctx context.Context, userID string, since time.Time) (*repositories.HabitChanges, error) {
return &repositories.HabitChanges{
Created: []*entities.Habit{},
Updated: []*entities.Habit{},
Deleted: []string{},
}, nil
}
type mockEntryRepoForBatch struct {
entries map[string]*entities.HabitEntry
createFunc func(ctx context.Context, entry *entities.HabitEntry) error
updateFunc func(ctx context.Context, entry *entities.HabitEntry) error
softDeleteFunc func(ctx context.Context, id string) error
}
func (m *mockEntryRepoForBatch) FindByID(ctx context.Context, id string) (*entities.HabitEntry, error) {
entry, ok := m.entries[id]
if !ok {
return nil, errors.ErrNotFound
}
return entry, nil
}
func (m *mockEntryRepoForBatch) Create(ctx context.Context, entry *entities.HabitEntry) error {
if m.createFunc != nil {
return m.createFunc(ctx, entry)
}
m.entries[entry.ID] = entry
return nil
}
func (m *mockEntryRepoForBatch) Update(ctx context.Context, entry *entities.HabitEntry) error {
if m.updateFunc != nil {
return m.updateFunc(ctx, entry)
}
m.entries[entry.ID] = entry
return nil
}
func (m *mockEntryRepoForBatch) SoftDelete(ctx context.Context, id string) error {
if m.softDeleteFunc != nil {
return m.softDeleteFunc(ctx, id)
}
delete(m.entries, id)
return nil
}
func (m *mockEntryRepoForBatch) FindByHabitID(ctx context.Context, habitID string) ([]*entities.HabitEntry, error) {
return nil, nil
}
func (m *mockEntryRepoForBatch) FindByHabitIDAndDateRange(ctx context.Context, habitID string, from, to time.Time) ([]*entities.HabitEntry, error) {
return nil, nil
}
func (m *mockEntryRepoForBatch) FindByUserID(ctx context.Context, userID string) ([]*entities.HabitEntry, error) {
return nil, nil
}
func (m *mockEntryRepoForBatch) FindPendingByHabitID(ctx context.Context, habitID string, beforeDate time.Time) ([]*entities.HabitEntry, error) {
return nil, nil
}
func (m *mockEntryRepoForBatch) Delete(ctx context.Context, id string) error {
return nil
}
func (m *mockEntryRepoForBatch) GetChangesSince(ctx context.Context, userID string, since time.Time) (*repositories.HabitEntryChanges, error) {
return &repositories.HabitEntryChanges{
Created: []*entities.HabitEntry{},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
}, nil
}
func TestApplySyncBatchHandler_CreateNewHabits(t *testing.T) {
habitRepo := &mockHabitRepoForBatch{
habits: make(map[string]*entities.Habit),
}
entryRepo := &mockEntryRepoForBatch{
entries: make(map[string]*entities.HabitEntry),
}
handler := NewApplySyncBatchHandler(habitRepo, entryRepo)
newHabit := entities.NewHabit("user-123", "New Habit", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
newHabit.ID = "habit-new"
cmd := ApplySyncBatchCommand{
UserID: "user-123",
Habits: HabitBatchChanges{
Created: []*entities.Habit{newHabit},
Updated: []*entities.Habit{},
Deleted: []string{},
},
Entries: EntryBatchChanges{
Created: []*entities.HabitEntry{},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
},
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(habitRepo.habits) != 1 {
t.Errorf("Expected 1 habit created, got %d", len(habitRepo.habits))
}
}
func TestApplySyncBatchHandler_UpdateExistingHabits(t *testing.T) {
oldTime := time.Now().Add(-1 * time.Hour)
newTime := time.Now()
existingHabit := entities.NewHabit("user-123", "Old Name", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
existingHabit.ID = "habit-1"
existingHabit.UpdatedAt = oldTime
habitRepo := &mockHabitRepoForBatch{
habits: map[string]*entities.Habit{
"habit-1": existingHabit,
},
}
entryRepo := &mockEntryRepoForBatch{
entries: make(map[string]*entities.HabitEntry),
}
handler := NewApplySyncBatchHandler(habitRepo, entryRepo)
updatedHabit := entities.NewHabit("user-123", "New Name", value_objects.HabitTypeCounter, value_objects.FrequencyWeekly, false, false)
updatedHabit.ID = "habit-1"
updatedHabit.UpdatedAt = newTime
cmd := ApplySyncBatchCommand{
UserID: "user-123",
Habits: HabitBatchChanges{
Created: []*entities.Habit{},
Updated: []*entities.Habit{updatedHabit},
Deleted: []string{},
},
Entries: EntryBatchChanges{
Created: []*entities.HabitEntry{},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
},
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if habitRepo.habits["habit-1"].Name != "New Name" {
t.Errorf("Expected habit name to be updated to 'New Name', got '%s'", habitRepo.habits["habit-1"].Name)
}
}
func TestApplySyncBatchHandler_LastWriteWins(t *testing.T) {
serverTime := time.Now()
clientTime := serverTime.Add(-30 * time.Minute)
serverHabit := entities.NewHabit("user-123", "Server Version", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
serverHabit.ID = "habit-1"
serverHabit.UpdatedAt = serverTime
habitRepo := &mockHabitRepoForBatch{
habits: map[string]*entities.Habit{
"habit-1": serverHabit,
},
}
entryRepo := &mockEntryRepoForBatch{
entries: make(map[string]*entities.HabitEntry),
}
handler := NewApplySyncBatchHandler(habitRepo, entryRepo)
clientHabit := entities.NewHabit("user-123", "Client Version", value_objects.HabitTypeCounter, value_objects.FrequencyWeekly, false, false)
clientHabit.ID = "habit-1"
clientHabit.UpdatedAt = clientTime
cmd := ApplySyncBatchCommand{
UserID: "user-123",
Habits: HabitBatchChanges{
Created: []*entities.Habit{},
Updated: []*entities.Habit{clientHabit},
Deleted: []string{},
},
Entries: EntryBatchChanges{
Created: []*entities.HabitEntry{},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
},
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if habitRepo.habits["habit-1"].Name != "Server Version" {
t.Errorf("Expected server version to win (Last-Write-Wins), got '%s'", habitRepo.habits["habit-1"].Name)
}
}
func TestApplySyncBatchHandler_DeleteHabits(t *testing.T) {
existingHabit := entities.NewHabit("user-123", "To Delete", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
existingHabit.ID = "habit-1"
habitRepo := &mockHabitRepoForBatch{
habits: map[string]*entities.Habit{
"habit-1": existingHabit,
},
}
entryRepo := &mockEntryRepoForBatch{
entries: make(map[string]*entities.HabitEntry),
}
handler := NewApplySyncBatchHandler(habitRepo, entryRepo)
cmd := ApplySyncBatchCommand{
UserID: "user-123",
Habits: HabitBatchChanges{
Created: []*entities.Habit{},
Updated: []*entities.Habit{},
Deleted: []string{"habit-1"},
},
Entries: EntryBatchChanges{
Created: []*entities.HabitEntry{},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
},
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(habitRepo.habits) != 0 {
t.Errorf("Expected habit to be deleted, but still exists")
}
}
func TestApplySyncBatchHandler_ValidatesUserOwnership(t *testing.T) {
habitRepo := &mockHabitRepoForBatch{
habits: make(map[string]*entities.Habit),
}
entryRepo := &mockEntryRepoForBatch{
entries: make(map[string]*entities.HabitEntry),
}
handler := NewApplySyncBatchHandler(habitRepo, entryRepo)
habitForDifferentUser := entities.NewHabit("user-456", "Not Yours", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
habitForDifferentUser.ID = "habit-1"
cmd := ApplySyncBatchCommand{
UserID: "user-123",
Habits: HabitBatchChanges{
Created: []*entities.Habit{habitForDifferentUser},
Updated: []*entities.Habit{},
Deleted: []string{},
},
Entries: EntryBatchChanges{
Created: []*entities.HabitEntry{},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
},
}
err := handler.Handle(context.Background(), cmd)
if err == nil {
t.Error("Expected error for user mismatch, got nil")
}
}
func TestApplySyncBatchHandler_ProcessesEntries(t *testing.T) {
habitRepo := &mockHabitRepoForBatch{
habits: make(map[string]*entities.Habit),
}
entryRepo := &mockEntryRepoForBatch{
entries: make(map[string]*entities.HabitEntry),
}
handler := NewApplySyncBatchHandler(habitRepo, entryRepo)
newEntry := entities.NewHabitEntry("habit-1", time.Now(), nil)
newEntry.ID = "entry-new"
cmd := ApplySyncBatchCommand{
UserID: "user-123",
Habits: HabitBatchChanges{
Created: []*entities.Habit{},
Updated: []*entities.Habit{},
Deleted: []string{},
},
Entries: EntryBatchChanges{
Created: []*entities.HabitEntry{newEntry},
Updated: []*entities.HabitEntry{},
Deleted: []string{},
},
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(entryRepo.entries) != 1 {
t.Errorf("Expected 1 entry created, got %d", len(entryRepo.entries))
}
}