Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
214 lines
6.4 KiB
Go
214 lines
6.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|