206 lines
6.3 KiB
Go
206 lines
6.3 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"os"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
)
|
||
|
|
|
||
|
|
const a1CompanyName = "A1 Slovenija"
|
||
|
|
|
||
|
|
// Integration smoke: A1 Slovenija must own processed products after migrate + seed-demo.
|
||
|
|
// Skips when DATABASE_URL is unset (CI without Postgres).
|
||
|
|
func TestLocalDemoCoHasProducts(t *testing.T) {
|
||
|
|
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
||
|
|
if dsn == "" {
|
||
|
|
t.Skip("DATABASE_URL not set")
|
||
|
|
}
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
pg, err := pgxpool.New(ctx, dsn)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("postgres: %v", err)
|
||
|
|
}
|
||
|
|
defer pg.Close()
|
||
|
|
|
||
|
|
var (
|
||
|
|
id uuid.UUID
|
||
|
|
name string
|
||
|
|
products int64
|
||
|
|
)
|
||
|
|
err = pg.QueryRow(ctx, `
|
||
|
|
SELECT c.id, c.name,
|
||
|
|
(SELECT COUNT(*) FROM processed_products p WHERE p.company_id = c.id)
|
||
|
|
FROM companies c
|
||
|
|
WHERE c.name = $1
|
||
|
|
OR c.legacy_company_id = $2
|
||
|
|
ORDER BY (SELECT COUNT(*) FROM processed_products p WHERE p.company_id = c.id) DESC
|
||
|
|
LIMIT 1`, a1CompanyName, billing.A1LegacyCompanyID).Scan(&id, &name, &products)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("query A1 Slovenija: %v (run migrate + seed-demo first)", err)
|
||
|
|
}
|
||
|
|
if products <= 0 {
|
||
|
|
t.Fatalf("%s %s has %d products; want > 0", name, id, products)
|
||
|
|
}
|
||
|
|
t.Logf("%s id=%s products=%d", name, id, products)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Integration smoke: A1 cohort keeps dump-faithful wallet (not fake demo 1M / Legacy rename).
|
||
|
|
func TestLocalDemoCoLegacyCredits(t *testing.T) {
|
||
|
|
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
||
|
|
if dsn == "" {
|
||
|
|
t.Skip("DATABASE_URL not set")
|
||
|
|
}
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
pg, err := pgxpool.New(ctx, dsn)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("postgres: %v", err)
|
||
|
|
}
|
||
|
|
defer pg.Close()
|
||
|
|
|
||
|
|
var (
|
||
|
|
companyName string
|
||
|
|
planName string
|
||
|
|
monthly int
|
||
|
|
maxProd *int
|
||
|
|
isCustom bool
|
||
|
|
isLegacy bool
|
||
|
|
total int
|
||
|
|
used int
|
||
|
|
)
|
||
|
|
err = pg.QueryRow(ctx, `
|
||
|
|
SELECT c.name, p.name, p.monthly_credits, p.max_products, p.is_custom,
|
||
|
|
COALESCE(p.is_legacy, false),
|
||
|
|
cb.total_credits, cb.used_credits
|
||
|
|
FROM companies c
|
||
|
|
JOIN company_plans cp ON cp.company_id = c.id AND cp.is_active = true
|
||
|
|
JOIN plans p ON p.id = cp.plan_id
|
||
|
|
JOIN credit_balances cb ON cb.company_id = c.id
|
||
|
|
WHERE c.name = $1 OR c.legacy_company_id = $2
|
||
|
|
ORDER BY (SELECT COUNT(*) FROM processed_products pp WHERE pp.company_id = c.id) DESC
|
||
|
|
LIMIT 1`, a1CompanyName, billing.A1LegacyCompanyID).
|
||
|
|
Scan(&companyName, &planName, &monthly, &maxProd, &isCustom, &isLegacy, &total, &used)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("query A1 plan/wallet: %v (run migrate + seed-demo first)", err)
|
||
|
|
}
|
||
|
|
if !strings.EqualFold(companyName, a1CompanyName) {
|
||
|
|
t.Fatalf("company=%s want %s", companyName, a1CompanyName)
|
||
|
|
}
|
||
|
|
if !billing.IsLegacyPlan(planName, isLegacy) {
|
||
|
|
t.Fatalf("plan=%s is_legacy=%v want A1/Legacy cohort", planName, isLegacy)
|
||
|
|
}
|
||
|
|
if total == 1_000_000 && used == 0 {
|
||
|
|
t.Fatalf("wallet looks like fake demo pack total=%d used=%d; want dump credit_balances", total, used)
|
||
|
|
}
|
||
|
|
// Dump A1 was ~2500/216; allow local drift but reject empty or fake demo packs.
|
||
|
|
if total < 1000 {
|
||
|
|
t.Fatalf("total_credits=%d want >= 1000 (dump-shaped A1 wallet)", total)
|
||
|
|
}
|
||
|
|
if used < 0 {
|
||
|
|
t.Fatalf("used_credits=%d want >= 0", used)
|
||
|
|
}
|
||
|
|
if monthly == 1_000_000 {
|
||
|
|
t.Fatalf("plan monthly_credits inflated to demo 1M; dump A1 monthly was 0")
|
||
|
|
}
|
||
|
|
t.Logf("company=%s plan=%s monthly=%d total=%d used=%d remaining=%d is_legacy=%v is_custom=%v",
|
||
|
|
companyName, planName, monthly, total, used, total-used, isLegacy, isCustom)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Integration smoke: demo users belong only to Platform Demo (not A1).
|
||
|
|
func TestDemoUsersIsolatedFromA1(t *testing.T) {
|
||
|
|
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
||
|
|
if dsn == "" {
|
||
|
|
t.Skip("DATABASE_URL not set")
|
||
|
|
}
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
pg, err := pgxpool.New(ctx, dsn)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("postgres: %v", err)
|
||
|
|
}
|
||
|
|
defer pg.Close()
|
||
|
|
|
||
|
|
rows, err := pg.Query(ctx, `
|
||
|
|
SELECT u.email, c.name, COALESCE(c.legacy_company_id, '')
|
||
|
|
FROM users u
|
||
|
|
JOIN memberships m ON m.user_id = u.id AND m.status = 'active'
|
||
|
|
JOIN companies c ON c.id = m.company_id
|
||
|
|
WHERE lower(u.email) IN ('demo@descrybe.local', 'demo@descrybe.test')
|
||
|
|
ORDER BY u.email, c.name`)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("query demo memberships: %v (run seed-demo first)", err)
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
type mem struct {
|
||
|
|
email, company, legacy string
|
||
|
|
}
|
||
|
|
var found []mem
|
||
|
|
for rows.Next() {
|
||
|
|
var m mem
|
||
|
|
if err := rows.Scan(&m.email, &m.company, &m.legacy); err != nil {
|
||
|
|
t.Fatalf("scan: %v", err)
|
||
|
|
}
|
||
|
|
found = append(found, m)
|
||
|
|
if strings.EqualFold(m.legacy, billing.A1LegacyCompanyID) ||
|
||
|
|
strings.EqualFold(m.company, a1CompanyName) ||
|
||
|
|
strings.EqualFold(m.company, "Local Demo Co") {
|
||
|
|
t.Fatalf("demo %s still member of A1 tenant %q (legacy=%s)", m.email, m.company, m.legacy)
|
||
|
|
}
|
||
|
|
if !strings.EqualFold(m.company, "Platform Demo") && !strings.EqualFold(m.company, "Demo") {
|
||
|
|
t.Fatalf("demo %s company=%q want Platform Demo", m.email, m.company)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err := rows.Err(); err != nil {
|
||
|
|
t.Fatalf("rows: %v", err)
|
||
|
|
}
|
||
|
|
if len(found) == 0 {
|
||
|
|
t.Fatal("no demo memberships found — run go run ./cmd/seed-demo")
|
||
|
|
}
|
||
|
|
t.Logf("demo memberships ok: %+v", found)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Integration smoke: Free plan definition stays at 0 AI credits after A1 seed.
|
||
|
|
func TestFreePlanUnaffectedByEnterpriseSeed(t *testing.T) {
|
||
|
|
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
||
|
|
if dsn == "" {
|
||
|
|
t.Skip("DATABASE_URL not set")
|
||
|
|
}
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
pg, err := pgxpool.New(ctx, dsn)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("postgres: %v", err)
|
||
|
|
}
|
||
|
|
defer pg.Close()
|
||
|
|
|
||
|
|
var (
|
||
|
|
name string
|
||
|
|
monthly int
|
||
|
|
maxProd *int
|
||
|
|
custom bool
|
||
|
|
)
|
||
|
|
err = pg.QueryRow(ctx, `
|
||
|
|
SELECT name, monthly_credits, max_products, is_custom
|
||
|
|
FROM plans WHERE lower(name) = 'free' ORDER BY id LIMIT 1`).
|
||
|
|
Scan(&name, &monthly, &maxProd, &custom)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("query Free plan: %v", err)
|
||
|
|
}
|
||
|
|
if monthly != 0 {
|
||
|
|
t.Fatalf("Free monthly_credits=%d want 0", monthly)
|
||
|
|
}
|
||
|
|
wantMax := billing.PlanMaxProducts("Free")
|
||
|
|
if wantMax == nil || maxProd == nil || *maxProd != *wantMax {
|
||
|
|
t.Fatalf("Free max_products=%v want %v", maxProd, wantMax)
|
||
|
|
}
|
||
|
|
t.Logf("Free plan ok name=%s monthly=%d max_products=%d", name, monthly, *maxProd)
|
||
|
|
}
|