add tests
This commit is contained in:
398
tests/test_helper.go
Normal file
398
tests/test_helper.go
Normal file
@@ -0,0 +1,398 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"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")
|
||||
// Set test-specific environment variables
|
||||
os.Setenv("TESTING_ENV", "true") // Used to bypass
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user