package automation import ( "context" "testing" "time" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/worker" ) func TestMacroService_Create(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) accountID, userID := seedTestAccount(dbProvider.DB(), t) svc := NewMacroService(dbProvider) macro := &Macro{ AccountID: accountID, Name: "Close and label", Actions: Actions{ {ActionName: "resolve", ActionParams: map[string]interface{}{}}, {ActionName: "add_label", ActionParams: map[string]interface{}{"labels": []string{"closed"}}}, }, Visibility: MacroVisibilityGlobal, CreatedByID: userID, UpdatedByID: userID, } err := svc.Create(context.Background(), macro) if err != nil { t.Fatalf("expected no error creating macro, got: %v", err) } if macro.ID == 0 { t.Fatal("expected macro ID to be set after creation") } } func TestMacroService_GetByID(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) accountID, userID := seedTestAccount(dbProvider.DB(), t) svc := NewMacroService(dbProvider) macro := &Macro{ AccountID: accountID, Name: "Test macro", Actions: Actions{}, Visibility: MacroVisibilityGlobal, CreatedByID: userID, UpdatedByID: userID, } if err := svc.Create(context.Background(), macro); err != nil { t.Fatalf("setup: %v", err) } found, err := svc.GetByID(context.Background(), macro.ID) if err != nil { t.Fatalf("expected no error getting macro, got: %v", err) } if found.ID != macro.ID { t.Fatalf("expected macro ID %d, got %d", macro.ID, found.ID) } if found.Name != "Test macro" { t.Fatalf("expected name 'Test macro', got %q", found.Name) } } func TestMacroService_GetByID_NotFound(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) svc := NewMacroService(dbProvider) _, err := svc.GetByID(context.Background(), 9999) if err == nil { t.Fatal("expected error for non-existent macro, got nil") } } func TestMacroService_ListByAccount(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) accountID, userID := seedTestAccount(dbProvider.DB(), t) svc := NewMacroService(dbProvider) // Create global macro m1 := &Macro{ AccountID: accountID, Name: "Global macro", Actions: Actions{}, Visibility: MacroVisibilityGlobal, CreatedByID: userID, UpdatedByID: userID, } if err := svc.Create(context.Background(), m1); err != nil { t.Fatalf("setup: %v", err) } // Create personal macro owned by userID m2 := &Macro{ AccountID: accountID, Name: "Personal macro (mine)", Actions: Actions{}, Visibility: MacroVisibilityPersonal, CreatedByID: userID, UpdatedByID: userID, } if err := svc.Create(context.Background(), m2); err != nil { t.Fatalf("setup: %v", err) } macros, err := svc.ListByAccount(context.Background(), accountID, userID) if err != nil { t.Fatalf("expected no error listing macros, got: %v", err) } // Should see both global and personal (owned by this user) if len(macros) != 2 { t.Fatalf("expected 2 macros (global + personal-own), got %d", len(macros)) } } func TestMacroService_ListByAccount_PersonalFiltering(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) accountID, userID := seedTestAccount(dbProvider.DB(), t) svc := NewMacroService(dbProvider) // Create a second user in the same account otherUser := &model.User{Name: "Other User", Email: "other@example.com"} db := dbProvider.DB() if err := db.Create(otherUser).Error; err != nil { t.Fatalf("setup: %v", err) } // Create personal macro owned by OTHER user (should NOT be visible to userID) m := &Macro{ AccountID: accountID, Name: "Someone else's macro", Actions: Actions{}, Visibility: MacroVisibilityPersonal, CreatedByID: otherUser.ID, UpdatedByID: otherUser.ID, } if err := svc.Create(context.Background(), m); err != nil { t.Fatalf("setup: %v", err) } // userID should not see other user's personal macro macros, err := svc.ListByAccount(context.Background(), accountID, userID) if err != nil { t.Fatalf("expected no error, got: %v", err) } for _, mc := range macros { if mc.Visibility == MacroVisibilityPersonal && mc.CreatedByID != userID { t.Fatalf("should not see other user's personal macro: %q (created_by_id=%d)", mc.Name, mc.CreatedByID) } } } func TestMacroService_Update(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) accountID, userID := seedTestAccount(dbProvider.DB(), t) svc := NewMacroService(dbProvider) macro := &Macro{ AccountID: accountID, Name: "Original", Actions: Actions{}, Visibility: MacroVisibilityPersonal, CreatedByID: userID, UpdatedByID: userID, } if err := svc.Create(context.Background(), macro); err != nil { t.Fatalf("setup: %v", err) } macro.Name = "Updated" macro.Visibility = MacroVisibilityGlobal err := svc.Update(context.Background(), macro) if err != nil { t.Fatalf("expected no error updating macro, got: %v", err) } found, _ := svc.GetByID(context.Background(), macro.ID) if found.Name != "Updated" { t.Fatalf("expected name 'Updated', got %q", found.Name) } if found.Visibility != MacroVisibilityGlobal { t.Fatal("expected visibility to be global after update") } } func TestMacroService_Delete(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) accountID, userID := seedTestAccount(dbProvider.DB(), t) svc := NewMacroService(dbProvider) macro := &Macro{ AccountID: accountID, Name: "Delete me", Actions: Actions{}, Visibility: MacroVisibilityGlobal, CreatedByID: userID, UpdatedByID: userID, } if err := svc.Create(context.Background(), macro); err != nil { t.Fatalf("setup: %v", err) } err := svc.Delete(context.Background(), macro.ID) if err != nil { t.Fatalf("expected no error deleting macro, got: %v", err) } _, err = svc.GetByID(context.Background(), macro.ID) if err == nil { t.Fatal("expected error getting deleted macro, got nil") } } func TestMacroService_ExecuteForDisplayIDsQueuesDurableMacroJob(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) db := dbProvider.DB() accountID, userID := seedTestAccount(db, t) inboxID := seedTestInbox(db, t, accountID) contactID := seedTestContact(db, t, accountID) displayID := uint(616) conversationID := seedTestConversationWithDetails(db, t, accountID, inboxID, contactID, "open", "low", "web", 0) if err := db.Model(&model.Conversation{}).Where("id = ?", conversationID).Update("display_id", displayID).Error; err != nil { t.Fatalf("set display id: %v", err) } macro := &Macro{ AccountID: accountID, Name: "durable macro", Actions: Actions{ {ActionName: "add_label", ActionParams: map[string]interface{}{"labels": []string{"durable"}}}, {ActionName: "change_status", ActionParams: map[string]interface{}{"status": "resolved"}}, }, Visibility: MacroVisibilityGlobal, CreatedByID: userID, UpdatedByID: userID, } if err := NewMacroService(dbProvider).Create(context.Background(), macro); err != nil { t.Fatalf("create macro: %v", err) } wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 5, 13, 15, 0, 0, time.UTC) })) svc := NewMacroServiceWithWorker(dbProvider, wp) if err := svc.ExecuteForDisplayIDs(context.Background(), accountID, macro.ID, []uint{displayID}, userID); err != nil { t.Fatalf("queue macro execution: %v", err) } var before model.Conversation if err := db.First(&before, conversationID).Error; err != nil { t.Fatalf("load conversation before worker: %v", err) } if before.Status != "open" { t.Fatalf("macro should not run synchronously, status=%s", before.Status) } var count int64 if err := db.Model(&model.BackgroundJob{}).Where("job_type = ? AND queue = ? AND status = ?", TaskTypeMacroExecution, "medium", model.BackgroundJobStatusQueued).Count(&count).Error; err != nil { t.Fatalf("count macro jobs: %v", err) } if count != 1 { t.Fatalf("expected one queued macro job, got %d", count) } processed, err := wp.ProcessOne(context.Background()) if err != nil || !processed { t.Fatalf("process macro job: processed=%v err=%v", processed, err) } var after model.Conversation if err := db.First(&after, conversationID).Error; err != nil { t.Fatalf("load conversation after worker: %v", err) } if after.Status != "resolved" { t.Fatalf("expected macro job to resolve conversation, got %s", after.Status) } var label ConversationLabel if err := db.Where("conversation_id = ? AND label = ?", conversationID, "durable").First(&label).Error; err != nil { t.Fatalf("expected macro job to add label: %v", err) } } func TestMacroService_ExecuteForDisplayIDsCreatesAttachmentMessages(t *testing.T) { dbProvider := setupAutomationTestDBProvider(t) db := dbProvider.DB() accountID, userID := seedTestAccount(db, t) inboxID := seedTestInbox(db, t, accountID) contactID := seedTestContact(db, t, accountID) displayID := uint(717) conversationID := seedTestConversationWithDetails(db, t, accountID, inboxID, contactID, "open", "low", "web", 0) if err := db.Model(&model.Conversation{}).Where("id = ?", conversationID).Update("display_id", displayID).Error; err != nil { t.Fatalf("set display id: %v", err) } upload := &model.DirectUpload{UploadUUID: "macro-exec-upload", AccountID: accountID, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "macro.pdf", FileType: "file", MimeType: "application/pdf", FileSize: 456, FileURL: "/uploads/account/1/macro.pdf"} if err := db.Create(upload).Error; err != nil { t.Fatalf("create upload: %v", err) } macro := &Macro{AccountID: accountID, Name: "file macro", Actions: Actions{{ActionName: "send_attachment", ActionParams: map[string]interface{}{"blob_id": "macro-exec-upload"}}}, Visibility: MacroVisibilityGlobal, CreatedByID: userID, UpdatedByID: userID} svc := NewMacroService(dbProvider) if err := svc.Create(context.Background(), macro); err != nil { t.Fatalf("create macro: %v", err) } if err := svc.ExecuteForDisplayIDs(context.Background(), accountID, macro.ID, []uint{displayID}, userID); err != nil { t.Fatalf("execute macro: %v", err) } var message model.Message if err := db.Where("conversation_id = ? AND content_type = ?", conversationID, "file").First(&message).Error; err != nil { t.Fatalf("expected file message: %v", err) } if message.SenderID == nil || *message.SenderID != userID { t.Fatalf("expected macro sender %d, got %#v", userID, message.SenderID) } var attachment model.Attachment if err := db.Where("message_id = ?", message.ID).First(&attachment).Error; err != nil { t.Fatalf("expected attachment: %v", err) } if attachment.FileURL != upload.FileURL || attachment.FileName != upload.OriginalName { t.Fatalf("unexpected attachment: %#v", attachment) } }