package service import ( "context" "encoding/json" "testing" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) // ========== Portal test helpers ========== // setupPortalService creates PortalRepo + PortalService test instances. func setupPortalService(t *testing.T) (*gorm.DB, *repository.PortalRepo, *PortalService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewPortalRepo(db) svc := NewPortalService(repo) return db, repo, svc } // ========== Create ========== func TestPortalService_Create(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) req := &CreatePortalRequest{ Name: "Help Center", Slug: "help-center", Description: "Customer support portal", LogoURL: "https://example.com/logo.png", HeaderText: "Welcome to our help center", HomepageLink: "https://example.com", PageTitle: "Help Center", Color: "#ff6600", CustomDomain: "help.example.com", Locale: "zh", PortalConfiguration: json.RawMessage(`{"allowed_locales":["en","zh"]}`), SSLSettings: json.RawMessage(`{"enabled":true}`), HomepageContent: "Some homepage content", } portal, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) assert.NotZero(t, portal.ID) assert.Equal(t, account.ID, portal.AccountID) assert.Equal(t, "Help Center", portal.Name) assert.Equal(t, "help-center", portal.Slug) assert.Equal(t, "Customer support portal", portal.Description) assert.Equal(t, "https://example.com/logo.png", portal.LogoURL) assert.Equal(t, "Welcome to our help center", portal.HeaderText) assert.Equal(t, "https://example.com", portal.HomepageLink) assert.Equal(t, "Help Center", portal.PageTitle) assert.Equal(t, "#ff6600", portal.Color) assert.Equal(t, "help.example.com", portal.CustomDomain) assert.Equal(t, "zh", portal.Locale) assert.Equal(t, json.RawMessage(`{"allowed_locales":["en","zh"]}`), portal.PortalConfiguration) assert.Equal(t, json.RawMessage(`{"enabled":true}`), portal.SSLSettings) assert.Equal(t, "Some homepage content", portal.HomepageContent) assert.False(t, portal.Archived) } func TestPortalService_Create_Defaults(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) // Create with minimal fields — Color and Locale should default req := &CreatePortalRequest{ Name: "Minimal Portal", Slug: "minimal-portal", } portal, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) assert.NotZero(t, portal.ID) assert.Equal(t, "#1f93ff", portal.Color) // default color assert.Equal(t, "en", portal.Locale) // default locale assert.Equal(t, json.RawMessage(`{}`), portal.SSLSettings) // default SSL assert.False(t, portal.Archived) } // ========== GetByID ========== func TestPortalService_GetByID(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) portal := createTestPortal(t, db, account.ID) got, err := svc.GetByID(context.Background(), portal.ID) require.NoError(t, err) assert.Equal(t, portal.ID, got.ID) assert.Equal(t, portal.AccountID, got.AccountID) assert.Equal(t, portal.Name, got.Name) assert.Equal(t, portal.Slug, got.Slug) } func TestPortalService_GetByID_NotFound(t *testing.T) { _, _, svc := setupPortalService(t) _, err := svc.GetByID(context.Background(), 99999) require.Error(t, err) assert.Contains(t, err.Error(), "get portal") } // ========== Update ========== func TestPortalService_Update(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) portal := createTestPortal(t, db, account.ID) archived := true req := &UpdatePortalRequest{ Name: "Updated KB", Description: "Updated description", LogoURL: "https://example.com/new-logo.png", HeaderText: "Updated header", HomepageLink: "https://updated.example.com", PageTitle: "Updated Title", Color: "#00ff00", Archived: &archived, CustomDomain: "kb.updated.com", Locale: "fr", PortalConfiguration: json.RawMessage(`{"layout":"grid"}`), SSLSettings: json.RawMessage(`{"enabled":false}`), HomepageContent: "Updated content", } updated, err := svc.Update(context.Background(), portal.ID, req) require.NoError(t, err) assert.Equal(t, "Updated KB", updated.Name) assert.Equal(t, "Updated description", updated.Description) assert.Equal(t, "https://example.com/new-logo.png", updated.LogoURL) assert.Equal(t, "Updated header", updated.HeaderText) assert.Equal(t, "https://updated.example.com", updated.HomepageLink) assert.Equal(t, "Updated Title", updated.PageTitle) assert.Equal(t, "#00ff00", updated.Color) assert.True(t, updated.Archived) assert.Equal(t, "kb.updated.com", updated.CustomDomain) assert.Equal(t, "fr", updated.Locale) assert.Equal(t, json.RawMessage(`{"layout":"grid"}`), updated.PortalConfiguration) assert.Equal(t, json.RawMessage(`{"enabled":false}`), updated.SSLSettings) assert.Equal(t, "Updated content", updated.HomepageContent) } func TestPortalService_Update_PartialFields(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) portal := createTestPortal(t, db, account.ID, func(p *model.Portal) { p.Description = "Original desc" p.Color = "#1f93ff" p.Locale = "en" }) // Only update Name and Description — other fields should remain unchanged req := &UpdatePortalRequest{ Name: "Partial Update", Description: "New description", } updated, err := svc.Update(context.Background(), portal.ID, req) require.NoError(t, err) assert.Equal(t, "Partial Update", updated.Name) assert.Equal(t, "New description", updated.Description) // Unchanged fields assert.Equal(t, "#1f93ff", updated.Color) assert.Equal(t, "en", updated.Locale) } func TestPortalService_Update_NotFound(t *testing.T) { _, _, svc := setupPortalService(t) req := &UpdatePortalRequest{ Name: "Does Not Exist", } _, err := svc.Update(context.Background(), 99999, req) require.Error(t, err) assert.Contains(t, err.Error(), "find portal") } // ========== Delete ========== func TestPortalService_Delete(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) portal := createTestPortal(t, db, account.ID) err := svc.Delete(context.Background(), portal.ID) require.NoError(t, err) // Verify the portal is gone _, getErr := svc.GetByID(context.Background(), portal.ID) require.Error(t, getErr) } func TestPortalService_Delete_NotFound(t *testing.T) { _, _, svc := setupPortalService(t) err := svc.Delete(context.Background(), 99999) require.Error(t, err) assert.Contains(t, err.Error(), "find portal for delete") } // ========== ListByAccountID ========== func TestPortalService_ListByAccountID(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) // Create multiple portals portal1 := createTestPortal(t, db, account.ID, func(p *model.Portal) { p.Name = "Portal One" p.Slug = "portal-one-" + p.Slug }) portal2 := createTestPortal(t, db, account.ID, func(p *model.Portal) { p.Name = "Portal Two" p.Slug = "portal-two-" + p.Slug }) portal3 := createTestPortal(t, db, account.ID, func(p *model.Portal) { p.Name = "Portal Three" p.Slug = "portal-three-" + p.Slug }) portals, count, err := svc.ListByAccountID(context.Background(), account.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(3), count) assert.Len(t, portals, 3) // Check all portal IDs are present ids := make(map[uint]bool) for _, p := range portals { ids[p.ID] = true } assert.True(t, ids[portal1.ID]) assert.True(t, ids[portal2.ID]) assert.True(t, ids[portal3.ID]) } func TestPortalService_ListByAccountID_Pagination(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) // Create 5 portals for i := 0; i < 5; i++ { createTestPortal(t, db, account.ID) } // Page 1, perPage=2 → 2 items, total count=5 portals, count, err := svc.ListByAccountID(context.Background(), account.ID, 1, 2) require.NoError(t, err) assert.Equal(t, int64(5), count) assert.Len(t, portals, 2) // Page 2, perPage=2 → 2 items portals2, count2, err := svc.ListByAccountID(context.Background(), account.ID, 2, 2) require.NoError(t, err) assert.Equal(t, int64(5), count2) assert.Len(t, portals2, 2) // Page 3, perPage=2 → 1 item portals3, count3, err := svc.ListByAccountID(context.Background(), account.ID, 3, 2) require.NoError(t, err) assert.Equal(t, int64(5), count3) assert.Len(t, portals3, 1) } func TestPortalService_ListByAccountID_EmptyAccount(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) // No portals created for this account portals, count, err := svc.ListByAccountID(context.Background(), account.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(0), count) assert.Len(t, portals, 0) } func TestPortalService_ListByAccountID_DifferentAccounts(t *testing.T) { db, _, svc := setupPortalService(t) account1 := createTestAccountWithName(t, db, "Account One") account2 := createTestAccountWithName(t, db, "Account Two") // Create portals for account1 only createTestPortal(t, db, account1.ID) createTestPortal(t, db, account1.ID) // account2 should have 0 portals portals, count, err := svc.ListByAccountID(context.Background(), account2.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(0), count) assert.Len(t, portals, 0) // account1 should have 2 portals portals1, count1, err := svc.ListByAccountID(context.Background(), account1.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(2), count1) assert.Len(t, portals1, 2) } // ========== Archive ========== func TestPortalService_Archive(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) portal := createTestPortal(t, db, account.ID) assert.False(t, portal.Archived) archived, err := svc.Archive(context.Background(), portal.ID) require.NoError(t, err) assert.True(t, archived.Archived) assert.Equal(t, portal.ID, archived.ID) assert.Equal(t, portal.Name, archived.Name) } func TestPortalService_Archive_NotFound(t *testing.T) { _, _, svc := setupPortalService(t) // Archive on non-existent portal — the repo's Archive method does a WHERE update // which silently succeeds for non-existent IDs, then GetByID fails _, err := svc.Archive(context.Background(), 99999) require.Error(t, err) } // ========== RemoveLogo ========== func TestPortalService_RemoveLogo(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) portal := createTestPortal(t, db, account.ID, func(p *model.Portal) { p.LogoURL = "https://example.com/logo.png" }) assert.Equal(t, "https://example.com/logo.png", portal.LogoURL) updated, err := svc.RemoveLogo(context.Background(), portal.ID) require.NoError(t, err) assert.Equal(t, "", updated.LogoURL) assert.Equal(t, portal.ID, updated.ID) assert.Equal(t, portal.Name, updated.Name) } func TestPortalService_RemoveLogo_NotFound(t *testing.T) { _, _, svc := setupPortalService(t) // RemoveLogo on non-existent portal — repo RemoveLogo silently succeeds for // non-existent IDs, then GetByID fails _, err := svc.RemoveLogo(context.Background(), 99999) require.Error(t, err) } // ========== SendInstructions ========== func TestPortalService_SendInstructions_Success(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) // Create portal with custom domain req := &CreatePortalRequest{ Name: "Help Center", Slug: "help-send", CustomDomain: "help.example.com", } _, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) // Fetch the portal from DB directly to get its ID var portal model.Portal require.NoError(t, db.Where("slug = ?", "help-send").First(&portal).Error) err = svc.SendInstructions(context.Background(), portal.ID, &SendInstructionsRequest{Email: "admin@example.com"}) assert.NoError(t, err) } func TestPortalService_SendInstructions_NoCustomDomain(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) // Create portal without custom domain req := &CreatePortalRequest{ Name: "Help Center", Slug: "help-no-domain", } portal, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) err = svc.SendInstructions(context.Background(), portal.ID, &SendInstructionsRequest{Email: "admin@example.com"}) assert.Error(t, err) } func TestPortalService_SendInstructions_InvalidEmail(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) req := &CreatePortalRequest{ Name: "Help Center", Slug: "help-bad-email", CustomDomain: "help.example.com", } portal, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) err = svc.SendInstructions(context.Background(), portal.ID, &SendInstructionsRequest{Email: "not-an-email"}) assert.Error(t, err) } func TestPortalService_SendInstructions_PortalNotFound(t *testing.T) { _, _, svc := setupPortalService(t) err := svc.SendInstructions(context.Background(), 99999, &SendInstructionsRequest{Email: "admin@example.com"}) assert.Error(t, err) } // ========== SSLStatus ========== func TestPortalService_SSLStatus_NoCustomDomain(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) req := &CreatePortalRequest{ Name: "Help Center", Slug: "help-ssl-no", } portal, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) status, err := svc.SSLStatus(context.Background(), portal.ID) require.NoError(t, err) assert.Equal(t, "no_custom_domain", status.SSLState) } func TestPortalService_SSLStatus_WithCustomDomain(t *testing.T) { db, _, svc := setupPortalService(t) account := createTestAccount(t, db) req := &CreatePortalRequest{ Name: "Help Center", Slug: "help-ssl-yes", CustomDomain: "help.example.com", } portal, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) status, err := svc.SSLStatus(context.Background(), portal.ID) require.NoError(t, err) assert.Equal(t, portal.ID, status.PortalID) assert.Equal(t, "help.example.com", status.CustomDomain) // Default SSL state is "pending" for newly created portals assert.Equal(t, "pending", status.SSLState) }