package integration
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"goyco/internal/config"
"goyco/internal/database"
"goyco/internal/handlers"
"goyco/internal/middleware"
"goyco/internal/repositories"
"goyco/internal/server"
"goyco/internal/services"
"goyco/internal/testutils"
)
type testContext struct {
Router http.Handler
Suite *testutils.ServiceSuite
AuthService *services.AuthFacade
}
type commonHandlers struct {
AuthService *services.AuthFacade
VoteService *services.VoteService
MetadataService services.TitleFetcher
AuthHandler *handlers.AuthHandler
PostHandler *handlers.PostHandler
VoteHandler *handlers.VoteHandler
UserHandler *handlers.UserHandler
APIHandler *handlers.APIHandler
}
func setupCommonHandlers(t *testing.T, suite *testutils.ServiceSuite, useMonitoring bool) *commonHandlers {
t.Helper()
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
if err != nil {
t.Fatalf("Failed to create auth service: %v", err)
}
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
metadataService := suite.TitleFetcher
authHandler := handlers.NewAuthHandler(authService, suite.UserRepo)
postHandler := handlers.NewPostHandler(suite.PostRepo, metadataService, voteService)
voteHandler := handlers.NewVoteHandler(voteService)
userHandler := handlers.NewUserHandler(suite.UserRepo, authService)
var apiHandler *handlers.APIHandler
if useMonitoring {
apiHandler = handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService, suite.DB, middleware.NewInMemoryDBMonitor())
} else {
apiHandler = handlers.NewAPIHandler(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService)
}
return &commonHandlers{
AuthService: authService,
VoteService: voteService,
MetadataService: metadataService,
AuthHandler: authHandler,
PostHandler: postHandler,
VoteHandler: voteHandler,
UserHandler: userHandler,
APIHandler: apiHandler,
}
}
type routerConfigBuilder struct {
authHandler *handlers.AuthHandler
postHandler *handlers.PostHandler
voteHandler *handlers.VoteHandler
userHandler *handlers.UserHandler
apiHandler *handlers.APIHandler
authService middleware.TokenVerifier
pageHandler *handlers.PageHandler
staticDir string
debug bool
disableCache bool
disableCompression bool
dbMonitor middleware.DBMonitor
rateLimitConfig config.RateLimitConfig
}
func newRouterConfigBuilder() *routerConfigBuilder {
return &routerConfigBuilder{
debug: false,
disableCache: false,
disableCompression: false,
dbMonitor: middleware.NewInMemoryDBMonitor(),
rateLimitConfig: testutils.AppTestConfig.RateLimit,
}
}
func (b *routerConfigBuilder) withHandlers(h *commonHandlers) *routerConfigBuilder {
b.authHandler = h.AuthHandler
b.postHandler = h.PostHandler
b.voteHandler = h.VoteHandler
b.userHandler = h.UserHandler
b.apiHandler = h.APIHandler
b.authService = h.AuthService
return b
}
func (b *routerConfigBuilder) withIndividualHandlers(
authHandler *handlers.AuthHandler,
postHandler *handlers.PostHandler,
voteHandler *handlers.VoteHandler,
userHandler *handlers.UserHandler,
apiHandler *handlers.APIHandler,
authService middleware.TokenVerifier,
) *routerConfigBuilder {
b.authHandler = authHandler
b.postHandler = postHandler
b.voteHandler = voteHandler
b.userHandler = userHandler
b.apiHandler = apiHandler
b.authService = authService
return b
}
func (b *routerConfigBuilder) withPageHandler(pageHandler *handlers.PageHandler) *routerConfigBuilder {
b.pageHandler = pageHandler
return b
}
func (b *routerConfigBuilder) withStaticDir(staticDir string) *routerConfigBuilder {
b.staticDir = staticDir
return b
}
func (b *routerConfigBuilder) withRateLimitConfig(rateLimitConfig config.RateLimitConfig) *routerConfigBuilder {
b.rateLimitConfig = rateLimitConfig
return b
}
func (b *routerConfigBuilder) build() server.RouterConfig {
return server.RouterConfig{
AuthHandler: b.authHandler,
PostHandler: b.postHandler,
VoteHandler: b.voteHandler,
UserHandler: b.userHandler,
APIHandler: b.apiHandler,
AuthService: b.authService,
PageHandler: b.pageHandler,
StaticDir: b.staticDir,
Debug: b.debug,
DisableCache: b.disableCache,
DisableCompression: b.disableCompression,
DBMonitor: b.dbMonitor,
RateLimitConfig: b.rateLimitConfig,
}
}
func buildRouterConfig(h *commonHandlers, staticDir string, pageHandler *handlers.PageHandler) server.RouterConfig {
return newRouterConfigBuilder().
withHandlers(h).
withPageHandler(pageHandler).
withStaticDir(staticDir).
build()
}
func setupTestContext(t *testing.T) *testContext {
t.Helper()
middleware.StopAllRateLimiters()
suite := testutils.NewServiceSuite(t)
h := setupCommonHandlers(t, suite, true)
staticDir := t.TempDir()
robotsFile := filepath.Join(staticDir, "robots.txt")
os.WriteFile(robotsFile, []byte("User-agent: *\nDisallow: /"), 0644)
router := server.NewRouter(buildRouterConfig(h, staticDir, nil))
return &testContext{
Router: router,
Suite: suite,
AuthService: h.AuthService,
}
}
func setupPageHandlerTestContext(t *testing.T) *testContext {
t.Helper()
middleware.StopAllRateLimiters()
suite := testutils.NewServiceSuite(t)
h := setupCommonHandlers(t, suite, false)
staticDir := t.TempDir()
templatesDir := t.TempDir()
baseTemplate := `{{define "layout"}}
{{.Title}}
{{block "content" .}}{{end}}
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "base.gohtml"), []byte(baseTemplate), 0644)
os.MkdirAll(filepath.Join(templatesDir, "partials"), 0755)
homeTemplate := `{{define "content"}}Home
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "home.gohtml"), []byte(homeTemplate), 0644)
loginTemplate := `{{define "content"}}Login
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "login.gohtml"), []byte(loginTemplate), 0644)
registerTemplate := `{{define "content"}}Register
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "register.gohtml"), []byte(registerTemplate), 0644)
settingsTemplate := `{{define "content"}}Settings
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "settings.gohtml"), []byte(settingsTemplate), 0644)
postTemplate := `{{define "content"}}{{.Post.Title}}
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "post.gohtml"), []byte(postTemplate), 0644)
errorTemplate := `{{define "content"}}Error
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "error.gohtml"), []byte(errorTemplate), 0644)
confirmTemplate := `{{define "content"}}Confirm
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "confirm.gohtml"), []byte(confirmTemplate), 0644)
confirmEmailTemplate := `{{define "content"}}Confirm Email
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "confirm_email.gohtml"), []byte(confirmEmailTemplate), 0644)
resendTemplate := `{{define "content"}}Resend
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "resend-verification.gohtml"), []byte(resendTemplate), 0644)
resendVerificationTemplate := `{{define "content"}}Resend Verification
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "resend_verification.gohtml"), []byte(resendVerificationTemplate), 0644)
forgotTemplate := `{{define "content"}}Forgot Password
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "forgot-password.gohtml"), []byte(forgotTemplate), 0644)
forgotPasswordTemplate := `{{define "content"}}Forgot Password
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "forgot_password.gohtml"), []byte(forgotPasswordTemplate), 0644)
resetTemplate := `{{define "content"}}Reset Password
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "reset-password.gohtml"), []byte(resetTemplate), 0644)
resetPasswordTemplate := `{{define "content"}}Reset Password
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "reset_password.gohtml"), []byte(resetPasswordTemplate), 0644)
searchTemplate := `{{define "content"}}Search
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "search.gohtml"), []byte(searchTemplate), 0644)
newPostTemplate := `{{define "content"}}New Post
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "new-post.gohtml"), []byte(newPostTemplate), 0644)
newPostTemplate2 := `{{define "content"}}New Post
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "new_post.gohtml"), []byte(newPostTemplate2), 0644)
confirmDeleteTemplate := `{{define "content"}}Confirm Delete
{{end}}`
os.WriteFile(filepath.Join(templatesDir, "confirm_delete.gohtml"), []byte(confirmDeleteTemplate), 0644)
pageHandler, err := handlers.NewPageHandler(templatesDir, h.AuthService, suite.PostRepo, h.VoteService, suite.UserRepo, h.MetadataService, testutils.AppTestConfig)
if err != nil {
t.Fatalf("Failed to create page handler: %v", err)
}
router := server.NewRouter(buildRouterConfig(h, staticDir, pageHandler))
return &testContext{
Router: router,
Suite: suite,
AuthService: h.AuthService,
}
}
func getCSRFToken(t *testing.T, router http.Handler, path string, cookies ...*http.Cookie) string {
t.Helper()
req := httptest.NewRequest("GET", path, nil)
for _, cookie := range cookies {
req.AddCookie(cookie)
}
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
cookieList := rec.Result().Cookies()
for _, cookie := range cookieList {
if cookie.Name == "csrf_token" {
return cookie.Value
}
}
t.Fatal("CSRF token not found")
return ""
}
func assertJSONResponse(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) map[string]any {
t.Helper()
if rec.Code != expectedStatus {
t.Errorf("Expected status %d, got %d. Body: %s", expectedStatus, rec.Code, rec.Body.String())
return nil
}
var response map[string]any
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v. Body: %s", err, rec.Body.String())
return nil
}
return response
}
func assertErrorResponse(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) {
t.Helper()
if rec.Code != expectedStatus {
t.Errorf("Expected status %d, got %d. Body: %s", expectedStatus, rec.Code, rec.Body.String())
return
}
var response map[string]any
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode error response: %v. Body: %s", err, rec.Body.String())
return
}
if _, ok := response["error"]; !ok {
if _, ok := response["message"]; !ok {
t.Error("Expected error or message field in error response")
}
}
}
func assertStatus(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) {
t.Helper()
if rec.Code != expectedStatus {
t.Errorf("Expected status %d, got %d. Body: %s", expectedStatus, rec.Code, rec.Body.String())
}
}
func assertStatusRange(t *testing.T, rec *httptest.ResponseRecorder, minStatus, maxStatus int) {
t.Helper()
if rec.Code < minStatus || rec.Code > maxStatus {
t.Errorf("Expected status between %d and %d, got %d. Body: %s", minStatus, maxStatus, rec.Code, rec.Body.String())
}
}
func assertCookie(t *testing.T, rec *httptest.ResponseRecorder, name, expectedValue string) {
t.Helper()
cookies := rec.Result().Cookies()
for _, cookie := range cookies {
if cookie.Name == name {
if expectedValue != "" && cookie.Value != expectedValue {
t.Errorf("Expected cookie %s value %s, got %s", name, expectedValue, cookie.Value)
}
return
}
}
t.Errorf("Expected cookie %s not found", name)
}
func assertCookieCleared(t *testing.T, rec *httptest.ResponseRecorder, name string) {
t.Helper()
cookies := rec.Result().Cookies()
for _, cookie := range cookies {
if cookie.Name == name {
if cookie.Value != "" {
t.Errorf("Expected cookie %s to be cleared, got value %s", name, cookie.Value)
}
return
}
}
}
func assertHeader(t *testing.T, rec *httptest.ResponseRecorder, name, expectedValue string) {
t.Helper()
actualValue := rec.Header().Get(name)
if actualValue != expectedValue {
t.Errorf("Expected header %s=%s, got %s", name, expectedValue, actualValue)
}
}
func assertHeaderContains(t *testing.T, rec *httptest.ResponseRecorder, name, substring string) {
t.Helper()
actualValue := rec.Header().Get(name)
if !strings.Contains(actualValue, substring) {
t.Errorf("Expected header %s to contain %s, got %s", name, substring, actualValue)
}
}
type authenticatedUser struct {
User *database.User
Token string
}
func createAuthenticatedUser(t *testing.T, authService *services.AuthFacade, userRepo repositories.UserRepository, username, email string) *authenticatedUser {
t.Helper()
password := "SecurePass123!"
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
t.Fatalf("Failed to hash password: %v", err)
}
user := &database.User{
Username: username,
Email: email,
Password: string(hashedPassword),
EmailVerified: true,
}
if err := userRepo.Create(user); err != nil {
t.Fatalf("Failed to create authenticated user: %v", err)
}
loginResult, err := authService.Login(username, password)
if err != nil {
t.Fatalf("Failed to login authenticated user: %v", err)
}
return &authenticatedUser{
User: loginResult.User,
Token: loginResult.AccessToken,
}
}
func uniqueTestUsername(t *testing.T, prefix string) string {
return fmt.Sprintf("%s_%d_%d", prefix, time.Now().UnixNano(), len(t.Name()))
}
func uniqueTestEmail(t *testing.T, prefix string) string {
return fmt.Sprintf("%s_%d_%d@example.com", prefix, time.Now().UnixNano(), len(t.Name()))
}
func createUserWithCleanup(t *testing.T, ctx *testContext, username, email string) *authenticatedUser {
t.Helper()
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, username, email)
t.Cleanup(func() {
if err := ctx.Suite.UserRepo.Delete(user.User.ID); err != nil {
t.Logf("Failed to cleanup user %d: %v", user.User.ID, err)
}
})
return user
}