add server api get and update service control endpoints

This commit is contained in:
Fran Jurmanović
2025-07-29 20:50:44 +02:00
parent 44acb170a7
commit 647f4f7487
27 changed files with 424 additions and 2025 deletions

View File

@@ -2,7 +2,9 @@ package main
import ( import (
"acc-server-manager/local/utl/cache" "acc-server-manager/local/utl/cache"
"acc-server-manager/local/utl/configs"
"acc-server-manager/local/utl/db" "acc-server-manager/local/utl/db"
"acc-server-manager/local/utl/jwt"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
"acc-server-manager/local/utl/server" "acc-server-manager/local/utl/server"
"fmt" "fmt"
@@ -10,10 +12,12 @@ import (
"go.uber.org/dig" "go.uber.org/dig"
_ "acc-server-manager/docs" _ "acc-server-manager/swagger"
) )
func main() { func main() {
configs.Init()
jwt.Init()
// Initialize new logging system // Initialize new logging system
if err := logging.InitializeLogging(); err != nil { if err := logging.InitializeLogging(); err != nil {
fmt.Printf("Failed to initialize logging system: %v\n", err) fmt.Printf("Failed to initialize logging system: %v\n", err)

27
cmd/api/swagger.go Normal file
View File

@@ -0,0 +1,27 @@
// Package main ACC Server Manager API
//
// @title ACC Server Manager API
// @version 1.0
// @description API for managing Assetto Corsa Competizione dedicated servers
//
// @contact.name ACC Server Manager Support
// @contact.url https://github.com/yourusername/acc-server-manager
//
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
//
// @host localhost:3000
// @BasePath /api/v1
// @schemes http https
//
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
//
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
package main
// This file exists solely for Swagger documentation generation.
// Run: swag init -g cmd/api/swagger.go -o docs/

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"acc-server-manager/local/controller" "acc-server-manager/local/controller"
"acc-server-manager/local/middleware"
"acc-server-manager/local/utl/common" "acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/configs" "acc-server-manager/local/utl/configs"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
@@ -29,8 +30,12 @@ func Init(di *dig.Container, app *fiber.App) {
Lookup: groups.Group("/lookup"), Lookup: groups.Group("/lookup"),
StateHistory: serverIdGroup.Group("/state-history"), StateHistory: serverIdGroup.Group("/state-history"),
Membership: groups.Group("/membership"), Membership: groups.Group("/membership"),
System: groups.Group("/system"),
} }
accessKeyMiddleware := middleware.NewAccessKeyMiddleware()
routeGroups.Api.Use(accessKeyMiddleware.Authenticate)
err := di.Provide(func() *common.RouteGroups { err := di.Provide(func() *common.RouteGroups {
return routeGroups return routeGroups
}) })

View File

@@ -1,149 +0,0 @@
package controller
import (
"acc-server-manager/local/middleware"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type ApiController struct {
service *service.ApiService
errorHandler *error_handler.ControllerErrorHandler
}
// NewApiController
// Initializes ApiController.
//
// Args:
// *services.ApiService: API service
// *Fiber.RouterGroup: Fiber Router Group
// Returns:
// *ApiController: Controller for "api" interactions
func NewApiController(as *service.ApiService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ApiController {
ac := &ApiController{
service: as,
errorHandler: error_handler.NewControllerErrorHandler(),
}
apiGroup := routeGroups.Api
apiGroup.Use(auth.Authenticate)
apiGroup.Get("/", ac.getFirst)
apiGroup.Get("/:service", ac.getStatus)
apiGroup.Post("/start", ac.startServer)
apiGroup.Post("/stop", ac.stopServer)
apiGroup.Post("/restart", ac.restartServer)
return ac
}
// getFirst returns API
//
// @Summary Return API
// @Description Return API
// @Tags api
// @Success 200 {array} string
// @Router /v1/api [get]
func (ac *ApiController) getFirst(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
// getStatus returns service status
//
// @Summary Return service status
// @Description Returns service status
// @Param service path string true "required"
// @Tags api
// @Success 200 {array} string
// @Router /v1/api/{service} [get]
func (ac *ApiController) getStatus(c *fiber.Ctx) error {
service := c.Params("service")
if service == "" {
serverId := c.Params("service")
if _, err := uuid.Parse(serverId); err != nil {
return ac.errorHandler.HandleUUIDError(c, "server ID")
}
c.Locals("serverId", serverId)
} else {
c.Locals("service", service)
}
apiModel, err := ac.service.GetStatus(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(string(apiModel))
}
// startServer starts service
//
// @Summary Start service
// @Description Starts service
// @Param name body string true "required"
// @Tags api
// @Success 200 {array} string
// @Router /v1/api/start [post]
func (ac *ApiController) startServer(c *fiber.Ctx) error {
model := new(Service)
if err := c.BodyParser(model); err != nil {
return ac.errorHandler.HandleParsingError(c, err)
}
c.Locals("service", model.Name)
c.Locals("serverId", model.ServerId)
apiModel, err := ac.service.ApiStartServer(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
// stopServer stops service
//
// @Summary Stop service
// @Description Stops service
// @Param name body string true "required"
// @Tags api
// @Success 200 {array} string
// @Router /v1/api/stop [post]
func (ac *ApiController) stopServer(c *fiber.Ctx) error {
model := new(Service)
if err := c.BodyParser(model); err != nil {
return ac.errorHandler.HandleParsingError(c, err)
}
c.Locals("service", model.Name)
c.Locals("serverId", model.ServerId)
apiModel, err := ac.service.ApiStopServer(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
// restartServer returns API
//
// @Summary Restart service
// @Description Restarts service
// @Param name body string true "required"
// @Tags api
// @Success 200 {array} string
// @Router /v1/api/restart [post]
func (ac *ApiController) restartServer(c *fiber.Ctx) error {
model := new(Service)
if err := c.BodyParser(model); err != nil {
return ac.errorHandler.HandleParsingError(c, err)
}
c.Locals("service", model.Name)
c.Locals("serverId", model.ServerId)
apiModel, err := ac.service.ApiRestartServer(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
type Service struct {
Name string `json:"name" xml:"name" form:"name"`
ServerId string `json:"serverId" xml:"serverId" form:"serverId"`
}

View File

@@ -31,10 +31,28 @@ func NewServerController(ss *service.ServerService, routeGroups *common.RouteGro
serverRoutes.Post("/", auth.HasPermission(model.ServerCreate), ac.CreateServer) serverRoutes.Post("/", auth.HasPermission(model.ServerCreate), ac.CreateServer)
serverRoutes.Put("/:id", auth.HasPermission(model.ServerUpdate), ac.UpdateServer) serverRoutes.Put("/:id", auth.HasPermission(model.ServerUpdate), ac.UpdateServer)
serverRoutes.Delete("/:id", auth.HasPermission(model.ServerDelete), ac.DeleteServer) serverRoutes.Delete("/:id", auth.HasPermission(model.ServerDelete), ac.DeleteServer)
apiServerRoutes := routeGroups.Api.Group("/server")
apiServerRoutes.Get("/", auth.HasPermission(model.ServerView), ac.GetAllApi)
return ac return ac
} }
// GetAll returns Servers // GetAll returns Servers
func (ac *ServerController) GetAllApi(c *fiber.Ctx) error {
var filter model.ServerFilter
if err := common.ParseQueryFilter(c, &filter); err != nil {
return ac.errorHandler.HandleValidationError(c, err, "query_filter")
}
ServerModel, err := ac.service.GetAll(c, &filter)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
var apiServers []model.ServerAPI
for _, server := range *ServerModel {
apiServers = append(apiServers, *server.ToServerAPI())
}
return c.JSON(apiServers)
}
func (ac *ServerController) GetAll(c *fiber.Ctx) error { func (ac *ServerController) GetAll(c *fiber.Ctx) error {
var filter model.ServerFilter var filter model.ServerFilter
if err := common.ParseQueryFilter(c, &filter); err != nil { if err := common.ParseQueryFilter(c, &filter); err != nil {

View File

@@ -0,0 +1,148 @@
package controller
import (
"acc-server-manager/local/middleware"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
)
type ServiceControlController struct {
service *service.ServiceControlService
errorHandler *error_handler.ControllerErrorHandler
}
// NewServiceControlController
// Initializes ServiceControlController.
//
// Args:
// *services.ServiceControlService: Service control service
// *Fiber.RouterGroup: Fiber Router Group
// Returns:
// *ServiceControlController: Controller for service control interactions
func NewServiceControlController(as *service.ServiceControlService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ServiceControlController {
ac := &ServiceControlController{
service: as,
errorHandler: error_handler.NewControllerErrorHandler(),
}
serviceRoutes := routeGroups.Server.Group("/service")
serviceRoutes.Get("/:service", ac.getStatus)
serviceRoutes.Post("/start", ac.startServer)
serviceRoutes.Post("/stop", ac.stopServer)
serviceRoutes.Post("/restart", ac.restartServer)
return ac
}
// getStatus returns service status
//
// @Summary Get service status
// @Description Get the current status of a Windows service
// @Tags Service Control
// @Accept json
// @Produce json
// @Param service path string true "Service name"
// @Success 200 {object} object{status=string,state=string} "Service status information"
// @Failure 400 {object} error_handler.ErrorResponse "Invalid service name"
// @Failure 401 {object} error_handler.ErrorResponse "Unauthorized"
// @Failure 404 {object} error_handler.ErrorResponse "Service not found"
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
// @Security BearerAuth
// @Router /v1/service-control/{service} [get]
func (ac *ServiceControlController) getStatus(c *fiber.Ctx) error {
id := c.Params("id")
c.Locals("serverId", id)
apiModel, err := ac.service.GetStatus(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(string(apiModel))
}
// startServer starts service
//
// @Summary Start a Windows service
// @Description Start a stopped Windows service for an ACC server
// @Tags Service Control
// @Accept json
// @Produce json
// @Param service body object{name=string} true "Service name to start"
// @Success 200 {object} object{message=string} "Service started successfully"
// @Failure 400 {object} error_handler.ErrorResponse "Invalid request body"
// @Failure 401 {object} error_handler.ErrorResponse "Unauthorized"
// @Failure 403 {object} error_handler.ErrorResponse "Insufficient permissions"
// @Failure 404 {object} error_handler.ErrorResponse "Service not found"
// @Failure 409 {object} error_handler.ErrorResponse "Service already running"
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
// @Security BearerAuth
// @Router /v1/service-control/start [post]
func (ac *ServiceControlController) startServer(c *fiber.Ctx) error {
id := c.Params("id")
c.Locals("serverId", id)
apiModel, err := ac.service.ServiceControlStartServer(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
// stopServer stops service
//
// @Summary Stop a Windows service
// @Description Stop a running Windows service for an ACC server
// @Tags Service Control
// @Accept json
// @Produce json
// @Param service body object{name=string} true "Service name to stop"
// @Success 200 {object} object{message=string} "Service stopped successfully"
// @Failure 400 {object} error_handler.ErrorResponse "Invalid request body"
// @Failure 401 {object} error_handler.ErrorResponse "Unauthorized"
// @Failure 403 {object} error_handler.ErrorResponse "Insufficient permissions"
// @Failure 404 {object} error_handler.ErrorResponse "Service not found"
// @Failure 409 {object} error_handler.ErrorResponse "Service already stopped"
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
// @Security BearerAuth
// @Router /v1/service-control/stop [post]
func (ac *ServiceControlController) stopServer(c *fiber.Ctx) error {
id := c.Params("id")
c.Locals("serverId", id)
apiModel, err := ac.service.ServiceControlStopServer(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
// restartServer restarts service
//
// @Summary Restart a Windows service
// @Description Stop and start a Windows service for an ACC server
// @Tags Service Control
// @Accept json
// @Produce json
// @Param service body object{name=string} true "Service name to restart"
// @Success 200 {object} object{message=string} "Service restarted successfully"
// @Failure 400 {object} error_handler.ErrorResponse "Invalid request body"
// @Failure 401 {object} error_handler.ErrorResponse "Unauthorized"
// @Failure 403 {object} error_handler.ErrorResponse "Insufficient permissions"
// @Failure 404 {object} error_handler.ErrorResponse "Service not found"
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
// @Security BearerAuth
// @Router /v1/service-control/restart [post]
func (ac *ServiceControlController) restartServer(c *fiber.Ctx) error {
id := c.Params("id")
c.Locals("serverId", id)
apiModel, err := ac.service.ServiceControlRestartServer(c)
if err != nil {
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
type Service struct {
Name string `json:"name" xml:"name" form:"name"`
ServerId string `json:"serverId" xml:"serverId" form:"serverId"`
}

View File

@@ -0,0 +1,38 @@
package controller
import (
"acc-server-manager/local/utl/common"
"github.com/gofiber/fiber/v2"
)
type SystemController struct {
}
// NewSystemController
// Initializes SystemController.
//
// Args:
// *services.SystemService: Service control service
// *Fiber.RouterGroup: Fiber Router Group
// Returns:
// *SystemController: Controller for service control interactions
func NewSystemController(routeGroups *common.RouteGroups) *SystemController {
ac := &SystemController{}
apiGroup := routeGroups.System
apiGroup.Get("/health", ac.getFirst)
return ac
}
// getFirst returns service control status
//
// @Summary Return service control status
// @Description Return service control status
// @Tags service-control
// @Success 200 {array} string
// @Router /v1/service-control [get]
func (ac *SystemController) getFirst(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -0,0 +1,59 @@
package middleware
import (
"acc-server-manager/local/utl/configs"
"acc-server-manager/local/utl/logging"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// AccessKeyMiddleware provides authentication and permission middleware.
type AccessKeyMiddleware struct {
userInfo CachedUserInfo
}
// NewAccessKeyMiddleware creates a new AccessKeyMiddleware.
func NewAccessKeyMiddleware() *AccessKeyMiddleware {
auth := &AccessKeyMiddleware{
userInfo: CachedUserInfo{UserID: uuid.New().String(), Username: "access_key", RoleName: "Admin", Permissions: make(map[string]bool), CachedAt: time.Now()},
}
return auth
}
// Authenticate is a middleware for JWT authentication with enhanced security.
func (m *AccessKeyMiddleware) Authenticate(ctx *fiber.Ctx) error {
// Log authentication attempt
ip := ctx.IP()
userAgent := ctx.Get("User-Agent")
authHeader := ctx.Get("Access-Key")
if authHeader == "" {
logging.Error("Authentication failed: missing Access-Key header from IP %s", ip)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Missing or malformed JWT",
})
}
if len(authHeader) < 10 || len(authHeader) > 2048 {
logging.Error("Authentication failed: invalid token length from IP %s", ip)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
if authHeader != configs.AccessKey {
logging.Error("Authentication failed: invalid token from IP %s, User-Agent: %s", ip, userAgent)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
ctx.Locals("userID", m.userInfo.UserID)
ctx.Locals("userInfo", m.userInfo)
ctx.Locals("authTime", time.Now())
logging.InfoWithContext("AUTH", "User %s authenticated successfully from IP %s", m.userInfo.UserID, ip)
return ctx.Next()
}

View File

@@ -35,9 +35,9 @@ type ConfigFilter struct {
} }
// ApiFilter defines filtering options for Api queries // ApiFilter defines filtering options for Api queries
type ApiFilter struct { type ServiceControlFilter struct {
BaseFilter BaseFilter
Api string `query:"api"` ServiceControl string `query:"serviceControl"`
} }
// MembershipFilter defines filtering options for User queries // MembershipFilter defines filtering options for User queries

View File

@@ -16,6 +16,25 @@ const (
ServiceNamePrefix = "ACC-Server" ServiceNamePrefix = "ACC-Server"
) )
// Server represents an ACC server instance
type ServerAPI struct {
Name string `json:"name"`
Status ServiceStatus `json:"status"`
State *ServerState `json:"state"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
}
func (s *Server) ToServerAPI() *ServerAPI {
return &ServerAPI{
Name: s.Name,
Status: s.Status,
State: s.State,
PlayerCount: s.State.PlayerCount,
Track: s.State.Track,
}
}
// Server represents an ACC server instance // Server represents an ACC server instance
type Server struct { type Server struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"` ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`

View File

@@ -104,6 +104,6 @@ func (s ServiceStatus) Value() (driver.Value, error) {
return s.String(), nil return s.String(), nil
} }
type ApiModel struct { type ServiceControlModel struct {
Api string `json:"api"` ServiceControl string `json:"serviceControl"`
} }

View File

@@ -1,17 +0,0 @@
package repository
import (
"acc-server-manager/local/model"
"gorm.io/gorm"
)
type ApiRepository struct {
*BaseRepository[model.ApiModel, model.ApiFilter]
}
func NewApiRepository(db *gorm.DB) *ApiRepository {
return &ApiRepository{
BaseRepository: NewBaseRepository[model.ApiModel, model.ApiFilter](db, model.ApiModel{}),
}
}

View File

@@ -10,7 +10,7 @@ import (
// Args: // Args:
// *dig.Container: Dig Container // *dig.Container: Dig Container
func InitializeRepositories(c *dig.Container) { func InitializeRepositories(c *dig.Container) {
c.Provide(NewApiRepository) c.Provide(NewServiceControlRepository)
c.Provide(NewStateHistoryRepository) c.Provide(NewStateHistoryRepository)
c.Provide(NewServerRepository) c.Provide(NewServerRepository)
c.Provide(NewConfigRepository) c.Provide(NewConfigRepository)

View File

@@ -0,0 +1,17 @@
package repository
import (
"acc-server-manager/local/model"
"gorm.io/gorm"
)
type ServiceControlRepository struct {
*BaseRepository[model.ServiceControlModel, model.ServiceControlFilter]
}
func NewServiceControlRepository(db *gorm.DB) *ServiceControlRepository {
return &ServiceControlRepository{
BaseRepository: NewBaseRepository[model.ServiceControlModel, model.ServiceControlFilter](db, model.ServiceControlModel{}),
}
}

View File

@@ -26,7 +26,7 @@ const (
type ServerService struct { type ServerService struct {
repository *repository.ServerRepository repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService apiService *ServiceControlService
configService *ConfigService configService *ConfigService
steamService *SteamService steamService *SteamService
windowsService *WindowsService windowsService *WindowsService
@@ -63,7 +63,7 @@ func (s *ServerService) ensureLogTailing(server *model.Server, instance *trackin
func NewServerService( func NewServerService(
repository *repository.ServerRepository, repository *repository.ServerRepository,
stateHistoryRepo *repository.StateHistoryRepository, stateHistoryRepo *repository.StateHistoryRepository,
apiService *ApiService, apiService *ServiceControlService,
configService *ConfigService, configService *ConfigService,
steamService *SteamService, steamService *SteamService,
windowsService *WindowsService, windowsService *WindowsService,

View File

@@ -20,7 +20,7 @@ func InitializeServices(c *dig.Container) {
// Provide services // Provide services
c.Provide(NewServerService) c.Provide(NewServerService)
c.Provide(NewStateHistoryService) c.Provide(NewStateHistoryService)
c.Provide(NewApiService) c.Provide(NewServiceControlService)
c.Provide(NewConfigService) c.Provide(NewConfigService)
c.Provide(NewLookupService) c.Provide(NewLookupService)
c.Provide(NewSteamService) c.Provide(NewSteamService)
@@ -29,7 +29,7 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewMembershipService) c.Provide(NewMembershipService)
logging.Debug("Initializing service dependencies") logging.Debug("Initializing service dependencies")
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService) { err := c.Invoke(func(server *ServerService, api *ServiceControlService, config *ConfigService) {
logging.Debug("Setting up service cross-references") logging.Debug("Setting up service cross-references")
api.SetServerService(server) api.SetServerService(server)
config.SetServerService(server) config.SetServerService(server)

View File

@@ -10,17 +10,17 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type ApiService struct { type ServiceControlService struct {
repository *repository.ApiRepository repository *repository.ServiceControlRepository
serverRepository *repository.ServerRepository serverRepository *repository.ServerRepository
serverService *ServerService serverService *ServerService
statusCache *model.ServerStatusCache statusCache *model.ServerStatusCache
windowsService *WindowsService windowsService *WindowsService
} }
func NewApiService(repository *repository.ApiRepository, func NewServiceControlService(repository *repository.ServiceControlRepository,
serverRepository *repository.ServerRepository) *ApiService { serverRepository *repository.ServerRepository) *ServiceControlService {
return &ApiService{ return &ServiceControlService{
repository: repository, repository: repository,
serverRepository: serverRepository, serverRepository: serverRepository,
statusCache: model.NewServerStatusCache(model.CacheConfig{ statusCache: model.NewServerStatusCache(model.CacheConfig{
@@ -32,11 +32,11 @@ func NewApiService(repository *repository.ApiRepository,
} }
} }
func (as *ApiService) SetServerService(serverService *ServerService) { func (as *ServiceControlService) SetServerService(serverService *ServerService) {
as.serverService = serverService as.serverService = serverService
} }
func (as *ApiService) GetStatus(ctx *fiber.Ctx) (string, error) { func (as *ServiceControlService) GetStatus(ctx *fiber.Ctx) (string, error) {
serviceName, err := as.GetServiceName(ctx) serviceName, err := as.GetServiceName(ctx)
if err != nil { if err != nil {
return "", err return "", err
@@ -59,7 +59,7 @@ func (as *ApiService) GetStatus(ctx *fiber.Ctx) (string, error) {
return status.String(), nil return status.String(), nil
} }
func (as *ApiService) ApiStartServer(ctx *fiber.Ctx) (string, error) { func (as *ServiceControlService) ServiceControlStartServer(ctx *fiber.Ctx) (string, error) {
serviceName, err := as.GetServiceName(ctx) serviceName, err := as.GetServiceName(ctx)
if err != nil { if err != nil {
return "", err return "", err
@@ -83,7 +83,7 @@ func (as *ApiService) ApiStartServer(ctx *fiber.Ctx) (string, error) {
return status.String(), nil return status.String(), nil
} }
func (as *ApiService) ApiStopServer(ctx *fiber.Ctx) (string, error) { func (as *ServiceControlService) ServiceControlStopServer(ctx *fiber.Ctx) (string, error) {
serviceName, err := as.GetServiceName(ctx) serviceName, err := as.GetServiceName(ctx)
if err != nil { if err != nil {
return "", err return "", err
@@ -107,7 +107,7 @@ func (as *ApiService) ApiStopServer(ctx *fiber.Ctx) (string, error) {
return status.String(), nil return status.String(), nil
} }
func (as *ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) { func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (string, error) {
serviceName, err := as.GetServiceName(ctx) serviceName, err := as.GetServiceName(ctx)
if err != nil { if err != nil {
return "", err return "", err
@@ -131,12 +131,12 @@ func (as *ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) {
return status.String(), nil return status.String(), nil
} }
func (as *ApiService) StatusServer(serviceName string) (string, error) { func (as *ServiceControlService) StatusServer(serviceName string) (string, error) {
return as.windowsService.Status(context.Background(), serviceName) return as.windowsService.Status(context.Background(), serviceName)
} }
// GetCachedStatus gets the cached status for a service name without requiring fiber context // GetCachedStatus gets the cached status for a service name without requiring fiber context
func (as *ApiService) GetCachedStatus(serviceName string) (string, error) { func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, error) {
// Try to get status from cache // Try to get status from cache
if status, shouldCheck := as.statusCache.GetStatus(serviceName); !shouldCheck { if status, shouldCheck := as.statusCache.GetStatus(serviceName); !shouldCheck {
return status.String(), nil return status.String(), nil
@@ -154,7 +154,7 @@ func (as *ApiService) GetCachedStatus(serviceName string) (string, error) {
return status.String(), nil return status.String(), nil
} }
func (as *ApiService) StartServer(serviceName string) (string, error) { func (as *ServiceControlService) StartServer(serviceName string) (string, error) {
status, err := as.windowsService.Start(context.Background(), serviceName) status, err := as.windowsService.Start(context.Background(), serviceName)
if err != nil { if err != nil {
return "", err return "", err
@@ -168,7 +168,7 @@ func (as *ApiService) StartServer(serviceName string) (string, error) {
return status, err return status, err
} }
func (as *ApiService) StopServer(serviceName string) (string, error) { func (as *ServiceControlService) StopServer(serviceName string) (string, error) {
status, err := as.windowsService.Stop(context.Background(), serviceName) status, err := as.windowsService.Stop(context.Background(), serviceName)
if err != nil { if err != nil {
return "", err return "", err
@@ -183,7 +183,7 @@ func (as *ApiService) StopServer(serviceName string) (string, error) {
return status, err return status, err
} }
func (as *ApiService) RestartServer(serviceName string) (string, error) { func (as *ServiceControlService) RestartServer(serviceName string) (string, error) {
status, err := as.windowsService.Restart(context.Background(), serviceName) status, err := as.windowsService.Restart(context.Background(), serviceName)
if err != nil { if err != nil {
return "", err return "", err
@@ -197,7 +197,7 @@ func (as *ApiService) RestartServer(serviceName string) (string, error) {
return status, err return status, err
} }
func (as *ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) { func (as *ServiceControlService) GetServiceName(ctx *fiber.Ctx) (string, error) {
var server *model.Server var server *model.Server
var err error var err error
serviceName, ok := ctx.Locals("service").(string) serviceName, ok := ctx.Locals("service").(string)

View File

@@ -24,6 +24,7 @@ type RouteGroups struct {
Lookup fiber.Router Lookup fiber.Router
StateHistory fiber.Router StateHistory fiber.Router
Membership fiber.Router Membership fiber.Router
System fiber.Router
} }
func CheckError(err error) { func CheckError(err error) {

View File

@@ -13,14 +13,16 @@ var (
Secret string Secret string
SecretCode string SecretCode string
EncryptionKey string EncryptionKey string
AccessKey string
) )
func init() { func Init() {
godotenv.Load() godotenv.Load()
// Fail fast if critical environment variables are missing // Fail fast if critical environment variables are missing
Secret = getEnvRequired("APP_SECRET") Secret = getEnvRequired("APP_SECRET")
SecretCode = getEnvRequired("APP_SECRET_CODE") SecretCode = getEnvRequired("APP_SECRET_CODE")
EncryptionKey = getEnvRequired("ENCRYPTION_KEY") EncryptionKey = getEnvRequired("ENCRYPTION_KEY")
AccessKey = getEnvRequired("ACCESS_KEY")
if len(EncryptionKey) != 32 { if len(EncryptionKey) != 32 {
log.Fatal("ENCRYPTION_KEY must be exactly 32 bytes long for AES-256") log.Fatal("ENCRYPTION_KEY must be exactly 32 bytes long for AES-256")

View File

@@ -35,7 +35,7 @@ func Migrate(db *gorm.DB) {
// Run GORM AutoMigrate for all models // Run GORM AutoMigrate for all models
err := db.AutoMigrate( err := db.AutoMigrate(
&model.ApiModel{}, &model.ServiceControlModel{},
&model.Config{}, &model.Config{},
&model.Track{}, &model.Track{},
&model.CarModel{}, &model.CarModel{},
@@ -55,7 +55,7 @@ func Migrate(db *gorm.DB) {
// Don't panic, just log the error as custom migrations may have handled this // Don't panic, just log the error as custom migrations may have handled this
} }
db.FirstOrCreate(&model.ApiModel{Api: "Works"}) db.FirstOrCreate(&model.ServiceControlModel{ServiceControl: "Works"})
Seed(db) Seed(db)
} }

View File

@@ -22,7 +22,7 @@ type Claims struct {
} }
// init initializes the JWT secret key from environment variable // init initializes the JWT secret key from environment variable
func init() { func Init() {
jwtSecret := os.Getenv("JWT_SECRET") jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" { if jwtSecret == "" {
log.Fatal("JWT_SECRET environment variable is required and cannot be empty") log.Fatal("JWT_SECRET environment variable is required and cannot be empty")

View File

@@ -2,6 +2,8 @@ package tests
import ( import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/utl/configs"
"acc-server-manager/local/utl/jwt"
"bytes" "bytes"
"context" "context"
"errors" "errors"
@@ -45,8 +47,12 @@ func SetTestEnv() {
os.Setenv("APP_SECRET_CODE", "test-code-for-testing-123456789012") os.Setenv("APP_SECRET_CODE", "test-code-for-testing-123456789012")
os.Setenv("ENCRYPTION_KEY", "12345678901234567890123456789012") os.Setenv("ENCRYPTION_KEY", "12345678901234567890123456789012")
os.Setenv("JWT_SECRET", "test-jwt-secret-key-for-testing-123456789012345678901234567890") 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 // Set test-specific environment variables
os.Setenv("TESTING_ENV", "true") // Used to bypass os.Setenv("TESTING_ENV", "true") // Used to bypass
configs.Init()
jwt.Init()
} }
// NewTestHelper creates a new test helper with in-memory database // NewTestHelper creates a new test helper with in-memory database

View File

@@ -1,547 +0,0 @@
package controller
import (
"acc-server-manager/local/controller"
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/tests"
"bytes"
"encoding/json"
"io"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func TestConfigController_GetConfig_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Setup expected response
expectedConfig := &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),
}
mockConfigService.getConfigResponse = expectedConfig
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Create test request
serverID := uuid.New().String()
req := httptest.NewRequest("GET", "/config/configuration.json", nil)
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response model.Configuration
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, expectedConfig.UdpPort, response.UdpPort)
tests.AssertEqual(t, expectedConfig.TcpPort, response.TcpPort)
tests.AssertEqual(t, expectedConfig.MaxConnections, response.MaxConnections)
}
func TestConfigController_GetConfig_Unauthorized(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Create test request
req := httptest.NewRequest("GET", "/config/configuration.json", nil)
req.Header.Set("Content-Type", "application/json")
// Mock authentication failure
mockAuth.authenticated = false
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
}
func TestConfigController_GetConfig_ServiceError(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Setup service error
mockConfigService.shouldFailGet = true
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Create test request
req := httptest.NewRequest("GET", "/config/configuration.json", nil)
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 500, resp.StatusCode)
}
func TestConfigController_UpdateConfig_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Setup expected response
expectedConfig := &model.Config{
ID: uuid.New(),
ServerID: uuid.New(),
ConfigFile: "configuration.json",
OldConfig: `{"udpPort": "9231"}`,
NewConfig: `{"udpPort": "9999"}`,
}
mockConfigService.updateConfigResponse = expectedConfig
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config/:id"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Prepare request body
updateData := map[string]interface{}{
"udpPort": "9999",
"tcpPort": "10000",
}
bodyBytes, err := json.Marshal(updateData)
tests.AssertNoError(t, err)
// Create test request
serverID := uuid.New().String()
req := httptest.NewRequest("PUT", "/config/"+serverID+"/configuration.json", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response model.Config
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, expectedConfig.ConfigFile, response.ConfigFile)
tests.AssertEqual(t, expectedConfig.OldConfig, response.OldConfig)
tests.AssertEqual(t, expectedConfig.NewConfig, response.NewConfig)
// Verify service was called with correct data
tests.AssertEqual(t, true, mockConfigService.updateConfigCalled)
}
func TestConfigController_UpdateConfig_WithRestart(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Setup expected response
expectedConfig := &model.Config{
ID: uuid.New(),
ServerID: uuid.New(),
ConfigFile: "configuration.json",
OldConfig: `{"udpPort": "9231"}`,
NewConfig: `{"udpPort": "9999"}`,
}
mockConfigService.updateConfigResponse = expectedConfig
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config/:id"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Prepare request body
updateData := map[string]interface{}{
"udpPort": "9999",
}
bodyBytes, err := json.Marshal(updateData)
tests.AssertNoError(t, err)
// Create test request with restart parameter
serverID := uuid.New().String()
req := httptest.NewRequest("PUT", "/config/"+serverID+"/configuration.json?restart=true", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Verify both services were called
tests.AssertEqual(t, true, mockConfigService.updateConfigCalled)
tests.AssertEqual(t, true, mockApiService.restartServerCalled)
}
func TestConfigController_UpdateConfig_InvalidUUID(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config/:id"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Prepare request body
updateData := map[string]interface{}{
"udpPort": "9999",
}
bodyBytes, err := json.Marshal(updateData)
tests.AssertNoError(t, err)
// Create test request with invalid UUID
req := httptest.NewRequest("PUT", "/config/invalid-uuid/configuration.json", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 400, resp.StatusCode)
// Verify service was not called
tests.AssertEqual(t, false, mockConfigService.updateConfigCalled)
}
func TestConfigController_UpdateConfig_InvalidJSON(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config/:id"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Create test request with invalid JSON
serverID := uuid.New().String()
req := httptest.NewRequest("PUT", "/config/"+serverID+"/configuration.json", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 400, resp.StatusCode)
// Verify service was not called
tests.AssertEqual(t, false, mockConfigService.updateConfigCalled)
}
func TestConfigController_UpdateConfig_ServiceError(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Setup service error
mockConfigService.shouldFailUpdate = true
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config/:id"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Prepare request body
updateData := map[string]interface{}{
"udpPort": "9999",
}
bodyBytes, err := json.Marshal(updateData)
tests.AssertNoError(t, err)
// Create test request
serverID := uuid.New().String()
req := httptest.NewRequest("PUT", "/config/"+serverID+"/configuration.json", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 500, resp.StatusCode)
// Verify service was called
tests.AssertEqual(t, true, mockConfigService.updateConfigCalled)
}
func TestConfigController_GetConfigs_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock services
mockConfigService := &MockConfigService{}
mockApiService := &MockApiService{}
// Setup expected response
expectedConfigs := &model.Configurations{
Configuration: model.Configuration{
UdpPort: model.IntString(9231),
TcpPort: model.IntString(9232),
},
Settings: model.ServerSettings{
ServerName: "Test Server",
},
Event: model.EventConfig{
Track: "spa",
},
}
mockConfigService.getConfigsResponse = expectedConfigs
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Config: app.Group("/config"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewConfigController(mockConfigService, routeGroups, mockApiService, mockAuth)
// Create test request
req := httptest.NewRequest("GET", "/config/", nil)
req.Header.Set("Content-Type", "application/json")
// Mock authentication
mockAuth.authenticated = true
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response model.Configurations
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, expectedConfigs.Configuration.UdpPort, response.Configuration.UdpPort)
tests.AssertEqual(t, expectedConfigs.Settings.ServerName, response.Settings.ServerName)
tests.AssertEqual(t, expectedConfigs.Event.Track, response.Event.Track)
}
// MockConfigService implements the ConfigService interface for testing
type MockConfigService struct {
getConfigResponse interface{}
getConfigsResponse *model.Configurations
updateConfigResponse *model.Config
shouldFailGet bool
shouldFailUpdate bool
getConfigCalled bool
getConfigsCalled bool
updateConfigCalled bool
}
func (m *MockConfigService) GetConfig(c *fiber.Ctx) (interface{}, error) {
m.getConfigCalled = true
if m.shouldFailGet {
return nil, tests.ErrorForTesting("service error")
}
return m.getConfigResponse, nil
}
func (m *MockConfigService) GetConfigs(c *fiber.Ctx) (*model.Configurations, error) {
m.getConfigsCalled = true
if m.shouldFailGet {
return nil, tests.ErrorForTesting("service error")
}
return m.getConfigsResponse, nil
}
func (m *MockConfigService) UpdateConfig(c *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
m.updateConfigCalled = true
if m.shouldFailUpdate {
return nil, tests.ErrorForTesting("service error")
}
return m.updateConfigResponse, nil
}
// Additional methods that might be needed by the service interface
func (m *MockConfigService) LoadConfigs(server *model.Server) (*model.Configurations, error) {
return m.getConfigsResponse, nil
}
func (m *MockConfigService) GetConfiguration(server *model.Server) (*model.Configuration, error) {
if config, ok := m.getConfigResponse.(*model.Configuration); ok {
return config, nil
}
return nil, tests.ErrorForTesting("type assertion failed")
}
func (m *MockConfigService) GetEventConfig(server *model.Server) (*model.EventConfig, error) {
if config, ok := m.getConfigResponse.(*model.EventConfig); ok {
return config, nil
}
return nil, tests.ErrorForTesting("type assertion failed")
}
func (m *MockConfigService) SaveConfiguration(server *model.Server, config *model.Configuration) error {
return nil
}
func (m *MockConfigService) SetServerService(serverService *service.ServerService) {
// Mock implementation
}
// MockApiService implements the ApiService interface for testing
type MockApiService struct {
restartServerCalled bool
shouldFailRestart bool
}
func (m *MockApiService) ApiRestartServer(c *fiber.Ctx) (interface{}, error) {
m.restartServerCalled = true
if m.shouldFailRestart {
return nil, tests.ErrorForTesting("restart failed")
}
return fiber.Map{"message": "server restarted"}, nil
}
// MockAuthMiddleware implements the AuthMiddleware interface for testing
type MockAuthMiddleware struct {
authenticated bool
hasPermission bool
}
func (m *MockAuthMiddleware) Authenticate(c *fiber.Ctx) error {
if !m.authenticated {
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
return c.Next()
}
func (m *MockAuthMiddleware) HasPermission(permission string) fiber.Handler {
return func(c *fiber.Ctx) error {
if !m.authenticated {
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
if !m.hasPermission {
return c.Status(403).JSON(fiber.Map{"error": "Forbidden"})
}
return c.Next()
}
}
func (m *MockAuthMiddleware) AuthRateLimit() fiber.Handler {
return func(c *fiber.Ctx) error {
return c.Next()
}
}
func (m *MockAuthMiddleware) RequireHTTPS() fiber.Handler {
return func(c *fiber.Ctx) error {
return c.Next()
}
}
func (m *MockAuthMiddleware) InvalidateUserPermissions(userID string) {
// Mock implementation
}
func (m *MockAuthMiddleware) InvalidateAllUserPermissions() {
// Mock implementation
}

View File

@@ -14,6 +14,11 @@ import (
) )
func TestController_JSONParsing_Success(t *testing.T) { func TestController_JSONParsing_Success(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test basic JSON parsing functionality // Test basic JSON parsing functionality
app := fiber.New() app := fiber.New()
@@ -55,6 +60,11 @@ func TestController_JSONParsing_Success(t *testing.T) {
} }
func TestController_JSONParsing_InvalidJSON(t *testing.T) { func TestController_JSONParsing_InvalidJSON(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test handling of invalid JSON // Test handling of invalid JSON
app := fiber.New() app := fiber.New()
@@ -87,6 +97,11 @@ func TestController_JSONParsing_InvalidJSON(t *testing.T) {
} }
func TestController_UUIDValidation_Success(t *testing.T) { func TestController_UUIDValidation_Success(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test UUID parameter validation // Test UUID parameter validation
app := fiber.New() app := fiber.New()
@@ -123,6 +138,11 @@ func TestController_UUIDValidation_Success(t *testing.T) {
} }
func TestController_UUIDValidation_InvalidUUID(t *testing.T) { func TestController_UUIDValidation_InvalidUUID(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test handling of invalid UUID // Test handling of invalid UUID
app := fiber.New() app := fiber.New()
@@ -157,6 +177,11 @@ func TestController_UUIDValidation_InvalidUUID(t *testing.T) {
} }
func TestController_QueryParameters_Success(t *testing.T) { func TestController_QueryParameters_Success(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test query parameter handling // Test query parameter handling
app := fiber.New() app := fiber.New()
@@ -194,6 +219,11 @@ func TestController_QueryParameters_Success(t *testing.T) {
} }
func TestController_HTTPMethods_Success(t *testing.T) { func TestController_HTTPMethods_Success(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test different HTTP methods // Test different HTTP methods
app := fiber.New() app := fiber.New()
@@ -249,6 +279,11 @@ func TestController_HTTPMethods_Success(t *testing.T) {
} }
func TestController_ErrorHandling_StatusCodes(t *testing.T) { func TestController_ErrorHandling_StatusCodes(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test different error status codes // Test different error status codes
app := fiber.New() app := fiber.New()
@@ -293,6 +328,11 @@ func TestController_ErrorHandling_StatusCodes(t *testing.T) {
} }
func TestController_ConfigurationModel_JSONSerialization(t *testing.T) { func TestController_ConfigurationModel_JSONSerialization(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test Configuration model JSON serialization // Test Configuration model JSON serialization
app := fiber.New() app := fiber.New()
@@ -333,6 +373,11 @@ func TestController_ConfigurationModel_JSONSerialization(t *testing.T) {
} }
func TestController_UserModel_JSONSerialization(t *testing.T) { func TestController_UserModel_JSONSerialization(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test User model JSON serialization (password should be hidden) // Test User model JSON serialization (password should be hidden)
app := fiber.New() app := fiber.New()
@@ -370,6 +415,11 @@ func TestController_UserModel_JSONSerialization(t *testing.T) {
} }
func TestController_MiddlewareChaining_Success(t *testing.T) { func TestController_MiddlewareChaining_Success(t *testing.T) {
// Setup environment and test helper
tests.SetTestEnv()
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Test middleware chaining // Test middleware chaining
app := fiber.New() app := fiber.New()

View File

@@ -1,598 +0,0 @@
package controller
import (
"acc-server-manager/local/controller"
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/tests"
"bytes"
"context"
"encoding/json"
"io"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func TestMembershipController_Login_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service
mockMembershipService := &MockMembershipService{
loginResponse: "mock-jwt-token-12345",
}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Prepare request body
loginData := map[string]string{
"username": "testuser",
"password": "password123",
}
bodyBytes, err := json.Marshal(loginData)
tests.AssertNoError(t, err)
// Create test request
req := httptest.NewRequest("POST", "/auth/login", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response map[string]string
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, "mock-jwt-token-12345", response["token"])
tests.AssertEqual(t, true, mockMembershipService.loginCalled)
}
func TestMembershipController_Login_InvalidCredentials(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service with login failure
mockMembershipService := &MockMembershipService{
shouldFailLogin: true,
}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Prepare request body
loginData := map[string]string{
"username": "baduser",
"password": "wrongpassword",
}
bodyBytes, err := json.Marshal(loginData)
tests.AssertNoError(t, err)
// Create test request
req := httptest.NewRequest("POST", "/auth/login", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
// Verify service was called
tests.AssertEqual(t, true, mockMembershipService.loginCalled)
}
func TestMembershipController_Login_InvalidJSON(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service
mockMembershipService := &MockMembershipService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Create test request with invalid JSON
req := httptest.NewRequest("POST", "/auth/login", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 400, resp.StatusCode)
// Verify service was not called
tests.AssertEqual(t, false, mockMembershipService.loginCalled)
}
func TestMembershipController_CreateUser_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create expected user response
expectedUser := &model.User{
ID: uuid.New(),
Username: "newuser",
RoleID: uuid.New(),
}
// Create mock service
mockMembershipService := &MockMembershipService{
createUserResponse: expectedUser,
}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{authenticated: true}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Prepare request body
createUserData := map[string]string{
"username": "newuser",
"password": "password123",
"role": "User",
}
bodyBytes, err := json.Marshal(createUserData)
tests.AssertNoError(t, err)
// Create test request
req := httptest.NewRequest("POST", "/membership/", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response model.User
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, expectedUser.ID, response.ID)
tests.AssertEqual(t, expectedUser.Username, response.Username)
tests.AssertEqual(t, true, mockMembershipService.createUserCalled)
}
func TestMembershipController_CreateUser_Unauthorized(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service
mockMembershipService := &MockMembershipService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{authenticated: false}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Prepare request body
createUserData := map[string]string{
"username": "newuser",
"password": "password123",
"role": "User",
}
bodyBytes, err := json.Marshal(createUserData)
tests.AssertNoError(t, err)
// Create test request
req := httptest.NewRequest("POST", "/membership/", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
// Verify service was not called
tests.AssertEqual(t, false, mockMembershipService.createUserCalled)
}
func TestMembershipController_CreateUser_Forbidden(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service
mockMembershipService := &MockMembershipService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{
authenticated: true,
hasPermission: false, // User doesn't have MembershipCreate permission
}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Prepare request body
createUserData := map[string]string{
"username": "newuser",
"password": "password123",
"role": "User",
}
bodyBytes, err := json.Marshal(createUserData)
tests.AssertNoError(t, err)
// Create test request
req := httptest.NewRequest("POST", "/membership/", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 403, resp.StatusCode)
// Verify service was not called
tests.AssertEqual(t, false, mockMembershipService.createUserCalled)
}
func TestMembershipController_ListUsers_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create expected users response
expectedUsers := []*model.User{
{
ID: uuid.New(),
Username: "user1",
RoleID: uuid.New(),
},
{
ID: uuid.New(),
Username: "user2",
RoleID: uuid.New(),
},
}
// Create mock service
mockMembershipService := &MockMembershipService{
listUsersResponse: expectedUsers,
}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{
authenticated: true,
hasPermission: true,
}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Create test request
req := httptest.NewRequest("GET", "/membership/", nil)
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response []*model.User
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, 2, len(response))
tests.AssertEqual(t, expectedUsers[0].Username, response[0].Username)
tests.AssertEqual(t, expectedUsers[1].Username, response[1].Username)
tests.AssertEqual(t, true, mockMembershipService.listUsersCalled)
}
func TestMembershipController_GetUser_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create expected user response
userID := uuid.New()
expectedUser := &model.User{
ID: userID,
Username: "testuser",
RoleID: uuid.New(),
}
// Create mock service
mockMembershipService := &MockMembershipService{
getUserResponse: expectedUser,
}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{
authenticated: true,
hasPermission: true,
}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Create test request
req := httptest.NewRequest("GET", "/membership/"+userID.String(), nil)
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response model.User
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, expectedUser.ID, response.ID)
tests.AssertEqual(t, expectedUser.Username, response.Username)
tests.AssertEqual(t, true, mockMembershipService.getUserCalled)
}
func TestMembershipController_GetUser_InvalidUUID(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service
mockMembershipService := &MockMembershipService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{
authenticated: true,
hasPermission: true,
}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Create test request with invalid UUID
req := httptest.NewRequest("GET", "/membership/invalid-uuid", nil)
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 400, resp.StatusCode)
// Verify service was not called
tests.AssertEqual(t, false, mockMembershipService.getUserCalled)
}
func TestMembershipController_DeleteUser_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create mock service
mockMembershipService := &MockMembershipService{}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{
authenticated: true,
hasPermission: true,
}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Create test request
userID := uuid.New().String()
req := httptest.NewRequest("DELETE", "/membership/"+userID, nil)
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Verify service was called
tests.AssertEqual(t, true, mockMembershipService.deleteUserCalled)
}
func TestMembershipController_GetMe_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create expected user response
expectedUser := &model.User{
ID: uuid.New(),
Username: "currentuser",
RoleID: uuid.New(),
}
// Create mock service
mockMembershipService := &MockMembershipService{
getUserWithPermissionsResponse: expectedUser,
}
// Create Fiber app with controller
app := fiber.New()
routeGroups := &common.RouteGroups{
Auth: app.Group("/auth"),
Membership: app.Group("/membership"),
}
mockAuth := &MockAuthMiddleware{authenticated: true}
controller.NewMembershipController(mockMembershipService, mockAuth, routeGroups)
// Create test request
req := httptest.NewRequest("GET", "/auth/me", nil)
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
// Parse response
var response model.User
body, err := io.ReadAll(resp.Body)
tests.AssertNoError(t, err)
err = json.Unmarshal(body, &response)
tests.AssertNoError(t, err)
// Verify response
tests.AssertEqual(t, expectedUser.ID, response.ID)
tests.AssertEqual(t, expectedUser.Username, response.Username)
}
// MockMembershipService implements the MembershipService interface for testing
type MockMembershipService struct {
loginResponse string
createUserResponse *model.User
listUsersResponse []*model.User
getUserResponse *model.User
getUserWithPermissionsResponse *model.User
getRolesResponse []*model.Role
shouldFailLogin bool
shouldFailCreateUser bool
shouldFailListUsers bool
shouldFailGetUser bool
shouldFailGetUserWithPermissions bool
shouldFailDeleteUser bool
shouldFailUpdateUser bool
shouldFailGetRoles bool
loginCalled bool
createUserCalled bool
listUsersCalled bool
getUserCalled bool
getUserWithPermissionsCalled bool
deleteUserCalled bool
updateUserCalled bool
getRolesCalled bool
}
func (m *MockMembershipService) Login(ctx context.Context, username, password string) (string, error) {
m.loginCalled = true
if m.shouldFailLogin {
return "", tests.ErrorForTesting("invalid credentials")
}
return m.loginResponse, nil
}
func (m *MockMembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
m.createUserCalled = true
if m.shouldFailCreateUser {
return nil, tests.ErrorForTesting("failed to create user")
}
return m.createUserResponse, nil
}
func (m *MockMembershipService) ListUsers(ctx context.Context) ([]*model.User, error) {
m.listUsersCalled = true
if m.shouldFailListUsers {
return nil, tests.ErrorForTesting("failed to list users")
}
return m.listUsersResponse, nil
}
func (m *MockMembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) {
m.getUserCalled = true
if m.shouldFailGetUser {
return nil, tests.ErrorForTesting("user not found")
}
return m.getUserResponse, nil
}
func (m *MockMembershipService) GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error) {
m.getUserWithPermissionsCalled = true
if m.shouldFailGetUserWithPermissions {
return nil, tests.ErrorForTesting("user not found")
}
return m.getUserWithPermissionsResponse, nil
}
func (m *MockMembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
m.deleteUserCalled = true
if m.shouldFailDeleteUser {
return tests.ErrorForTesting("failed to delete user")
}
return nil
}
func (m *MockMembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, updates map[string]interface{}) (*model.User, error) {
m.updateUserCalled = true
if m.shouldFailUpdateUser {
return nil, tests.ErrorForTesting("failed to update user")
}
return m.getUserResponse, nil
}
func (m *MockMembershipService) GetRoles(ctx context.Context) ([]*model.Role, error) {
m.getRolesCalled = true
if m.shouldFailGetRoles {
return nil, tests.ErrorForTesting("failed to get roles")
}
return m.getRolesResponse, nil
}
func (m *MockMembershipService) SetCacheInvalidator(invalidator service.CacheInvalidator) {
// Mock implementation
}
func (m *MockMembershipService) SetupInitialData(ctx context.Context) error {
// Mock implementation - no-op for testing
return nil
}

View File

@@ -1,684 +0,0 @@
package service
import (
"acc-server-manager/local/middleware"
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/cache"
"acc-server-manager/local/utl/jwt"
"acc-server-manager/tests"
"context"
"errors"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
jwtLib "github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
)
func TestAuthMiddleware_Authenticate_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "User",
Permissions: []model.Permission{
{Name: "read", Description: "Read permission"},
},
},
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with valid token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
}
func TestAuthMiddleware_Authenticate_MissingToken(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
mockMembershipService := &MockMembershipService{}
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request without token
req := httptest.NewRequest("GET", "/test", nil)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
}
func TestAuthMiddleware_Authenticate_InvalidToken(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
mockMembershipService := &MockMembershipService{}
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with invalid token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
}
func TestAuthMiddleware_Authenticate_MalformedHeader(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
mockMembershipService := &MockMembershipService{}
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
testCases := []struct {
name string
header string
}{
{"Missing Bearer", "invalid-token"},
{"Extra parts", "Bearer token1 token2"},
{"Wrong prefix", "Basic token"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", tc.header)
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
})
}
}
func TestAuthMiddleware_Authenticate_ExpiredToken(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate token with very short expiration (simulate expired token)
claims := &jwt.Claims{
UserID: user.ID.String(),
RegisteredClaims: jwtLib.RegisteredClaims{
ExpiresAt: jwtLib.NewNumericDate(time.Now().Add(-1 * time.Hour)), // Expired
},
}
token := jwtLib.NewWithClaims(jwtLib.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwt.SecretKey)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with expired token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+tokenString)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
}
func TestAuthMiddleware_HasPermission_Success(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user with permissions
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "User",
Permissions: []model.Permission{
{Name: "read", Description: "Read permission"},
{Name: "write", Description: "Write permission"},
},
},
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Use(authMiddleware.HasPermission("read"))
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with valid token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
}
func TestAuthMiddleware_HasPermission_Forbidden(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user without required permission
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "User",
Permissions: []model.Permission{
{Name: "read", Description: "Read permission"},
},
},
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Use(authMiddleware.HasPermission("admin")) // User doesn't have admin permission
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with valid token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 403, resp.StatusCode)
}
func TestAuthMiddleware_HasPermission_SuperAdmin(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user with Super Admin role
user := &model.User{
ID: uuid.New(),
Username: "superadmin",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "Super Admin",
Permissions: []model.Permission{
{Name: "basic", Description: "Basic permission"},
},
},
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Use(authMiddleware.HasPermission("any-permission")) // Super Admin has all permissions
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with valid token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
}
func TestAuthMiddleware_HasPermission_Admin(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user with Admin role
user := &model.User{
ID: uuid.New(),
Username: "admin",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "Admin",
Permissions: []model.Permission{
{Name: "basic", Description: "Basic permission"},
},
},
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Use(authMiddleware.HasPermission("any-permission")) // Admin has all permissions
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with valid token
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp.StatusCode)
}
func TestAuthMiddleware_HasPermission_NoUserInContext(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
mockMembershipService := &MockMembershipService{}
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Create Fiber app for testing (skip authentication middleware)
app := fiber.New()
app.Use(authMiddleware.HasPermission("read")) // No user in context
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request
req := httptest.NewRequest("GET", "/test", nil)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
}
func TestAuthMiddleware_UserCaching(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "User",
Permissions: []model.Permission{
{Name: "read", Description: "Read permission"},
},
},
}
// Create mock membership service that tracks calls
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
getUserCallCount: 0,
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// First request - should call database
resp1, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp1.StatusCode)
tests.AssertEqual(t, 1, mockMembershipService.getUserCallCount)
// Second request - should use cache
resp2, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp2.StatusCode)
tests.AssertEqual(t, 1, mockMembershipService.getUserCallCount) // Should still be 1 (cached)
}
func TestAuthMiddleware_CacheInvalidation(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
Role: model.Role{
ID: uuid.New(),
Name: "User",
Permissions: []model.Permission{
{Name: "read", Description: "Read permission"},
},
},
}
// Create mock membership service
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{
user.ID.String(): user,
},
getUserCallCount: 0,
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// First request - should call database
resp1, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp1.StatusCode)
tests.AssertEqual(t, 1, mockMembershipService.getUserCallCount)
// Invalidate cache
authMiddleware.InvalidateUserPermissions(user.ID.String())
// Second request - should call database again due to cache invalidation
resp2, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 200, resp2.StatusCode)
tests.AssertEqual(t, 2, mockMembershipService.getUserCallCount) // Should be 2 (cache invalidated)
}
func TestAuthMiddleware_UserNotFound(t *testing.T) {
// Setup
helper := tests.NewTestHelper(t)
defer helper.Cleanup()
// Create test user for token generation
user := &model.User{
ID: uuid.New(),
Username: "testuser",
Password: "password123",
RoleID: uuid.New(),
}
// Create mock membership service without the user (user not found scenario)
mockMembershipService := &MockMembershipService{
users: map[string]*model.User{}, // Empty - user not found
}
// Create cache and auth middleware
cache := cache.NewInMemoryCache()
authMiddleware := middleware.NewAuthMiddleware(mockMembershipService, cache)
// Generate valid JWT for non-existent user
token, err := jwt.GenerateToken(user)
tests.AssertNoError(t, err)
// Create Fiber app for testing
app := fiber.New()
app.Use(authMiddleware.Authenticate)
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "success"})
})
// Create test request with valid token but non-existent user
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
// Execute request
resp, err := app.Test(req)
tests.AssertNoError(t, err)
tests.AssertEqual(t, 401, resp.StatusCode)
}
// MockMembershipService implements the MembershipService interface for testing
type MockMembershipService struct {
users map[string]*model.User
getUserCallCount int
shouldFailGet bool
}
func (m *MockMembershipService) GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error) {
m.getUserCallCount++
if m.shouldFailGet {
return nil, errors.New("database error")
}
user, exists := m.users[userID]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockMembershipService) SetCacheInvalidator(invalidator service.CacheInvalidator) {
// Mock implementation
}
func (m *MockMembershipService) Login(ctx context.Context, username, password string) (string, error) {
for _, user := range m.users {
if user.Username == username {
if err := user.VerifyPassword(password); err == nil {
return jwt.GenerateToken(user)
}
}
}
return "", errors.New("invalid credentials")
}
func (m *MockMembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
user := &model.User{
ID: uuid.New(),
Username: username,
Password: password,
RoleID: uuid.New(),
}
m.users[user.ID.String()] = user
return user, nil
}
func (m *MockMembershipService) ListUsers(ctx context.Context) ([]*model.User, error) {
users := make([]*model.User, 0, len(m.users))
for _, user := range m.users {
users = append(users, user)
}
return users, nil
}
func (m *MockMembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) {
user, exists := m.users[userID.String()]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockMembershipService) SetShouldFailGet(shouldFail bool) {
m.shouldFailGet = shouldFail
}