297 lines
9.0 KiB
Go
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
|
|
}
|