To gitea and beyond, let's go(-yco)
This commit is contained in:
350
internal/testutils/testutils.go
Normal file
350
internal/testutils/testutils.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"goyco/internal/config"
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/repositories"
|
||||
)
|
||||
|
||||
var AppTestConfig = &config.Config{
|
||||
JWT: config.JWTConfig{
|
||||
Secret: "test-secret-key-for-testing-purposes-only",
|
||||
Expiration: 24,
|
||||
RefreshExpiration: 168,
|
||||
Issuer: "goyco",
|
||||
Audience: "goyco-users",
|
||||
},
|
||||
App: config.AppConfig{
|
||||
BaseURL: "http://localhost:8080",
|
||||
BcryptCost: 10,
|
||||
},
|
||||
RateLimit: config.RateLimitConfig{
|
||||
AuthLimit: 5,
|
||||
GeneralLimit: 100,
|
||||
HealthLimit: 60,
|
||||
MetricsLimit: 10,
|
||||
TrustProxyHeaders: false,
|
||||
},
|
||||
}
|
||||
|
||||
func NewTestConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Database: config.DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: "5432",
|
||||
User: "test",
|
||||
Password: "test",
|
||||
Name: "test_db",
|
||||
SSLMode: "disable",
|
||||
},
|
||||
Server: config.ServerConfig{
|
||||
Host: "localhost",
|
||||
Port: "8080",
|
||||
},
|
||||
JWT: config.JWTConfig{
|
||||
Secret: "test-jwt-secret-key-that-is-long-enough",
|
||||
Expiration: 24,
|
||||
RefreshExpiration: 168,
|
||||
Issuer: "goyco",
|
||||
Audience: "goyco-users",
|
||||
},
|
||||
SMTP: config.SMTPConfig{
|
||||
Host: "localhost",
|
||||
Port: 587,
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
From: "test@example.com",
|
||||
},
|
||||
App: config.AppConfig{
|
||||
Debug: true,
|
||||
BaseURL: "http://localhost:8080",
|
||||
},
|
||||
RateLimit: config.RateLimitConfig{
|
||||
AuthLimit: 5,
|
||||
GeneralLimit: 100,
|
||||
HealthLimit: 60,
|
||||
MetricsLimit: 10,
|
||||
TrustProxyHeaders: false,
|
||||
},
|
||||
LogDir: "/tmp/goyco-test-logs",
|
||||
PIDDir: "/tmp/goyco-test-pids",
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeTestName(name string) string {
|
||||
|
||||
replacer := strings.NewReplacer(
|
||||
"/", "_",
|
||||
"#", "_",
|
||||
"\\", "_",
|
||||
"?", "_",
|
||||
"&", "_",
|
||||
"=", "_",
|
||||
" ", "_",
|
||||
)
|
||||
return replacer.Replace(name)
|
||||
}
|
||||
|
||||
func NewTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
sanitizedName := sanitizeTestName(t.Name())
|
||||
dbName := "file:memdb_" + sanitizedName + "?mode=memory&cache=shared&_journal_mode=WAL&_synchronous=NORMAL"
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to test database: %v", err)
|
||||
}
|
||||
err = db.AutoMigrate(
|
||||
&database.User{},
|
||||
&database.Post{},
|
||||
&database.Vote{},
|
||||
&database.AccountDeletionRequest{},
|
||||
&database.RefreshToken{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to migrate database: %v", err)
|
||||
}
|
||||
|
||||
if execErr := db.Exec("PRAGMA busy_timeout = 5000").Error; execErr != nil {
|
||||
t.Fatalf("Failed to configure busy timeout: %v", execErr)
|
||||
}
|
||||
if execErr := db.Exec("PRAGMA foreign_keys = ON").Error; execErr != nil {
|
||||
t.Fatalf("Failed to enable foreign keys: %v", execErr)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to access SQL DB: %v", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
||||
return db
|
||||
}
|
||||
|
||||
type HTTPTestHelpers struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewHTTPTestHelpers(t *testing.T) *HTTPTestHelpers {
|
||||
return &HTTPTestHelpers{t: t}
|
||||
}
|
||||
|
||||
func (h *HTTPTestHelpers) POST(url string, body any) *http.Request {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
h.t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (h *HTTPTestHelpers) GET(url string) *http.Request {
|
||||
return httptest.NewRequest("GET", url, nil)
|
||||
}
|
||||
|
||||
func (h *HTTPTestHelpers) PUT(url string, body any) *http.Request {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
h.t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest("PUT", url, bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (h *HTTPTestHelpers) DELETE(url string) *http.Request {
|
||||
return httptest.NewRequest("DELETE", url, nil)
|
||||
}
|
||||
|
||||
func WithURLParams(req *http.Request, params map[string]string) *http.Request {
|
||||
routeContext := chi.NewRouteContext()
|
||||
for key, value := range params {
|
||||
routeContext.URLParams.Add(key, value)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, routeContext)
|
||||
return req.WithContext(ctx)
|
||||
}
|
||||
|
||||
func WithUserContext(req *http.Request, key any, userID uint) *http.Request {
|
||||
ctx := context.WithValue(req.Context(), key, userID)
|
||||
return req.WithContext(ctx)
|
||||
}
|
||||
|
||||
func CreateTestUser(t *testing.T, db *gorm.DB) *database.User {
|
||||
t.Helper()
|
||||
user := &database.User{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Password: "hashedpassword123",
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := db.Create(user).Error; err != nil {
|
||||
t.Fatalf("Failed to create test user: %v", err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func CreateTestPost(t *testing.T, db *gorm.DB, authorID uint) *database.Post {
|
||||
t.Helper()
|
||||
post := &database.Post{
|
||||
Title: "Test Post",
|
||||
URL: "https://example.com/test",
|
||||
Content: "Test content",
|
||||
AuthorID: &authorID,
|
||||
}
|
||||
if err := db.Create(post).Error; err != nil {
|
||||
t.Fatalf("Failed to create test post: %v", err)
|
||||
}
|
||||
return post
|
||||
}
|
||||
|
||||
func CreateTestPIDFile(t *testing.T, pid int) string {
|
||||
dir := t.TempDir()
|
||||
pidFile := filepath.Join(dir, "goyco.pid")
|
||||
|
||||
err := os.WriteFile(pidFile, []byte(strconv.Itoa(pid)), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test PID file: %v", err)
|
||||
}
|
||||
|
||||
return pidFile
|
||||
}
|
||||
|
||||
type HandlerTestHelper struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewHandlerTestHelper(t *testing.T) *HandlerTestHelper {
|
||||
return &HandlerTestHelper{t: t}
|
||||
}
|
||||
|
||||
func (h *HandlerTestHelper) AssertResponseSuccess(t *testing.T, response map[string]any) {
|
||||
if success, ok := response["success"].(bool); !ok || !success {
|
||||
t.Fatalf("Expected success=true, got %v", response["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HandlerTestHelper) AssertResponseError(t *testing.T, response map[string]any) {
|
||||
if success, ok := response["success"].(bool); !ok || success {
|
||||
t.Fatalf("Expected success=false, got %v", response["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HandlerTestHelper) AssertStatusCode(t *testing.T, recorder *httptest.ResponseRecorder, expected int) {
|
||||
if recorder.Result().StatusCode != expected {
|
||||
t.Fatalf("Expected status %d, got %d", expected, recorder.Result().StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HandlerTestHelper) CreateTestRequestWithUser(method, url string, body any, userID uint) *http.Request {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
h.t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
req = httptest.NewRequest(method, url, bytes.NewBuffer(jsonBody))
|
||||
} else {
|
||||
req = httptest.NewRequest(method, url, nil)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return WithUserContext(req, middleware.UserIDKey, userID)
|
||||
}
|
||||
|
||||
func (h *HandlerTestHelper) CreateTestRequest(method, url string, body any) *http.Request {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
h.t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
req = httptest.NewRequest(method, url, bytes.NewBuffer(jsonBody))
|
||||
} else {
|
||||
req = httptest.NewRequest(method, url, nil)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (h *HandlerTestHelper) DecodeResponse(t *testing.T, recorder *httptest.ResponseRecorder) map[string]any {
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
type ServiceSuite struct {
|
||||
DB *gorm.DB
|
||||
UserRepo repositories.UserRepository
|
||||
PostRepo repositories.PostRepository
|
||||
VoteRepo repositories.VoteRepository
|
||||
DeletionRepo repositories.AccountDeletionRepository
|
||||
RefreshTokenRepo *repositories.RefreshTokenRepository
|
||||
EmailSender *MockEmailSender
|
||||
TitleFetcher *MockTitleFetcher
|
||||
}
|
||||
|
||||
func NewServiceSuite(t *testing.T) *ServiceSuite {
|
||||
t.Helper()
|
||||
db := NewTestDB(t)
|
||||
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
postRepo := repositories.NewPostRepository(db)
|
||||
voteRepo := repositories.NewVoteRepository(db)
|
||||
deletionRepo := repositories.NewAccountDeletionRepository(db)
|
||||
refreshTokenRepo := repositories.NewRefreshTokenRepository(db)
|
||||
|
||||
emailSender := &MockEmailSender{}
|
||||
titleFetcher := &MockTitleFetcher{}
|
||||
|
||||
t.Cleanup(func() {
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
})
|
||||
|
||||
return &ServiceSuite{
|
||||
DB: db,
|
||||
UserRepo: userRepo,
|
||||
PostRepo: postRepo,
|
||||
VoteRepo: voteRepo,
|
||||
DeletionRepo: deletionRepo,
|
||||
RefreshTokenRepo: refreshTokenRepo,
|
||||
EmailSender: emailSender,
|
||||
TitleFetcher: titleFetcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) Cleanup() {
|
||||
}
|
||||
|
||||
func HashPassword(password string) string {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to hash password: %v", err))
|
||||
}
|
||||
return string(hashed)
|
||||
}
|
||||
Reference in New Issue
Block a user