Files
gochat/backend/tests/e2e/e2e_test.go
T
2026-08-18 00:55:45 +08:00

297 lines
9.0 KiB
Go

package e2e
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/suite"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/auth"
"github.com/gochat/gochat/internal/config"
handler "github.com/gochat/gochat/internal/handler/api/v1"
"github.com/gochat/gochat/internal/middleware"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
testhelpers "github.com/gochat/gochat/tests/helpers"
)
// E2ETestSuite provides a full end-to-end test environment with a live HTTP server
// and a PostgreSQL database. All database-backed E2E test files use this suite.
// Reference: Chatwoot's integration test setup pattern (spec/integration/ helpers)
type E2ETestSuite struct {
suite.Suite
db *gorm.DB
server *httptest.Server
router *gin.Engine
config *config.Config
httpClient *http.Client
ctx context.Context
}
// SetupSuite initializes the test database and HTTP server once for the entire suite.
func (s *E2ETestSuite) SetupSuite() {
if !testhelpers.UsePostgres() {
s.T().Skip("database-backed E2E tests require PostgreSQL")
}
db := testhelpers.SetupTestDB(s.T())
s.db = db
// Create test config
cfg := &config.Config{
JWT: config.JWTConfig{
Secret: "e2e-test-secret-key",
ExpiryHours: 1,
RefreshExpiryHours: 24,
},
Server: config.ServerConfig{
Port: 0, // Use httptest, not real port
},
}
s.config = cfg
// Build router with test DB — minimal setup for e2e
r := gin.New()
r.Use(gin.Recovery())
s.router = r
// Register basic health route
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Wire auth routes for e2e testing
jwtService := auth.NewJWTService(&cfg.JWT)
// Use miniredis for refresh token store — avoids nil Redis panic
mr := miniredis.NewMiniRedis()
mr.Start()
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
_ = redisClient // keep miniredis alive for test duration
refreshTokenStore := auth.NewRefreshTokenStore(redisClient, &cfg.JWT)
authService := service.NewAuthService(db, jwtService, refreshTokenStore)
authHandler := handler.NewAuthHandler(authService)
// Register auth routes using helper function
handler.RegisterAuthRoutes(r.Group("/api/v1"), authHandler)
// Wire protected routes with AuthMiddleware
authMiddleware := middleware.AuthMiddleware(&cfg.JWT)
accountRepo := repository.NewAccountRepo(db)
accountService := service.NewAccountService(accountRepo)
accountHandler := handler.NewAccountHandler(accountService)
contactRepo := repository.NewContactRepo(db)
contactInboxRepo := repository.NewContactInboxRepo(db)
contactInboxService := service.NewContactInboxService(contactInboxRepo)
noteRepo := repository.NewNoteRepo(db)
contactService := service.NewContactService(contactRepo, contactInboxService, noteRepo)
contactMergeRepo := repository.NewContactMergeRepo(db)
contactMergeService := service.NewContactMergeService(contactMergeRepo, db)
contactNoteRepo := repository.NewContactNoteRepo(db)
contactNoteService := service.NewContactNoteService(contactRepo, contactNoteRepo)
contactHandler := handler.NewContactHandler(contactService, contactInboxService, contactMergeService, contactNoteService)
inboxRepo := repository.NewInboxRepo(db)
inboxService := service.NewInboxService(inboxRepo, nil, nil, nil, nil, nil, nil)
inboxHandler := handler.NewInboxHandler(inboxService)
protected := r.Group("/api/v1")
protected.Use(authMiddleware)
{
// Account CRUD
protected.GET("/accounts", accountHandler.List)
protected.GET("/accounts/:account_id", accountHandler.Get)
protected.POST("/accounts", accountHandler.Create)
protected.PUT("/accounts/:account_id", accountHandler.Update)
protected.DELETE("/accounts/:account_id", accountHandler.Delete)
// Account-scoped routes mirror the production router's :account_id parameter.
contacts := protected.Group("/accounts/:account_id")
{
contacts.GET("/contacts", contactHandler.List)
contacts.POST("/contacts", contactHandler.Create)
contacts.GET("/contacts/:contact_id", contactHandler.Get)
contacts.PUT("/contacts/:contact_id", contactHandler.Update)
contacts.DELETE("/contacts/:contact_id", contactHandler.Delete)
contacts.GET("/contacts/search", contactHandler.Search)
}
// Inbox CRUD
inboxes := protected.Group("/accounts/:account_id")
{
inboxes.GET("/inboxes", inboxHandler.List)
inboxes.POST("/inboxes", inboxHandler.Create)
inboxes.GET("/inboxes/:inbox_id", inboxHandler.Get)
inboxes.PUT("/inboxes/:inbox_id", inboxHandler.Update)
inboxes.DELETE("/inboxes/:inbox_id", inboxHandler.Delete)
}
}
// Create httptest server
s.server = httptest.NewServer(r)
s.httpClient = &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
s.ctx = context.Background()
fmt.Printf("E2E test server running at %s\n", s.server.URL)
}
// TearDownSuite closes the test server and database.
func (s *E2ETestSuite) TearDownSuite() {
if s.server != nil {
s.server.Close()
}
if s.db != nil {
sqlDB, err := s.db.DB()
if err == nil {
sqlDB.Close()
}
}
}
// SetupTest clears all database tables before each test.
func (s *E2ETestSuite) SetupTest() {
s.ClearDatabase()
}
// TearDownTest resets state after each test.
func (s *E2ETestSuite) TearDownTest() {}
// ClearDatabase truncates all tables to ensure test isolation.
func (s *E2ETestSuite) ClearDatabase() {
s.db.Exec("DELETE FROM dashboard_apps")
s.db.Exec("DELETE FROM messages")
s.db.Exec("DELETE FROM conversations")
s.db.Exec("DELETE FROM inbox_members")
s.db.Exec("DELETE FROM inboxes")
s.db.Exec("DELETE FROM attachments")
s.db.Exec("DELETE FROM notifications")
s.db.Exec("DELETE FROM notification_preferences")
s.db.Exec("DELETE FROM contact_inboxes")
s.db.Exec("DELETE FROM contacts")
s.db.Exec("DELETE FROM account_users")
s.db.Exec("DELETE FROM custom_roles")
s.db.Exec("DELETE FROM accounts")
s.db.Exec("DELETE FROM users")
}
// ServerURL returns the base URL of the test server.
func (s *E2ETestSuite) ServerURL() string {
return s.server.URL
}
// DB returns the test database instance.
func (s *E2ETestSuite) DB() *gorm.DB {
return s.db
}
// Config returns the test config.
func (s *E2ETestSuite) Config() *config.Config {
return s.config
}
// Router returns the test router engine.
func (s *E2ETestSuite) Router() *gin.Engine {
return s.router
}
// CreateTestAccount creates a test account in the database.
func (s *E2ETestSuite) CreateTestAccount(name string) *model.Account {
account := &model.Account{
Name: name,
Locale: "zh_CN",
Timezone: "UTC",
Active: true,
}
err := s.db.Create(account).Error
s.Require().NoError(err)
return account
}
// CreateTestUser creates a test user in the database.
func (s *E2ETestSuite) CreateTestUser(email, name, password, role string, accountID uint) *model.User {
now := time.Now()
user := &model.User{
AccountID: accountID,
Name: name,
Email: email,
Password: password,
Provider: "email",
Active: true,
Available: true,
ConfirmedAt: &now,
}
err := s.db.Create(user).Error
s.Require().NoError(err)
return user
}
// CreateTestInbox creates a test inbox in the database.
func (s *E2ETestSuite) CreateTestInbox(name, channelType string, accountID uint) *model.Inbox {
inbox := &model.Inbox{
AccountID: accountID,
Name: name,
ChannelType: channelType,
ChannelID: 1,
}
err := s.db.Create(inbox).Error
s.Require().NoError(err)
return inbox
}
// CreateTestContact creates a test contact in the database.
func (s *E2ETestSuite) CreateTestContact(name, email string, accountID uint) *model.Contact {
contact := &model.Contact{
AccountID: accountID,
Name: name,
Email: email,
}
err := s.db.Create(contact).Error
s.Require().NoError(err)
return contact
}
// CreateTestConversation creates a test conversation.
func (s *E2ETestSuite) CreateTestConversation(accountID, inboxID, contactID uint, status string) *model.Conversation {
// Look up the inbox to get its ChannelType
var inbox model.Inbox
err := s.db.Where("id = ?", inboxID).First(&inbox).Error
s.Require().NoError(err)
conv := &model.Conversation{
AccountID: accountID,
InboxID: inboxID,
ContactID: contactID,
Status: status,
ChannelType: inbox.ChannelType,
}
err = s.db.Create(conv).Error
s.Require().NoError(err)
return conv
}
// MakeRequest sends an HTTP request to the test server.
func (s *E2ETestSuite) MakeRequest(method, path string, body interface{}, headers map[string]string) *http.Response {
return s.makeRequestWithClient(method, path, body, headers, s.httpClient)
}
// makeRequestWithClient sends an HTTP request using a specific client.
func (s *E2ETestSuite) makeRequestWithClient(method, path string, body interface{}, headers map[string]string, client *http.Client) *http.Response {
// Implementation left for individual test files
return nil
}