test(parity): add frontend smoke harness
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/app"
|
||||
"github.com/gochat/gochat/internal/config"
|
||||
"github.com/gochat/gochat/internal/database"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/pkg/crypto"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := "serve"
|
||||
if len(os.Args) > 1 {
|
||||
cmd = os.Args[1]
|
||||
}
|
||||
|
||||
var err error
|
||||
switch cmd {
|
||||
case "serve", "server", "run":
|
||||
err = serve()
|
||||
case "seed":
|
||||
err = seed()
|
||||
case "help", "-h", "--help":
|
||||
printUsage()
|
||||
return
|
||||
default:
|
||||
err = fmt.Errorf("unknown command %q", cmd)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gochat: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("Usage: gochat [serve|seed]")
|
||||
fmt.Println(" serve Start the GoChat HTTP server")
|
||||
fmt.Println(" seed Create deterministic development/smoke data")
|
||||
}
|
||||
|
||||
func serve() error {
|
||||
env := os.Getenv("GOCHAT_ENV")
|
||||
if env == "" {
|
||||
env = "development"
|
||||
}
|
||||
application, err := app.Bootstrap(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return application.Run()
|
||||
}
|
||||
|
||||
func seed() error {
|
||||
env := os.Getenv("GOCHAT_ENV")
|
||||
if env == "" {
|
||||
env = "development"
|
||||
}
|
||||
cfg, err := config.LoadWithEnv(env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config load failed: %w", err)
|
||||
}
|
||||
if err := applogger.Init(applogger.Config{Level: cfg.Log.Level, Format: cfg.Log.Format, Output: "stdout", ErrorOutput: "stderr"}); err != nil {
|
||||
return fmt.Errorf("logger init failed: %w", err)
|
||||
}
|
||||
if shouldRunSeedMigrations(cfg) {
|
||||
if err := database.RunMigrations(cfg.Database.MigrateDSN(), cfg.Database.GetMigrationsPath()); err != nil {
|
||||
return fmt.Errorf("database migrations failed: %w", err)
|
||||
}
|
||||
}
|
||||
db, err := app.NewDatabase(&cfg.Database, cfg.Log.Level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeDB(db)
|
||||
|
||||
data, err := seedSmokeData(context.Background(), db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoded, _ := json.MarshalIndent(data, "", " ")
|
||||
fmt.Println(string(encoded))
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldRunSeedMigrations(cfg *config.Config) bool {
|
||||
raw := strings.ToLower(strings.TrimSpace(os.Getenv("GOCHAT_SEED_RUN_MIGRATIONS")))
|
||||
if raw == "true" || raw == "1" || raw == "yes" {
|
||||
return true
|
||||
}
|
||||
return cfg.Database.RunMigrations
|
||||
}
|
||||
|
||||
func closeDB(db *gorm.DB) {
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type smokeSeedSummary struct {
|
||||
AdminEmail string `json:"admin_email"`
|
||||
AdminPassword string `json:"admin_password"`
|
||||
AccountID uint `json:"account_id"`
|
||||
InboxID uint `json:"inbox_id"`
|
||||
ContactID uint `json:"contact_id"`
|
||||
ConversationID uint `json:"conversation_id"`
|
||||
ConversationUID string `json:"conversation_uuid"`
|
||||
SlaPolicyID uint `json:"sla_policy_id"`
|
||||
CustomRoleID uint `json:"custom_role_id"`
|
||||
CapacityPolicyID uint `json:"capacity_policy_id"`
|
||||
CaptainAssistantID uint `json:"captain_assistant_id"`
|
||||
}
|
||||
|
||||
func seedSmokeData(ctx context.Context, db *gorm.DB) (*smokeSeedSummary, error) {
|
||||
adminEmail := getenvDefault("GOCHAT_SEED_ADMIN_EMAIL", "admin@gochat.local")
|
||||
adminPassword := getenvDefault("GOCHAT_SEED_ADMIN_PASSWORD", "changeme")
|
||||
adminName := getenvDefault("GOCHAT_SEED_ADMIN_NAME", "Super Admin")
|
||||
accountName := getenvDefault("GOCHAT_SEED_ACCOUNT_NAME", "Test Account")
|
||||
inboxName := getenvDefault("GOCHAT_SEED_INBOX_NAME", "Test Website Inbox")
|
||||
|
||||
account := &model.Account{}
|
||||
if err := db.WithContext(ctx).Where("name = ?", accountName).FirstOrCreate(account, model.Account{Name: accountName, Locale: "en", Timezone: "UTC", Active: true, Status: "active", FeatureFlags: `{"captain":true,"csat":true}`}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed account: %w", err)
|
||||
}
|
||||
|
||||
hashed, err := crypto.HashPassword(adminPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash admin password: %w", err)
|
||||
}
|
||||
now := time.Now()
|
||||
admin := &model.User{}
|
||||
if err := db.WithContext(ctx).Where("email = ?", adminEmail).FirstOrCreate(admin, model.User{AccountID: account.ID, Name: adminName, DisplayName: adminName, Email: adminEmail, Password: hashed, PasswordDigest: hashed, Provider: "email", Role: "super_admin", Type: "User", Active: true, Available: true, ConfirmedAt: &now, UISettings: datatypes.JSON([]byte(`{}`)), CustomAttributes: datatypes.JSON([]byte(`{}`))}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed admin user: %w", err)
|
||||
}
|
||||
if err := db.WithContext(ctx).Model(admin).Updates(map[string]any{"account_id": account.ID, "password": hashed, "password_digest": hashed, "active": true, "available": true}).Error; err != nil {
|
||||
return nil, fmt.Errorf("update admin user: %w", err)
|
||||
}
|
||||
|
||||
if err := db.WithContext(ctx).Where("user_id = ? AND account_id = ?", admin.ID, account.ID).FirstOrCreate(&model.AccountUser{}, model.AccountUser{UserID: admin.ID, AccountID: account.ID, Role: "administrator", Availability: "online", AutoOffline: true}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed account user: %w", err)
|
||||
}
|
||||
|
||||
inbox := &model.Inbox{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, inboxName).FirstOrCreate(inbox, model.Inbox{AccountID: account.ID, Name: inboxName, ChannelType: "web_widget", Enabled: true, EnableAutoAssignment: true, GreetingEnabled: true, GreetingMessage: "Hello from GoChat", EnableEmailCollect: true, CsatSurveyEnabled: true, Timezone: "UTC", AllowMessagesAfterResolved: true, SenderNameType: "friendly_name", ChannelConfig: `{"website_url":"http://localhost:3036","website_token":"gochat-smoke-widget-token","widget_color":"#1f93ff"}`}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed inbox: %w", err)
|
||||
}
|
||||
if err := db.WithContext(ctx).Where("inbox_id = ? AND user_id = ?", inbox.ID, admin.ID).FirstOrCreate(&model.InboxMember{}, model.InboxMember{InboxID: inbox.ID, UserID: admin.ID, Role: "administrator"}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed inbox member: %w", err)
|
||||
}
|
||||
|
||||
contact := &model.Contact{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND email = ?", account.ID, "customer@gochat.local").FirstOrCreate(contact, model.Contact{AccountID: account.ID, Name: "Smoke Customer", Email: "customer@gochat.local", Identifier: "gochat-smoke-customer", ContactType: "lead", AdditionalAttributes: datatypes.JSON([]byte(`{}`)), CustomAttributes: datatypes.JSON([]byte(`{"plan":"enterprise"}`))}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed contact: %w", err)
|
||||
}
|
||||
contactInbox := &model.ContactInbox{}
|
||||
if err := db.WithContext(ctx).Where("contact_id = ? AND inbox_id = ?", contact.ID, inbox.ID).FirstOrCreate(contactInbox, model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, SourceID: "gochat-smoke-source", PubsubToken: "gochat-smoke-contact-pubsub"}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed contact inbox: %w", err)
|
||||
}
|
||||
|
||||
displayID := uint(1)
|
||||
lastActivity := time.Now().Unix()
|
||||
conversation := &model.Conversation{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND inbox_id = ? AND contact_id = ?", account.ID, inbox.ID, contact.ID).FirstOrCreate(conversation, model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, ContactInboxID: &contactInbox.ID, DisplayID: &displayID, AssigneeID: &admin.ID, Status: "open", Priority: "medium", ChannelType: "web_widget", Channel: "web_widget", Labels: "vip", LastActivityAt: &lastActivity}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed conversation: %w", err)
|
||||
}
|
||||
if err := seedMessage(ctx, db, conversation, inbox.ID, contact.ID, "incoming", "Hello, I need help with my order."); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := seedMessage(ctx, db, conversation, inbox.ID, admin.ID, "outgoing", "I can help with that."); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sla := &model.SlaPolicy{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, "Smoke SLA").FirstOrCreate(sla, model.SlaPolicy{AccountID: account.ID, Name: "Smoke SLA", Description: "B12 frontend smoke SLA", FirstResponseTimeThreshold: 300, NextResponseTimeThreshold: 600, ResolutionTimeThreshold: 3600}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed sla policy: %w", err)
|
||||
}
|
||||
if err := db.WithContext(ctx).Where("sla_policy_id = ? AND inbox_id = ?", sla.ID, inbox.ID).FirstOrCreate(&model.SlaPolicyInbox{}, model.SlaPolicyInbox{SlaPolicyID: sla.ID, InboxID: inbox.ID, AccountID: account.ID}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed sla policy inbox: %w", err)
|
||||
}
|
||||
|
||||
customRole := &model.CustomRole{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, "Smoke Custom Role").FirstOrCreate(customRole, model.CustomRole{AccountID: account.ID, Name: "Smoke Custom Role", Description: "B12 smoke role", Permissions: `["conversation_manage","contact_manage","report_manage"]`}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed custom role: %w", err)
|
||||
}
|
||||
capacity := &model.AgentCapacityPolicy{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, "Smoke Capacity").FirstOrCreate(capacity, model.AgentCapacityPolicy{AccountID: account.ID, Name: "Smoke Capacity", Description: "B12 smoke capacity", AssignmentLogic: "round_robin", ExclusionRules: json.RawMessage(`{}`)}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed capacity policy: %w", err)
|
||||
}
|
||||
if err := db.WithContext(ctx).Where("agent_capacity_policy_id = ? AND inbox_id = ?", capacity.ID, inbox.ID).FirstOrCreate(&model.InboxCapacityLimit{}, model.InboxCapacityLimit{AgentCapacityPolicyID: capacity.ID, InboxID: inbox.ID, ConversationLimit: 10}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed capacity limit: %w", err)
|
||||
}
|
||||
|
||||
assistant := &model.CaptainAssistant{}
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, "Smoke Captain").FirstOrCreate(assistant, model.CaptainAssistant{AccountID: account.ID, Name: "Smoke Captain", Description: "B12 smoke assistant", Status: model.AssistantStatusActive, Config: json.RawMessage(`{"model":"gpt-4o"}`)}).Error; err != nil {
|
||||
return nil, fmt.Errorf("seed captain assistant: %w", err)
|
||||
}
|
||||
|
||||
return &smokeSeedSummary{AdminEmail: adminEmail, AdminPassword: adminPassword, AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, ConversationID: conversation.ID, ConversationUID: conversation.UUID, SlaPolicyID: sla.ID, CustomRoleID: customRole.ID, CapacityPolicyID: capacity.ID, CaptainAssistantID: assistant.ID}, nil
|
||||
}
|
||||
|
||||
func seedMessage(ctx context.Context, db *gorm.DB, conversation *model.Conversation, inboxID, senderID uint, messageType, content string) error {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).Model(&model.Message{}).Where("conversation_id = ? AND content = ?", conversation.ID, content).Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("check message: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
senderType := "contact"
|
||||
if messageType == "outgoing" {
|
||||
senderType = "user"
|
||||
}
|
||||
msg := &model.Message{AccountID: conversation.AccountID, InboxID: inboxID, ConversationID: conversation.ID, SenderID: &senderID, SenderType: senderType, MessageType: messageType, ContentType: "text", Content: content, Status: "sent"}
|
||||
if err := db.WithContext(ctx).Create(msg).Error; err != nil {
|
||||
return fmt.Errorf("seed message: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getenvDefault(key, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user