Files
moteva/server/internal/application/brand_kit_service_test.go
T
root 789f51b29e feat(server): add brand kit management and project binding
Add a brand kit domain with memory and postgres stores, a BrandKitService
for CRUD and resolution, and REST endpoints to list, upsert, and delete
brand kits. Projects can bind a brand kit whose resolved context and
reference images feed into the agent's long-term memory and design prompts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:58:55 +08:00

174 lines
6.0 KiB
Go

package application
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"img_infinite_canvas/internal/domain/brandkit"
"img_infinite_canvas/internal/domain/design"
"img_infinite_canvas/internal/infrastructure/memory"
)
const (
brandKitTestUserA = "11111111-1111-4111-8111-111111111111"
brandKitTestUserB = "22222222-2222-4222-8222-222222222222"
)
func TestBrandKitServiceEnforcesOwnershipAndSingleDefault(t *testing.T) {
service := NewBrandKitService(memory.NewBrandKitStore())
ctxA := design.ContextWithUserID(context.Background(), brandKitTestUserA)
ctxB := design.ContextWithUserID(context.Background(), brandKitTestUserB)
first, err := service.Upsert(ctxA, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), true)
if err != nil {
t.Fatal(err)
}
if !first.IsDefault {
t.Fatal("expected first kit to be default")
}
if _, err := service.Upsert(ctxA, "brand-kit-beta", testBrandKitDocument("brand-kit-beta", "Beta"), true); err != nil {
t.Fatal(err)
}
kits, err := service.List(ctxA)
if err != nil {
t.Fatal(err)
}
defaults := 0
for _, kit := range kits {
if kit.IsDefault {
defaults++
if kit.ID != "brand-kit-beta" {
t.Fatalf("unexpected default kit %q", kit.ID)
}
}
}
if defaults != 1 {
t.Fatalf("expected one default, got %d", defaults)
}
if _, err := service.Get(ctxB, first.ID); !errors.Is(err, brandkit.ErrNotFound) {
t.Fatalf("expected user isolation, got %v", err)
}
}
func TestBrandKitServiceCompilesAuthoritativeContext(t *testing.T) {
service := NewBrandKitService(memory.NewBrandKitStore())
ctx := design.ContextWithUserID(context.Background(), brandKitTestUserA)
kit, err := service.Upsert(ctx, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), false)
if err != nil {
t.Fatal(err)
}
compiled, err := service.ResolveContext(ctx, kit.ID)
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{"Binding creative system", "Alpha", "Signal Green", "#28C76F", "Display"} {
if !strings.Contains(compiled, expected) {
t.Fatalf("compiled context missing %q: %s", expected, compiled)
}
}
if strings.Contains(compiled, "data:") {
t.Fatal("compiled context must not include embedded asset data")
}
}
func TestProjectBrandKitBindingUsesDefaultAndExplicitNone(t *testing.T) {
store := memory.NewBrandKitStore()
brandKits := NewBrandKitService(store)
ctx := design.ContextWithUserID(context.Background(), brandKitTestUserA)
if _, err := brandKits.Upsert(ctx, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), true); err != nil {
t.Fatal(err)
}
repo := memory.NewProjectRepository()
designs := NewDesignService(repo, nil, nil, nil, nil)
designs.SetBrandKitService(brandKits)
withDefault, err := designs.CreateProjectWithBrandKit(ctx, "Default", "", "", "", "", 0, false, nil)
if err != nil {
t.Fatal(err)
}
if withDefault.BrandKitID != "brand-kit-alpha" || !strings.Contains(withDefault.BrandContext, "Alpha") {
t.Fatalf("default Brand Kit was not bound: %#v", withDefault)
}
none := ""
withoutKit, err := designs.CreateProjectWithBrandKit(ctx, "None", "", "", "", "", 0, false, &none)
if err != nil {
t.Fatal(err)
}
if withoutKit.BrandKitID != "" || withoutKit.BrandContext != "" {
t.Fatalf("explicit None should bypass the default: %#v", withoutKit)
}
bound, err := designs.UpdateProjectBrandKit(ctx, withoutKit.ID, "brand-kit-alpha")
if err != nil {
t.Fatal(err)
}
if bound.BrandKitID != "brand-kit-alpha" {
t.Fatalf("expected explicit binding, got %q", bound.BrandKitID)
}
if err := designs.ClearBrandKitBindings(ctx, "brand-kit-alpha"); err != nil {
t.Fatal(err)
}
cleared, err := repo.Get(ctx, bound.ID)
if err != nil {
t.Fatal(err)
}
if cleared.BrandKitID != "" {
t.Fatalf("expected binding cleanup, got %q", cleared.BrandKitID)
}
}
func TestProjectBrandKitBindingRejectsAnotherUsersKit(t *testing.T) {
brandKits := NewBrandKitService(memory.NewBrandKitStore())
ctxA := design.ContextWithUserID(context.Background(), brandKitTestUserA)
ctxB := design.ContextWithUserID(context.Background(), brandKitTestUserB)
if _, err := brandKits.Upsert(ctxA, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), false); err != nil {
t.Fatal(err)
}
repo := memory.NewProjectRepository()
designs := NewDesignService(repo, nil, nil, nil, nil)
designs.SetBrandKitService(brandKits)
none := ""
project, err := designs.CreateProjectAsyncWithBrandKit(ctxB, "Other user", "", "", "", "", 0, false, &none)
if err != nil {
t.Fatal(err)
}
if _, err := designs.UpdateProjectBrandKit(ctxB, project.ID, "brand-kit-alpha"); !errors.Is(err, brandkit.ErrNotFound) {
t.Fatalf("expected ownership check to hide the Brand Kit, got %v", err)
}
stored, err := repo.Get(ctxB, project.ID)
if err != nil {
t.Fatal(err)
}
if stored.BrandKitID != "" {
t.Fatalf("failed binding must not mutate the project, got %q", stored.BrandKitID)
}
}
func testBrandKitDocument(id string, name string) string {
return fmt.Sprintf(`{
"version": 1,
"id": %q,
"name": %q,
"brandName": %q,
"tagline": "Make the signal clear",
"summary": "A focused product brand.",
"audience": "Creative teams",
"positioning": "Clear and precise",
"personality": "Confident",
"voice": {"traits": "Direct", "guidance": "Lead with outcomes", "sample": "Move with clarity"},
"logos": [{"id": "logo-primary", "name": "Primary", "variant": "primary", "usage": "Light backgrounds", "source": "upload", "dataUrl": "data:image/png;base64,AA=="}],
"colorGroups": [{"id": "colors-primary", "name": "Primary", "colors": [{"id": "color-signal", "name": "Signal Green", "hex": "#28C76F", "usage": "Key actions"}]}],
"typography": [{"id": "type-display", "label": "Display", "family": "Avenir Next", "weight": "700", "sizes": "48", "description": "Headlines"}],
"referenceImages": [],
"visual": {"keywords": "precise", "composition": "strict grid", "imagery": "real product", "dos": "use space", "donts": "avoid noise", "prompt": "Keep the product clear"},
"applyToNewProjects": false,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}`, id, name, name)
}