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.
150 lines
6.1 KiB
Go
150 lines
6.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/worker"
|
|
)
|
|
|
|
type fakeCsatTemplateProvider struct {
|
|
calls int
|
|
result *CsatTemplateProviderResult
|
|
err error
|
|
}
|
|
|
|
func (p *fakeCsatTemplateProvider) CreateTemplate(ctx context.Context, inbox *model.Inbox, template *model.CsatTemplate, req CreateCsatTemplateRequest) (*CsatTemplateProviderResult, error) {
|
|
p.calls++
|
|
if p.err != nil {
|
|
return nil, p.err
|
|
}
|
|
if p.result != nil {
|
|
return p.result, nil
|
|
}
|
|
return &CsatTemplateProviderResult{TemplateExists: true, Status: "PENDING", TemplateName: "csat_survey_test", TemplateID: "tmpl_1", Language: "en"}, nil
|
|
}
|
|
|
|
func (p *fakeCsatTemplateProvider) GetTemplateStatus(ctx context.Context, inbox *model.Inbox, template *model.CsatTemplate) (*CsatTemplateProviderResult, error) {
|
|
return p.result, p.err
|
|
}
|
|
|
|
func TestCsatTemplateService_CreateTemplateQueuesDurableProviderCreation(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
repo := repository.NewCsatTemplateRepo(db)
|
|
svc := NewCsatTemplateService(repo)
|
|
provider := &fakeCsatTemplateProvider{result: &CsatTemplateProviderResult{TemplateExists: true, Status: "PENDING", TemplateName: "csat_survey_1", TemplateID: "tmpl_123", Language: "en"}}
|
|
svc.SetProvider(provider)
|
|
now := time.Date(2026, 6, 5, 22, 0, 0, 0, time.UTC)
|
|
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return now }))
|
|
svc.SetWorkerPool(wp)
|
|
account := createTestAccount(t, db)
|
|
inbox := createTestInbox(t, db, account.ID, string(model.InboxChannelTypeWhatsApp))
|
|
|
|
template, err := svc.CreateTemplate(context.Background(), inbox.ID, CreateCsatTemplateRequest{Message: "Please rate our support", ButtonText: "Please rate us", Language: "en"})
|
|
if err != nil {
|
|
t.Fatalf("create template: %v", err)
|
|
}
|
|
if template.Status != "pending" {
|
|
t.Fatalf("expected pending before worker, got %s", template.Status)
|
|
}
|
|
if provider.calls != 0 {
|
|
t.Fatalf("expected provider deferred to worker, got %d calls", provider.calls)
|
|
}
|
|
|
|
var jobCount int64
|
|
if err := db.Model(&model.BackgroundJob{}).Where("job_type = ? AND queue = ? AND status = ?", TaskTypeCsatTemplateCreate, "automation", model.BackgroundJobStatusQueued).Count(&jobCount).Error; err != nil {
|
|
t.Fatalf("count jobs: %v", err)
|
|
}
|
|
if jobCount != 1 {
|
|
t.Fatalf("expected one csat template job, got %d", jobCount)
|
|
}
|
|
processed, err := wp.ProcessOne(context.Background())
|
|
if err != nil || !processed {
|
|
t.Fatalf("process csat template job: processed=%v err=%v", processed, err)
|
|
}
|
|
|
|
var completed model.CsatTemplate
|
|
if err := db.First(&completed, template.ID).Error; err != nil {
|
|
t.Fatalf("load completed template: %v", err)
|
|
}
|
|
if completed.Status != "PENDING" {
|
|
t.Fatalf("expected provider status PENDING, got %s", completed.Status)
|
|
}
|
|
var reloadedInbox model.Inbox
|
|
if err := db.First(&reloadedInbox, inbox.ID).Error; err != nil {
|
|
t.Fatalf("load inbox: %v", err)
|
|
}
|
|
if !strings.Contains(reloadedInbox.CsatConfig, "csat_survey_1") || !strings.Contains(reloadedInbox.CsatConfig, "tmpl_123") {
|
|
t.Fatalf("expected inbox csat_config to include provider template metadata, got %s", reloadedInbox.CsatConfig)
|
|
}
|
|
if provider.calls != 1 {
|
|
t.Fatalf("expected one provider call, got %d", provider.calls)
|
|
}
|
|
}
|
|
|
|
func TestCsatTemplateService_TwilioTemplateStoresContentSid(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
repo := repository.NewCsatTemplateRepo(db)
|
|
svc := NewCsatTemplateService(repo)
|
|
svc.SetProvider(&fakeCsatTemplateProvider{result: &CsatTemplateProviderResult{TemplateExists: true, Status: "PENDING", FriendlyName: "csat_survey_2", ContentSID: "HX123", ApprovalSID: "HA123", Language: "en"}})
|
|
account := createTestAccount(t, db)
|
|
inbox := createTestInbox(t, db, account.ID, string(model.InboxChannelTypeTwilioSMS), func(inbox *model.Inbox) {
|
|
inbox.ChannelConfig = `{"medium":"whatsapp"}`
|
|
})
|
|
|
|
template, err := svc.CreateTemplate(context.Background(), inbox.ID, CreateCsatTemplateRequest{Message: "Rate us over WhatsApp"})
|
|
if err != nil {
|
|
t.Fatalf("create twilio template: %v", err)
|
|
}
|
|
if template.Status != "PENDING" {
|
|
t.Fatalf("expected sync provider status PENDING, got %s", template.Status)
|
|
}
|
|
var reloadedInbox model.Inbox
|
|
if err := db.First(&reloadedInbox, inbox.ID).Error; err != nil {
|
|
t.Fatalf("load inbox: %v", err)
|
|
}
|
|
if !strings.Contains(reloadedInbox.CsatConfig, "HX123") || !strings.Contains(reloadedInbox.CsatConfig, "HA123") {
|
|
t.Fatalf("expected twilio content/approval SIDs in csat_config, got %s", reloadedInbox.CsatConfig)
|
|
}
|
|
}
|
|
|
|
func TestCsatTemplateService_TemplateJobRetriesProviderFailures(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
repo := repository.NewCsatTemplateRepo(db)
|
|
svc := NewCsatTemplateService(repo)
|
|
svc.SetProvider(&fakeCsatTemplateProvider{err: errors.New("provider unavailable")})
|
|
now := time.Date(2026, 6, 5, 22, 30, 0, 0, time.UTC)
|
|
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return now }), worker.WithBackoff(func(attempt int) time.Duration { return time.Minute }))
|
|
svc.SetWorkerPool(wp)
|
|
account := createTestAccount(t, db)
|
|
inbox := createTestInbox(t, db, account.ID, string(model.InboxChannelTypeWhatsApp))
|
|
|
|
template, err := svc.CreateTemplate(context.Background(), inbox.ID, CreateCsatTemplateRequest{Message: "Please rate our support"})
|
|
if err != nil {
|
|
t.Fatalf("enqueue template create: %v", err)
|
|
}
|
|
processed, err := wp.ProcessOne(context.Background())
|
|
if err == nil || !processed {
|
|
t.Fatalf("expected provider failure to retry, processed=%v err=%v", processed, err)
|
|
}
|
|
var job model.BackgroundJob
|
|
if err := db.Where("job_type = ?", TaskTypeCsatTemplateCreate).First(&job).Error; err != nil {
|
|
t.Fatalf("load template job: %v", err)
|
|
}
|
|
if job.Status != model.BackgroundJobStatusRetrying || job.LastError == "" {
|
|
t.Fatalf("expected retrying template job with error, got status=%s last_error=%q", job.Status, job.LastError)
|
|
}
|
|
var failed model.CsatTemplate
|
|
if err := db.First(&failed, template.ID).Error; err != nil {
|
|
t.Fatalf("load failed template: %v", err)
|
|
}
|
|
if failed.Status != "failed" {
|
|
t.Fatalf("expected failed template status, got %s", failed.Status)
|
|
}
|
|
}
|