Files
goods/api/internal/adminstore/category_test.go
T
sulaimaannaasif6866 4a693c8fe9
CI / Go (api) (pull_request) Successful in 1m15s
CI / Python (ingestion) (pull_request) Successful in 10s
CI / Migrations (postgres) (pull_request) Successful in 16s
feat(admin): 分类管理(分类树增删改移 + 后台页面)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-21 00:43:49 +00:00

131 lines
3.9 KiB
Go

package adminstore
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// newTestStore connects to the test database, skipping when it is unreachable
// or migrations have not been applied.
func newTestStore(t *testing.T) *Store {
t.Helper()
dsn := os.Getenv("OPENGOODS_DATABASE_URL")
if dsn == "" {
dsn = "postgres://opengoods:opengoods@localhost:5432/opengoods?sslmode=disable"
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Skipf("no database: %v", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
t.Skipf("database not reachable: %v", err)
}
var hasTable bool
if err := pool.QueryRow(ctx, "SELECT to_regclass('public.category') IS NOT NULL").Scan(&hasTable); err != nil || !hasTable {
pool.Close()
t.Skip("migrations not applied (category missing)")
}
t.Cleanup(pool.Close)
return New(pool)
}
func ptr(s string) *string { return &s }
func TestCategoryLifecycle(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
root, err := s.CreateCategory(ctx, "tester", CategoryInput{
NameZH: "测试根", Slug: ptr("test_root_" + randomHex(6)),
})
if err != nil {
t.Fatalf("create root: %v", err)
}
t.Cleanup(func() { _, _ = s.pool.Exec(ctx, "DELETE FROM category WHERE path <@ $1::ltree", root.Path) })
if root.Level != 0 || root.ParentID != nil {
t.Fatalf("root level/parent wrong: level=%d parent=%v", root.Level, root.ParentID)
}
child, err := s.CreateCategory(ctx, "tester", CategoryInput{
NameZH: "测试子", NameEN: ptr("Test Child"), ParentID: &root.ID,
})
if err != nil {
t.Fatalf("create child: %v", err)
}
if child.Level != 1 || child.ParentID == nil || *child.ParentID != root.ID {
t.Fatalf("child hierarchy wrong: %+v", child)
}
// Deleting a node with children must fail.
if err := s.DeleteCategory(ctx, root.ID, "tester"); !errors.Is(err, ErrCategoryHasChildren) {
t.Fatalf("expected ErrCategoryHasChildren, got %v", err)
}
// Rename child.
renamed, err := s.UpdateCategory(ctx, child.ID, "tester", CategoryInput{NameZH: "测试子-改名"})
if err != nil {
t.Fatalf("rename: %v", err)
}
if renamed.NameZH != "测试子-改名" {
t.Fatalf("rename not applied: %q", renamed.NameZH)
}
// Move child to a second root, descendants' path/level should follow.
root2, err := s.CreateCategory(ctx, "tester", CategoryInput{
NameZH: "测试根2", Slug: ptr("test_root2_" + randomHex(6)),
})
if err != nil {
t.Fatalf("create root2: %v", err)
}
t.Cleanup(func() { _, _ = s.pool.Exec(ctx, "DELETE FROM category WHERE path <@ $1::ltree", root2.Path) })
moved, err := s.UpdateCategory(ctx, child.ID, "tester", CategoryInput{NameZH: "测试子-改名", ParentID: &root2.ID})
if err != nil {
t.Fatalf("move: %v", err)
}
if moved.ParentID == nil || *moved.ParentID != root2.ID {
t.Fatalf("move parent wrong: %+v", moved)
}
if moved.Level != 1 {
t.Fatalf("moved level wrong: %d", moved.Level)
}
// Moving a node under itself must be rejected.
if _, err := s.UpdateCategory(ctx, root2.ID, "tester", CategoryInput{NameZH: "测试根2", ParentID: &child.ID}); !errors.Is(err, ErrInvalidParent) {
t.Fatalf("expected ErrInvalidParent for self-move, got %v", err)
}
// Duplicate path on create must be rejected.
if _, err := s.CreateCategory(ctx, "tester", CategoryInput{NameZH: "dup", Slug: ptr(root.Path)}); !errors.Is(err, ErrDuplicatePath) {
t.Fatalf("expected ErrDuplicatePath, got %v", err)
}
// Now the leaf can be deleted.
if err := s.DeleteCategory(ctx, child.ID, "tester"); err != nil {
t.Fatalf("delete leaf: %v", err)
}
}
func TestSlugify(t *testing.T) {
cases := map[string]string{
"Cooking Oil": "cooking_oil",
" Hello--Wld": "hello_wld",
"食品": "",
"a__b": "a_b",
}
for in, want := range cases {
if got := slugify(in); got != want {
t.Errorf("slugify(%q) = %q, want %q", in, got, want)
}
}
}