Files
acc-server-manager/tests/test_helper.go
Fran Jurmanović 60175f8052
Some checks failed
Release and Deploy / build (push) Failing after 2m11s
Release and Deploy / deploy (push) Has been skipped
2fa for polling and security
2025-08-16 16:21:39 +02:00

403 lines
11 KiB
Go

package tests
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/configs"
"bytes"
"context"
"errors"
"io"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/joho/godotenv"
"github.com/valyala/fasthttp"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// TestHelper provides utilities for testing
type TestHelper struct {
DB *gorm.DB
TempDir string
TestData *TestData
}
// TestData contains common test data structures
type TestData struct {
ServerID uuid.UUID
Server *model.Server
ConfigFiles map[string]string
SampleConfig *model.Configuration
}
// SetTestEnv sets the required environment variables for tests
func SetTestEnv() {
// Set required environment variables for testing
os.Setenv("APP_SECRET", "test-secret-key-for-testing-123456")
os.Setenv("APP_SECRET_CODE", "test-code-for-testing-123456789012")
os.Setenv("ENCRYPTION_KEY", "12345678901234567890123456789012")
os.Setenv("JWT_SECRET", "test-jwt-secret-key-for-testing-123456789012345678901234567890")
os.Setenv("ACCESS_KEY", "test-access-key-for-testing")
// Set test-specific environment variables
os.Setenv("TESTING_ENV", "true") // Used to bypass
configs.Init()
}
// NewTestHelper creates a new test helper with in-memory database
func NewTestHelper(t *testing.T) *TestHelper {
// Set required environment variables
SetTestEnv()
// Create temporary directory for test files
tempDir := t.TempDir()
// Create in-memory SQLite database for testing
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent), // Suppress SQL logs in tests
})
if err != nil {
t.Fatalf("Failed to connect to test database: %v", err)
}
// Auto-migrate the schema
err = db.AutoMigrate(
&model.Server{},
&model.Config{},
&model.User{},
&model.Role{},
&model.Permission{},
&model.StateHistory{},
)
// Explicitly ensure tables exist with correct structure
if !db.Migrator().HasTable(&model.StateHistory{}) {
err = db.Migrator().CreateTable(&model.StateHistory{})
if err != nil {
t.Fatalf("Failed to create state_histories table: %v", err)
}
}
if err != nil {
t.Fatalf("Failed to migrate test database: %v", err)
}
// Create test data
testData := createTestData(t, tempDir)
return &TestHelper{
DB: db,
TempDir: tempDir,
TestData: testData,
}
}
// createTestData creates common test data structures
func createTestData(t *testing.T, tempDir string) *TestData {
serverID := uuid.New()
// Create sample server
server := &model.Server{
ID: serverID,
Name: "Test Server",
Path: filepath.Join(tempDir, "server"),
ServiceName: "ACC-Server-Test",
Status: model.StatusStopped,
DateCreated: time.Now(),
FromSteamCMD: false,
}
// Create server directory
serverConfigDir := filepath.Join(tempDir, "server", "cfg")
if err := os.MkdirAll(serverConfigDir, 0755); err != nil {
t.Fatalf("Failed to create server config directory: %v", err)
}
// Sample configuration files content
configFiles := map[string]string{
"configuration.json": `{
"udpPort": "9231",
"tcpPort": "9232",
"maxConnections": "30",
"lanDiscovery": "1",
"registerToLobby": "1",
"configVersion": "1"
}`,
"settings.json": `{
"serverName": "Test ACC Server",
"adminPassword": "admin123",
"carGroup": "GT3",
"trackMedalsRequirement": "0",
"safetyRatingRequirement": "30",
"racecraftRatingRequirement": "30",
"password": "",
"spectatorPassword": "",
"maxCarSlots": "30",
"dumpLeaderboards": "1",
"isRaceLocked": "0",
"randomizeTrackWhenEmpty": "0",
"centralEntryListPath": "",
"allowAutoDQ": "1",
"shortFormationLap": "0",
"formationLapType": "3",
"ignorePrematureDisconnects": "1"
}`,
"event.json": `{
"track": "spa",
"preRaceWaitingTimeSeconds": "80",
"sessionOverTimeSeconds": "120",
"ambientTemp": "26",
"cloudLevel": 0.3,
"rain": 0.0,
"weatherRandomness": "1",
"postQualySeconds": "10",
"postRaceSeconds": "30",
"simracerWeatherConditions": "0",
"isFixedConditionQualification": "0",
"sessions": [
{
"hourOfDay": "10",
"dayOfWeekend": "1",
"timeMultiplier": "1",
"sessionType": "P",
"sessionDurationMinutes": "10"
},
{
"hourOfDay": "12",
"dayOfWeekend": "1",
"timeMultiplier": "1",
"sessionType": "Q",
"sessionDurationMinutes": "10"
},
{
"hourOfDay": "14",
"dayOfWeekend": "1",
"timeMultiplier": "1",
"sessionType": "R",
"sessionDurationMinutes": "25"
}
]
}`,
"assistRules.json": `{
"stabilityControlLevelMax": "0",
"disableAutosteer": "1",
"disableAutoLights": "0",
"disableAutoWiper": "0",
"disableAutoEngineStart": "0",
"disableAutoPitLimiter": "0",
"disableAutoGear": "0",
"disableAutoClutch": "0",
"disableIdealLine": "0"
}`,
"eventRules.json": `{
"qualifyStandingType": "1",
"pitWindowLengthSec": "600",
"driverStIntStringTimeSec": "300",
"mandatoryPitstopCount": "0",
"maxTotalDrivingTime": "0",
"isRefuellingAllowedInRace": 0,
"isRefuellingTimeFixed": 0,
"isMandatoryPitstopRefuellingRequired": 0,
"isMandatoryPitstopTyreChangeRequired": 0,
"isMandatoryPitstopSwapDriverRequired": 0,
"tyreSetCount": "0"
}`,
}
// Sample configuration struct
sampleConfig := &model.Configuration{
UdpPort: model.IntString(9231),
TcpPort: model.IntString(9232),
MaxConnections: model.IntString(30),
LanDiscovery: model.IntString(1),
RegisterToLobby: model.IntString(1),
ConfigVersion: model.IntString(1),
}
return &TestData{
ServerID: serverID,
Server: server,
ConfigFiles: configFiles,
SampleConfig: sampleConfig,
}
}
// CreateTestConfigFiles creates actual config files in the test directory
func (th *TestHelper) CreateTestConfigFiles() error {
serverConfigDir := filepath.Join(th.TestData.Server.Path, "cfg")
for filename, content := range th.TestData.ConfigFiles {
filePath := filepath.Join(serverConfigDir, filename)
// Encode content to UTF-16 LE BOM format as expected by the application
utf16Content, err := EncodeUTF16LEBOM([]byte(content))
if err != nil {
return err
}
if err := os.WriteFile(filePath, utf16Content, 0644); err != nil {
return err
}
}
return nil
}
// CreateMalformedConfigFile creates a config file with invalid JSON
func (th *TestHelper) CreateMalformedConfigFile(filename string) error {
serverConfigDir := filepath.Join(th.TestData.Server.Path, "cfg")
filePath := filepath.Join(serverConfigDir, filename)
malformedJSON := `{
"udpPort": "9231",
"tcpPort": "9232"
"maxConnections": "30" // Missing comma - invalid JSON
}`
return os.WriteFile(filePath, []byte(malformedJSON), 0644)
}
// RemoveConfigFile removes a config file to simulate missing file scenarios
func (th *TestHelper) RemoveConfigFile(filename string) error {
serverConfigDir := filepath.Join(th.TestData.Server.Path, "cfg")
filePath := filepath.Join(serverConfigDir, filename)
return os.Remove(filePath)
}
// InsertTestServer inserts the test server into the database
func (th *TestHelper) InsertTestServer() error {
return th.DB.Create(th.TestData.Server).Error
}
// CreateContext creates a test context
func (th *TestHelper) CreateContext() context.Context {
return context.Background()
}
// CreateFiberCtx creates a fiber.Ctx for testing
func (th *TestHelper) CreateFiberCtx() *fiber.Ctx {
// Create app and request for fiber context
app := fiber.New()
// Create a dummy request that doesn't depend on external http objects
req := httptest.NewRequest("GET", "/", nil)
// Create the fiber context from real request/response
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
// Store the original request for release later
ctx.Locals("original-request", req)
// Return the context which can be safely used in tests
return ctx
}
// ReleaseFiberCtx properly releases a fiber context created with CreateFiberCtx
func (th *TestHelper) ReleaseFiberCtx(app *fiber.App, ctx *fiber.Ctx) {
if app != nil && ctx != nil {
app.ReleaseCtx(ctx)
}
}
// Cleanup performs cleanup operations after tests
func (th *TestHelper) Cleanup() {
// Close database connection
if sqlDB, err := th.DB.DB(); err == nil {
sqlDB.Close()
}
// Temporary directory is automatically cleaned up by t.TempDir()
}
// LoadTestEnvFile loads environment variables from a .env file for testing
func LoadTestEnvFile() error {
// Try to load from .env file
return godotenv.Load()
}
// AssertNoError is a helper function to check for errors in tests
func AssertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
}
// AssertError is a helper function to check for expected errors
func AssertError(t *testing.T, err error, expectedMsg string) {
t.Helper()
if err == nil {
t.Fatalf("Expected error containing '%s', got no error", expectedMsg)
}
if expectedMsg != "" && err.Error() != expectedMsg {
t.Fatalf("Expected error '%s', got '%s'", expectedMsg, err.Error())
}
}
// AssertEqual checks if two values are equal
func AssertEqual(t *testing.T, expected, actual interface{}) {
t.Helper()
if expected != actual {
t.Fatalf("Expected %v, got %v", expected, actual)
}
}
// AssertNotNil checks if a value is not nil
func AssertNotNil(t *testing.T, value interface{}) {
t.Helper()
if value == nil {
t.Fatalf("Expected non-nil value, got nil")
}
}
// AssertNil checks if a value is nil
func AssertNil(t *testing.T, value interface{}) {
t.Helper()
if value != nil {
// Special handling for interface values that contain nil but aren't nil themselves
// For example, (*jwt.Claims)(nil) is not equal to nil, but it contains nil
switch v := value.(type) {
case *interface{}:
if v == nil || *v == nil {
return
}
case interface{}:
if v == nil {
return
}
}
t.Fatalf("Expected nil value, got %v", value)
}
}
// EncodeUTF16LEBOM encodes UTF-8 bytes to UTF-16 LE BOM format
func EncodeUTF16LEBOM(input []byte) ([]byte, error) {
encoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM)
return transformBytes(encoder.NewEncoder(), input)
}
// transformBytes applies a transform to input bytes
func transformBytes(t transform.Transformer, input []byte) ([]byte, error) {
var buf bytes.Buffer
w := transform.NewWriter(&buf, t)
if _, err := io.Copy(w, bytes.NewReader(input)); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ErrorForTesting creates an error for testing purposes
func ErrorForTesting(message string) error {
return errors.New(message)
}