Files
gochat/internal/service/portal_service_test.go
T
2026-06-04 15:44:48 +08:00

460 lines
14 KiB
Go

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