package service import ( "context" "errors" "testing" "gis/internal/domain" "github.com/google/uuid" ) // stubCategoryRepo is an in-memory CategoryRepository for tests. type stubCategoryRepo struct { store map[uuid.UUID]domain.Category } func newStubCategoryRepo() *stubCategoryRepo { return &stubCategoryRepo{store: map[uuid.UUID]domain.Category{}} } func (r *stubCategoryRepo) Create(_ context.Context, c domain.Category) (domain.Category, error) { if c.ID == uuid.Nil { c.ID = uuid.New() } r.store[c.ID] = c return c, nil } func (r *stubCategoryRepo) GetByID(_ context.Context, id uuid.UUID) (domain.Category, error) { c, ok := r.store[id] if !ok { return domain.Category{}, domain.ErrNotFound } return c, nil } func (r *stubCategoryRepo) List(_ context.Context, _ *uuid.UUID) ([]domain.Category, error) { return nil, nil } func (r *stubCategoryRepo) Update(_ context.Context, c domain.Category) (domain.Category, error) { r.store[c.ID] = c return c, nil } func (r *stubCategoryRepo) Delete(_ context.Context, id uuid.UUID) error { delete(r.store, id) return nil } func TestCategoryService_Create(t *testing.T) { ctx := context.Background() t.Run("root category succeeds", func(t *testing.T) { svc := NewCategoryService(newStubCategoryRepo()) got, err := svc.Create(ctx, CategoryInput{Name: "root"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Name != "root" || got.ParentID != nil { t.Fatalf("unexpected category: %+v", got) } }) t.Run("missing parent is a validation error", func(t *testing.T) { svc := NewCategoryService(newStubCategoryRepo()) missing := uuid.New() _, err := svc.Create(ctx, CategoryInput{Name: "child", ParentID: &missing}) if !errors.Is(err, domain.ErrValidation) { t.Fatalf("want ErrValidation, got %v", err) } }) t.Run("existing parent succeeds", func(t *testing.T) { repo := newStubCategoryRepo() svc := NewCategoryService(repo) root, _ := svc.Create(ctx, CategoryInput{Name: "root"}) child, err := svc.Create(ctx, CategoryInput{Name: "child", ParentID: &root.ID}) if err != nil { t.Fatalf("unexpected error: %v", err) } if child.ParentID == nil || *child.ParentID != root.ID { t.Fatalf("child not linked to parent: %+v", child) } }) } func TestCategoryService_Update_PreventsCycles(t *testing.T) { ctx := context.Background() repo := newStubCategoryRepo() svc := NewCategoryService(repo) root, _ := svc.Create(ctx, CategoryInput{Name: "root"}) child, _ := svc.Create(ctx, CategoryInput{Name: "child", ParentID: &root.ID}) t.Run("category cannot be its own parent", func(t *testing.T) { _, err := svc.Update(ctx, root.ID, CategoryInput{Name: "root", ParentID: &root.ID}) if !errors.Is(err, domain.ErrValidation) { t.Fatalf("want ErrValidation, got %v", err) } }) t.Run("category cannot descend from its own child", func(t *testing.T) { _, err := svc.Update(ctx, root.ID, CategoryInput{Name: "root", ParentID: &child.ID}) if !errors.Is(err, domain.ErrValidation) { t.Fatalf("want ErrValidation, got %v", err) } }) t.Run("valid reparent succeeds", func(t *testing.T) { other, _ := svc.Create(ctx, CategoryInput{Name: "other"}) updated, err := svc.Update(ctx, child.ID, CategoryInput{Name: "child", ParentID: &other.ID}) if err != nil { t.Fatalf("unexpected error: %v", err) } if updated.ParentID == nil || *updated.ParentID != other.ID { t.Fatalf("reparent failed: %+v", updated) } }) } func TestCategoryService_Update_MissingCategory(t *testing.T) { svc := NewCategoryService(newStubCategoryRepo()) _, err := svc.Update(context.Background(), uuid.New(), CategoryInput{Name: "x"}) if !errors.Is(err, domain.ErrNotFound) { t.Fatalf("want ErrNotFound, got %v", err) } }