Files
gochat/backend/internal/automation/template_variable_test.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
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.
2026-07-07 14:44:12 +08:00

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()