Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
340 lines
11 KiB
Go
340 lines
11 KiB
Go
package automation
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
// ===========================
|
|
// ResolveTemplateVars tests
|
|
// ===========================
|
|
|
|
func TestResolveTemplateVars_ContactName(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Contact: &model.Contact{Name: "Alice Smith"},
|
|
}
|
|
result := ResolveTemplateVars("Hello {{contact.name}}!", ctx)
|
|
if result != "Hello Alice Smith!" {
|
|
t.Fatalf("expected 'Hello Alice Smith!', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_ConversationStatus(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Conversation: &model.Conversation{Status: "open"},
|
|
}
|
|
result := ResolveTemplateVars("Status: {{conversation.status}}", ctx)
|
|
if result != "Status: open" {
|
|
t.Fatalf("expected 'Status: open', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_AgentName(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Assignee: &model.User{Name: "Bob Agent"},
|
|
}
|
|
result := ResolveTemplateVars("Assigned to {{agent.name}}", ctx)
|
|
if result != "Assigned to Bob Agent" {
|
|
t.Fatalf("expected 'Assigned to Bob Agent', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_MultipleVariables(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Contact: &model.Contact{Name: "Alice", Email: "alice@example.com"},
|
|
Conversation: &model.Conversation{Status: "open"},
|
|
Assignee: &model.User{Name: "Bob"},
|
|
}
|
|
result := ResolveTemplateVars("Hi {{contact.name}}, your ticket {{conversation.status}} is handled by {{agent.name}}. Email: {{contact.email}}", ctx)
|
|
expected := "Hi Alice, your ticket open is handled by Bob. Email: alice@example.com"
|
|
if result != expected {
|
|
t.Fatalf("expected '%s', got '%s'", expected, result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_NilContext(t *testing.T) {
|
|
ctx := TemplateContext{} // all nil
|
|
result := ResolveTemplateVars("Hello {{contact.name}}, status={{conversation.status}}", ctx)
|
|
// Unresolved variables → empty string (Chatwoot convention)
|
|
if result != "Hello , status=" {
|
|
t.Fatalf("expected 'Hello , status=', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_NilAssignee(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Conversation: &model.Conversation{Status: "open"},
|
|
Contact: &model.Contact{Name: "Alice"},
|
|
// Assignee is nil
|
|
}
|
|
result := ResolveTemplateVars("{{agent.name}} handling {{contact.name}}'s {{conversation.status}} ticket", ctx)
|
|
if result != " handling Alice's open ticket" {
|
|
t.Fatalf("expected ' handling Alice\\'s open ticket', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_NoVariables(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Contact: &model.Contact{Name: "Alice"},
|
|
}
|
|
result := ResolveTemplateVars("Static message with no variables", ctx)
|
|
if result != "Static message with no variables" {
|
|
t.Fatalf("expected unchanged string, got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_UnknownObject(t *testing.T) {
|
|
ctx := TemplateContext{}
|
|
result := ResolveTemplateVars("{{unknown.attr}} test", ctx)
|
|
if result != " test" {
|
|
t.Fatalf("expected ' test' (unknown object → empty), got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_UnknownAttribute(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Contact: &model.Contact{Name: "Alice"},
|
|
}
|
|
result := ResolveTemplateVars("{{contact.nonexistent}} test", ctx)
|
|
if result != " test" {
|
|
t.Fatalf("expected ' test' (unknown attr → empty), got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_InboxName(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Inbox: &model.Inbox{Name: "Support Chat"},
|
|
}
|
|
result := ResolveTemplateVars("From {{inbox.name}}", ctx)
|
|
if result != "From Support Chat" {
|
|
t.Fatalf("expected 'From Support Chat', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_TeamName(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Team: &model.Team{Name: "Escalation"},
|
|
}
|
|
result := ResolveTemplateVars("Team: {{team.name}}", ctx)
|
|
if result != "Team: Escalation" {
|
|
t.Fatalf("expected 'Team: Escalation', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_AccountName(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Account: &model.Account{Name: "Acme Corp"},
|
|
}
|
|
result := ResolveTemplateVars("Account: {{account.name}}", ctx)
|
|
if result != "Account: Acme Corp" {
|
|
t.Fatalf("expected 'Account: Acme Corp', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_ConversationID(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Conversation: &model.Conversation{Status: "open"},
|
|
}
|
|
// Need to set ID — in real usage GORM populates it
|
|
// For this test we test the format string path
|
|
result := ResolveTemplateVars("Ticket #{{conversation.id}}", ctx)
|
|
// ID is 0 for a non-persisted conversation
|
|
if result != "Ticket #0" {
|
|
t.Fatalf("expected 'Ticket #0', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateVars_ContactPhoneNumber(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Contact: &model.Contact{PhoneNumber: "+1-555-0123"},
|
|
}
|
|
result := ResolveTemplateVars("Call {{contact.phone_number}}", ctx)
|
|
if result != "Call +1-555-0123" {
|
|
t.Fatalf("expected 'Call +1-555-0123', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
// ===========================
|
|
// ResolveActionParams tests
|
|
// ===========================
|
|
|
|
func TestResolveActionParams_StringValues(t *testing.T) {
|
|
ctx := TemplateContext{
|
|
Contact: &model.Contact{Name: "Alice"},
|
|
}
|
|
params := map[string]interface{}{
|
|
"content": "Hello {{contact.name}}!",
|
|
"status": "resolved",
|
|
"assignee": 42, // non-string, should pass through unchanged
|
|
}
|
|
resolved := ResolveActionParams(params, ctx)
|
|
|
|
if resolved["content"] != "Hello Alice!" {
|
|
t.Fatalf("expected 'Hello Alice!', got '%s'", resolved["content"])
|
|
}
|
|
if resolved["status"] != "resolved" {
|
|
t.Fatalf("expected 'resolved', got '%s'", resolved["status"])
|
|
}
|
|
if resolved["assignee"] != 42 {
|
|
t.Fatalf("expected 42, got %v", resolved["assignee"])
|
|
}
|
|
}
|
|
|
|
func TestResolveActionParams_NilMap(t *testing.T) {
|
|
resolved := ResolveActionParams(nil, TemplateContext{})
|
|
if resolved != nil {
|
|
t.Fatalf("expected nil map, got non-nil")
|
|
}
|
|
}
|
|
|
|
func TestResolveActionParams_EmptyMap(t *testing.T) {
|
|
resolved := ResolveActionParams(map[string]interface{}{}, TemplateContext{})
|
|
if len(resolved) != 0 {
|
|
t.Fatalf("expected empty map, got %d entries", len(resolved))
|
|
}
|
|
}
|
|
|
|
// ===========================
|
|
// TemplateContextBuilder tests (DB-based)
|
|
// ===========================
|
|
|
|
func TestTemplateContextBuilder_Build(t *testing.T) {
|
|
dbProvider := setupAutomationTestDBProvider(t)
|
|
accountID, userID := seedTestAccount(dbProvider.DB(), t)
|
|
builder := NewTemplateContextBuilder(dbProvider)
|
|
|
|
// Create inbox
|
|
inbox := &model.Inbox{
|
|
AccountID: accountID, Name: "Support Inbox", ChannelType: "web_widget", ChannelID: 1,
|
|
}
|
|
if err := dbProvider.DB().Create(inbox).Error; err != nil {
|
|
t.Fatalf("failed to create inbox: %v", err)
|
|
}
|
|
|
|
// Create contact
|
|
contact := &model.Contact{
|
|
AccountID: accountID, Name: "Jane Customer", Email: "jane@test.com",
|
|
}
|
|
if err := dbProvider.DB().Select("AccountID", "Name", "Email").Create(contact).Error; err != nil {
|
|
t.Fatalf("failed to create contact: %v", err)
|
|
}
|
|
|
|
// Create conversation with assignee
|
|
assigneeID := userID
|
|
conv := &model.Conversation{
|
|
AccountID: accountID, InboxID: inbox.ID, ContactID: contact.ID,
|
|
AssigneeID: &assigneeID, Status: "open", Priority: "medium",
|
|
ChannelType: "web_widget", Channel: "web",
|
|
UUID: "test-uuid-template-build",
|
|
}
|
|
if err := dbProvider.DB().Select(
|
|
"AccountID", "InboxID", "ContactID", "AssigneeID",
|
|
"Status", "Priority", "ChannelType", "Channel", "UUID",
|
|
).Create(conv).Error; err != nil {
|
|
t.Fatalf("failed to create conversation: %v", err)
|
|
}
|
|
|
|
// Build template context
|
|
tctx, err := builder.Build(testContext, conv.ID)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
// Verify conversation loaded
|
|
if tctx.Conversation == nil || tctx.Conversation.Status != "open" {
|
|
t.Fatalf("expected conversation with status=open, got nil or wrong status")
|
|
}
|
|
|
|
// Verify contact loaded
|
|
if tctx.Contact == nil || tctx.Contact.Name != "Jane Customer" {
|
|
t.Fatalf("expected contact with name=Jane Customer, got nil or wrong name")
|
|
}
|
|
|
|
// Verify assignee loaded
|
|
if tctx.Assignee == nil || tctx.Assignee.Name != "Test User" {
|
|
t.Fatalf("expected assignee with name=Test User, got nil or wrong name")
|
|
}
|
|
|
|
// Verify inbox loaded
|
|
if tctx.Inbox == nil || tctx.Inbox.Name != "Support Inbox" {
|
|
t.Fatalf("expected inbox with name=Support Inbox, got nil or wrong name")
|
|
}
|
|
|
|
// Verify account loaded
|
|
if tctx.Account == nil || tctx.Account.Name != "Test Account" {
|
|
t.Fatalf("expected account with name=Test Account, got nil or wrong name")
|
|
}
|
|
|
|
// Now resolve template vars with this context
|
|
result := ResolveTemplateVars("Hi {{contact.name}}, your {{conversation.status}} ticket in {{inbox.name}} is assigned to {{agent.name}}.", tctx)
|
|
expected := "Hi Jane Customer, your open ticket in Support Inbox is assigned to Test User."
|
|
if result != expected {
|
|
t.Fatalf("expected '%s', got '%s'", expected, result)
|
|
}
|
|
}
|
|
|
|
func TestTemplateContextBuilder_ConversationNotFound(t *testing.T) {
|
|
dbProvider := setupAutomationTestDBProvider(t)
|
|
builder := NewTemplateContextBuilder(dbProvider)
|
|
|
|
_, err := builder.Build(testContext, 9999) // non-existent ID
|
|
if err == nil {
|
|
t.Fatalf("expected error for non-existent conversation, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTemplateContextBuilder_NoAssignee(t *testing.T) {
|
|
dbProvider := setupAutomationTestDBProvider(t)
|
|
accountID, _ := seedTestAccount(dbProvider.DB(), t)
|
|
builder := NewTemplateContextBuilder(dbProvider)
|
|
|
|
// Create inbox
|
|
inbox := &model.Inbox{
|
|
AccountID: accountID, Name: "Support Inbox", ChannelType: "web_widget", ChannelID: 1,
|
|
}
|
|
if err := dbProvider.DB().Create(inbox).Error; err != nil {
|
|
t.Fatalf("failed to create inbox: %v", err)
|
|
}
|
|
|
|
// Create contact
|
|
contact := &model.Contact{
|
|
AccountID: accountID, Name: "Solo Customer", Email: "solo@test.com",
|
|
}
|
|
if err := dbProvider.DB().Select("AccountID", "Name", "Email").Create(contact).Error; err != nil {
|
|
t.Fatalf("failed to create contact: %v", err)
|
|
}
|
|
|
|
// Create conversation WITHOUT assignee
|
|
conv := &model.Conversation{
|
|
AccountID: accountID, InboxID: inbox.ID, ContactID: contact.ID,
|
|
AssigneeID: nil, Status: "pending", Priority: "low",
|
|
ChannelType: "web_widget", Channel: "web",
|
|
UUID: "test-uuid-no-assignee",
|
|
}
|
|
if err := dbProvider.DB().Select(
|
|
"AccountID", "InboxID", "ContactID",
|
|
"Status", "Priority", "ChannelType", "Channel", "UUID",
|
|
).Create(conv).Error; err != nil {
|
|
t.Fatalf("failed to create conversation: %v", err)
|
|
}
|
|
|
|
tctx, err := builder.Build(testContext, conv.ID)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
if tctx.Assignee != nil {
|
|
t.Fatalf("expected nil assignee for unassigned conversation, got non-nil")
|
|
}
|
|
|
|
// {{agent.name}} should resolve to empty string
|
|
result := ResolveTemplateVars("Assigned: {{agent.name}}", tctx)
|
|
if result != "Assigned: " {
|
|
t.Fatalf("expected 'Assigned: ', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
// testContext is a reusable context for DB-based tests.
|
|
var testContext = context.Background() |