Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestValidateAttributeIDsOwned(t *testing.T) {
|
||||
a := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
||||
b := uuid.MustParse("22222222-2222-2222-2222-222222222222")
|
||||
c := uuid.MustParse("33333333-3333-3333-3333-333333333333")
|
||||
|
||||
t.Run("empty request", func(t *testing.T) {
|
||||
if err := validateAttributeIDsOwned(nil, nil); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all owned", func(t *testing.T) {
|
||||
if err := validateAttributeIDsOwned([]uuid.UUID{a, b}, []uuid.UUID{b, a}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate request ids still ok when owned", func(t *testing.T) {
|
||||
// Ownership query returns distinct rows; duplicates are allowed through to INSERT
|
||||
// (unique constraint rejects them later — same as the old per-row path).
|
||||
if err := validateAttributeIDsOwned([]uuid.UUID{a, a}, []uuid.UUID{a}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing id", func(t *testing.T) {
|
||||
err := validateAttributeIDsOwned([]uuid.UUID{a, c}, []uuid.UUID{a})
|
||||
if err == nil || err.Error() != "attribute not found" {
|
||||
t.Fatalf("got %v, want attribute not found", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cross-tenant treated as missing", func(t *testing.T) {
|
||||
err := validateAttributeIDsOwned([]uuid.UUID{b}, nil)
|
||||
if err == nil || err.Error() != "attribute not found" {
|
||||
t.Fatalf("got %v, want attribute not found", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func asUUID(t *testing.T, v any) uuid.UUID {
|
||||
t.Helper()
|
||||
switch x := v.(type) {
|
||||
case uuid.UUID:
|
||||
return x
|
||||
case string:
|
||||
id, err := uuid.Parse(x)
|
||||
if err != nil {
|
||||
t.Fatalf("parse uuid %q: %v", x, err)
|
||||
}
|
||||
return id
|
||||
case [16]byte:
|
||||
return uuid.UUID(x)
|
||||
default:
|
||||
t.Fatalf("unexpected uuid type %T", v)
|
||||
return uuid.Nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceCategoryAttributesBatch(t *testing.T) {
|
||||
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
||||
if dsn == "" {
|
||||
t.Skip("DATABASE_URL not set")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
pg, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("postgres: %v", err)
|
||||
}
|
||||
defer pg.Close()
|
||||
|
||||
companyID := uuid.New()
|
||||
_, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`,
|
||||
companyID, "links-test-"+companyID.String()[:8])
|
||||
if err != nil {
|
||||
t.Fatalf("insert company: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pg.Exec(context.Background(), `DELETE FROM companies WHERE id = $1`, companyID)
|
||||
})
|
||||
|
||||
svc := &Service{Pool: pg}
|
||||
catUnique := "links-cat-" + companyID.String()[:8]
|
||||
if _, err := svc.CreateCategory(ctx, companyID, "Links Test Cat", catUnique, nil, nil); err != nil {
|
||||
t.Fatalf("CreateCategory: %v", err)
|
||||
}
|
||||
attrA, err := svc.CreateAttribute(ctx, companyID, "color", "Color", "string", nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAttribute A: %v", err)
|
||||
}
|
||||
attrB, err := svc.CreateAttribute(ctx, companyID, "size", "Size", "string", nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAttribute B: %v", err)
|
||||
}
|
||||
idA := asUUID(t, attrA["id"])
|
||||
idB := asUUID(t, attrB["id"])
|
||||
|
||||
otherCompany := uuid.New()
|
||||
_, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`,
|
||||
otherCompany, "links-other-"+otherCompany.String()[:8])
|
||||
if err != nil {
|
||||
t.Fatalf("insert other company: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pg.Exec(context.Background(), `DELETE FROM companies WHERE id = $1`, otherCompany)
|
||||
})
|
||||
foreign, err := svc.CreateAttribute(ctx, otherCompany, "foreign", "Foreign", "string", nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAttribute foreign: %v", err)
|
||||
}
|
||||
foreignID := asUUID(t, foreign["id"])
|
||||
|
||||
t.Run("batch replace with required flags", func(t *testing.T) {
|
||||
err := svc.ReplaceCategoryAttributes(ctx, companyID, catUnique, []uuid.UUID{idA, idB}, map[string]bool{
|
||||
idA.String(): true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReplaceCategoryAttributes: %v", err)
|
||||
}
|
||||
items, err := svc.ListCategoryAttributes(ctx, companyID, catUnique)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCategoryAttributes: %v", err)
|
||||
}
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("want 2 links, got %d", len(items))
|
||||
}
|
||||
byAttr := map[uuid.UUID]bool{}
|
||||
for _, item := range items {
|
||||
aid := asUUID(t, item["attribute_id"])
|
||||
req, _ := item["required"].(bool)
|
||||
byAttr[aid] = req
|
||||
}
|
||||
if !byAttr[idA] {
|
||||
t.Fatal("attribute A should be required")
|
||||
}
|
||||
if byAttr[idB] {
|
||||
t.Fatal("attribute B should not be required")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("replace clears previous links", func(t *testing.T) {
|
||||
err := svc.ReplaceCategoryAttributes(ctx, companyID, catUnique, []uuid.UUID{idB}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ReplaceCategoryAttributes: %v", err)
|
||||
}
|
||||
items, err := svc.ListCategoryAttributes(ctx, companyID, catUnique)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCategoryAttributes: %v", err)
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("want 1 link, got %d", len(items))
|
||||
}
|
||||
if asUUID(t, items[0]["attribute_id"]) != idB {
|
||||
t.Fatalf("want attribute B, got %v", items[0]["attribute_id"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty list clears all", func(t *testing.T) {
|
||||
err := svc.ReplaceCategoryAttributes(ctx, companyID, catUnique, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ReplaceCategoryAttributes: %v", err)
|
||||
}
|
||||
items, err := svc.ListCategoryAttributes(ctx, companyID, catUnique)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCategoryAttributes: %v", err)
|
||||
}
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("want 0 links, got %d", len(items))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing attribute", func(t *testing.T) {
|
||||
err := svc.ReplaceCategoryAttributes(ctx, companyID, catUnique, []uuid.UUID{uuid.New()}, nil)
|
||||
if err == nil || err.Error() != "attribute not found" {
|
||||
t.Fatalf("got %v, want attribute not found", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cross-tenant attribute", func(t *testing.T) {
|
||||
err := svc.ReplaceCategoryAttributes(ctx, companyID, catUnique, []uuid.UUID{foreignID}, nil)
|
||||
if err == nil || err.Error() != "attribute not found" {
|
||||
t.Fatalf("got %v, want attribute not found", err)
|
||||
}
|
||||
items, err := svc.ListCategoryAttributes(ctx, companyID, catUnique)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCategoryAttributes: %v", err)
|
||||
}
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("failed replace must leave links empty, got %d", len(items))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("category not found", func(t *testing.T) {
|
||||
err := svc.ReplaceCategoryAttributes(ctx, companyID, "no-such-category", []uuid.UUID{idA}, nil)
|
||||
if err == nil || err.Error() != "category not found" {
|
||||
t.Fatalf("got %v, want category not found", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user