rdev/internal/domain/project_domain_test.go
jordan c86516c53a feat: Add multi-domain support with auto-generated slugs for landing page cookbook
Landing page cookbook implementation (Weeks 1-4):

Domain Infrastructure:
- Add project_domains table with migration (013_project_domains.sql)
- Add ProjectDomain model with domain types (primary_auto, primary_custom, alias)
- Add SlugGenerator and ProjectDomainRepository interfaces
- Implement postgres adapters for domain and slug management

Service Layer:
- Add domain CRUD methods to ProjectInfraService
- Generate 8-char random slugs for auto-domains
- Support custom subdomains during project creation
- Add site_live health check to project status
- Trigger CI build after template seeding

Handler Updates:
- Add DomainService interface and adapter pattern
- Rewrite domain handlers to use database-backed service
- Add proper error handling for duplicate/missing domains

CI Integration:
- Add TriggerBuild to CIProvider interface
- Implement TriggerBuild in Woodpecker adapter
- Manually trigger initial build after template seed

Cookbook & Scripts:
- Add landing-test.sh script for E2E testing
- Add release.sh for version releases
- Add logs.sh for quick log access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 12:55:59 -07:00

213 lines
5.3 KiB
Go

package domain
import (
"testing"
)
func TestGenerateSlug(t *testing.T) {
t.Run("generates correct length", func(t *testing.T) {
slug, err := GenerateSlug()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(slug) != SlugLength {
t.Errorf("slug length = %d, want %d", len(slug), SlugLength)
}
})
t.Run("contains only allowed characters", func(t *testing.T) {
for i := 0; i < 100; i++ {
slug, err := GenerateSlug()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := ValidateSlug(slug); err != nil {
t.Errorf("generated slug %q failed validation: %v", slug, err)
}
}
})
t.Run("generates unique slugs", func(t *testing.T) {
seen := make(map[string]bool)
for i := 0; i < 1000; i++ {
slug, err := GenerateSlug()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if seen[slug] {
t.Errorf("duplicate slug generated: %s", slug)
}
seen[slug] = true
}
})
}
func TestValidateSlug(t *testing.T) {
tests := []struct {
slug string
wantErr bool
}{
{"k7m2x9p4", false},
{"abcdefgh", false},
{"23456789", false},
{"", true}, // empty
{"short", true}, // too short
{"toolongslug", true}, // too long
{"ABCDEFGH", true}, // uppercase
{"k7m2x9p0", true}, // contains 0
{"k7m2x9p1", true}, // contains 1
{"k7m2x9po", true}, // contains o
{"k7m2x9pl", true}, // contains l
{"k7m2x9pi", true}, // contains i
{"k7m2-9p4", true}, // contains hyphen
{"k7m2_9p4", true}, // contains underscore
}
for _, tt := range tests {
t.Run(tt.slug, func(t *testing.T) {
err := ValidateSlug(tt.slug)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateSlug(%q) error = %v, wantErr = %v", tt.slug, err, tt.wantErr)
}
})
}
}
func TestDomainType(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
tests := []struct {
dtype DomainType
want bool
}{
{DomainTypePrimaryAuto, true},
{DomainTypePrimaryCustom, true},
{DomainTypeAlias, true},
{DomainType("invalid"), false},
{DomainType(""), false},
}
for _, tt := range tests {
if got := tt.dtype.Valid(); got != tt.want {
t.Errorf("%q.Valid() = %v, want %v", tt.dtype, got, tt.want)
}
}
})
t.Run("IsPrimary", func(t *testing.T) {
tests := []struct {
dtype DomainType
want bool
}{
{DomainTypePrimaryAuto, true},
{DomainTypePrimaryCustom, true},
{DomainTypeAlias, false},
}
for _, tt := range tests {
if got := tt.dtype.IsPrimary(); got != tt.want {
t.Errorf("%q.IsPrimary() = %v, want %v", tt.dtype, got, tt.want)
}
}
})
}
func TestValidateSubdomain(t *testing.T) {
tests := []struct {
subdomain string
wantErr bool
}{
{"my-app", false},
{"myapp123", false},
{"a", false},
{"landing-page", false},
{"", true}, // empty
{"-myapp", true}, // starts with hyphen
{"123app", true}, // starts with number
{"my_app", true}, // contains underscore
{"MyApp", true}, // contains uppercase
{"my.app", true}, // contains dot
{"www", true}, // reserved
{"api", true}, // reserved
{"git", true}, // reserved
}
for _, tt := range tests {
t.Run(tt.subdomain, func(t *testing.T) {
err := ValidateSubdomain(tt.subdomain)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateSubdomain(%q) error = %v, wantErr = %v", tt.subdomain, err, tt.wantErr)
}
})
}
}
func TestValidateFQDN(t *testing.T) {
tests := []struct {
domain string
wantErr bool
}{
{"example.com", false},
{"www.example.com", false},
{"sub.domain.example.com", false},
{"my-app.threesix.ai", false},
{"a.b.c", false},
{"", true},
{"example", false}, // single label is valid
{"-example.com", true}, // starts with hyphen
{"example-.com", true}, // ends with hyphen
{"exam ple.com", true}, // contains space
{"example..com", true}, // double dot
}
for _, tt := range tests {
t.Run(tt.domain, func(t *testing.T) {
err := ValidateFQDN(tt.domain)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateFQDN(%q) error = %v, wantErr = %v", tt.domain, err, tt.wantErr)
}
})
}
}
func TestIsSubdomainOf(t *testing.T) {
tests := []struct {
domain string
baseDomain string
want bool
}{
{"my-app.threesix.ai", "threesix.ai", true},
{"deep.sub.threesix.ai", "threesix.ai", true},
{"example.com", "threesix.ai", false},
{"threesix.ai", "threesix.ai", false}, // same domain, not subdomain
{"MY-APP.THREESIX.AI", "threesix.ai", true}, // case insensitive
}
for _, tt := range tests {
t.Run(tt.domain, func(t *testing.T) {
if got := IsSubdomainOf(tt.domain, tt.baseDomain); got != tt.want {
t.Errorf("IsSubdomainOf(%q, %q) = %v, want %v", tt.domain, tt.baseDomain, got, tt.want)
}
})
}
}
func TestExtractSubdomain(t *testing.T) {
tests := []struct {
domain string
baseDomain string
want string
}{
{"my-app.threesix.ai", "threesix.ai", "my-app"},
{"deep.sub.threesix.ai", "threesix.ai", "deep.sub"},
{"example.com", "threesix.ai", ""},
{"threesix.ai", "threesix.ai", ""},
{"MY-APP.THREESIX.AI", "threesix.ai", "my-app"}, // case normalized
}
for _, tt := range tests {
t.Run(tt.domain, func(t *testing.T) {
if got := ExtractSubdomain(tt.domain, tt.baseDomain); got != tt.want {
t.Errorf("ExtractSubdomain(%q, %q) = %q, want %q", tt.domain, tt.baseDomain, got, tt.want)
}
})
}
}