To gitea and beyond, let's go(-yco)
This commit is contained in:
358
internal/integration/helpers.go
Normal file
358
internal/integration/helpers.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"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
|
||||
}
|
||||
|
||||
func setupTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
middleware.StopAllRateLimiters()
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
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)
|
||||
apiHandler := handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService, suite.DB, middleware.NewInMemoryDBMonitor())
|
||||
|
||||
staticDir := t.TempDir()
|
||||
robotsFile := filepath.Join(staticDir, "robots.txt")
|
||||
os.WriteFile(robotsFile, []byte("User-agent: *\nDisallow: /"), 0644)
|
||||
|
||||
router := server.NewRouter(server.RouterConfig{
|
||||
AuthHandler: authHandler,
|
||||
PostHandler: postHandler,
|
||||
VoteHandler: voteHandler,
|
||||
UserHandler: userHandler,
|
||||
APIHandler: apiHandler,
|
||||
AuthService: authService,
|
||||
PageHandler: nil,
|
||||
StaticDir: staticDir,
|
||||
Debug: false,
|
||||
DisableCache: false,
|
||||
DisableCompression: false,
|
||||
DBMonitor: middleware.NewInMemoryDBMonitor(),
|
||||
RateLimitConfig: testutils.AppTestConfig.RateLimit,
|
||||
})
|
||||
|
||||
return &testContext{
|
||||
Router: router,
|
||||
Suite: suite,
|
||||
AuthService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
func setupPageHandlerTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
middleware.StopAllRateLimiters()
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
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)
|
||||
apiHandler := handlers.NewAPIHandler(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService)
|
||||
|
||||
staticDir := t.TempDir()
|
||||
templatesDir := t.TempDir()
|
||||
|
||||
baseTemplate := `{{define "layout"}}<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{block "content" .}}{{end}}
|
||||
</body>
|
||||
</html>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "base.gohtml"), []byte(baseTemplate), 0644)
|
||||
|
||||
os.MkdirAll(filepath.Join(templatesDir, "partials"), 0755)
|
||||
|
||||
homeTemplate := `{{define "content"}}<h1>Home</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "home.gohtml"), []byte(homeTemplate), 0644)
|
||||
|
||||
loginTemplate := `{{define "content"}}<h1>Login</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "login.gohtml"), []byte(loginTemplate), 0644)
|
||||
|
||||
registerTemplate := `{{define "content"}}<h1>Register</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "register.gohtml"), []byte(registerTemplate), 0644)
|
||||
|
||||
settingsTemplate := `{{define "content"}}<h1>Settings</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "settings.gohtml"), []byte(settingsTemplate), 0644)
|
||||
|
||||
postTemplate := `{{define "content"}}<h1>{{.Post.Title}}</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "post.gohtml"), []byte(postTemplate), 0644)
|
||||
|
||||
errorTemplate := `{{define "content"}}<h1>Error</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "error.gohtml"), []byte(errorTemplate), 0644)
|
||||
|
||||
confirmTemplate := `{{define "content"}}<h1>Confirm</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "confirm.gohtml"), []byte(confirmTemplate), 0644)
|
||||
|
||||
confirmEmailTemplate := `{{define "content"}}<h1>Confirm Email</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "confirm_email.gohtml"), []byte(confirmEmailTemplate), 0644)
|
||||
|
||||
resendTemplate := `{{define "content"}}<h1>Resend</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "resend-verification.gohtml"), []byte(resendTemplate), 0644)
|
||||
|
||||
resendVerificationTemplate := `{{define "content"}}<h1>Resend Verification</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "resend_verification.gohtml"), []byte(resendVerificationTemplate), 0644)
|
||||
|
||||
forgotTemplate := `{{define "content"}}<h1>Forgot Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "forgot-password.gohtml"), []byte(forgotTemplate), 0644)
|
||||
|
||||
forgotPasswordTemplate := `{{define "content"}}<h1>Forgot Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "forgot_password.gohtml"), []byte(forgotPasswordTemplate), 0644)
|
||||
|
||||
resetTemplate := `{{define "content"}}<h1>Reset Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "reset-password.gohtml"), []byte(resetTemplate), 0644)
|
||||
|
||||
resetPasswordTemplate := `{{define "content"}}<h1>Reset Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "reset_password.gohtml"), []byte(resetPasswordTemplate), 0644)
|
||||
|
||||
searchTemplate := `{{define "content"}}<h1>Search</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "search.gohtml"), []byte(searchTemplate), 0644)
|
||||
|
||||
newPostTemplate := `{{define "content"}}<h1>New Post</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "new-post.gohtml"), []byte(newPostTemplate), 0644)
|
||||
|
||||
newPostTemplate2 := `{{define "content"}}<h1>New Post</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "new_post.gohtml"), []byte(newPostTemplate2), 0644)
|
||||
|
||||
confirmDeleteTemplate := `{{define "content"}}<h1>Confirm Delete</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "confirm_delete.gohtml"), []byte(confirmDeleteTemplate), 0644)
|
||||
|
||||
pageHandler, err := handlers.NewPageHandler(templatesDir, authService, suite.PostRepo, voteService, suite.UserRepo, metadataService, testutils.AppTestConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create page handler: %v", err)
|
||||
}
|
||||
|
||||
router := server.NewRouter(server.RouterConfig{
|
||||
AuthHandler: authHandler,
|
||||
PostHandler: postHandler,
|
||||
VoteHandler: voteHandler,
|
||||
UserHandler: userHandler,
|
||||
APIHandler: apiHandler,
|
||||
AuthService: authService,
|
||||
PageHandler: pageHandler,
|
||||
StaticDir: staticDir,
|
||||
Debug: false,
|
||||
DisableCache: false,
|
||||
DisableCompression: false,
|
||||
DBMonitor: middleware.NewInMemoryDBMonitor(),
|
||||
RateLimitConfig: testutils.AppTestConfig.RateLimit,
|
||||
})
|
||||
|
||||
return &testContext{
|
||||
Router: router,
|
||||
Suite: suite,
|
||||
AuthService: 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
|
||||
}
|
||||
Reference in New Issue
Block a user