From 647f4f74877daf672b7fe1ffec9b072eed43f9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Tue, 29 Jul 2025 20:50:44 +0200 Subject: [PATCH] add server api get and update service control endpoints --- cmd/api/main.go | 6 +- cmd/api/swagger.go | 27 + local/api/api.go | 5 + local/controller/api.go | 149 ---- local/controller/server.go | 18 + local/controller/service_control.go | 148 ++++ .../{stateHistory.go => state_history.go} | 0 local/controller/system.go | 38 + local/middleware/access_key.go | 59 ++ local/model/filter.go | 4 +- local/model/server.go | 19 + local/model/{api.go => service_control.go} | 4 +- local/repository/api.go | 17 - local/repository/repository.go | 2 +- local/repository/service_control.go | 17 + local/service/server.go | 4 +- local/service/service.go | 4 +- local/service/{api.go => service_control.go} | 32 +- local/utl/common/common.go | 1 + local/utl/configs/configs.go | 4 +- local/utl/db/db.go | 4 +- local/utl/jwt/jwt.go | 2 +- tests/test_helper.go | 6 + .../config_controller_test.go.disabled | 547 -------------- .../unit/controller/controller_simple_test.go | 50 ++ .../membership_controller_test.go.disabled | 598 --------------- .../service/auth_service_test.go.disabled | 684 ------------------ 27 files changed, 424 insertions(+), 2025 deletions(-) create mode 100644 cmd/api/swagger.go delete mode 100644 local/controller/api.go create mode 100644 local/controller/service_control.go rename local/controller/{stateHistory.go => state_history.go} (100%) create mode 100644 local/controller/system.go create mode 100644 local/middleware/access_key.go rename local/model/{api.go => service_control.go} (96%) delete mode 100644 local/repository/api.go create mode 100644 local/repository/service_control.go rename local/service/{api.go => service_control.go} (80%) delete mode 100644 tests/unit/controller/config_controller_test.go.disabled delete mode 100644 tests/unit/controller/membership_controller_test.go.disabled delete mode 100644 tests/unit/service/auth_service_test.go.disabled diff --git a/cmd/api/main.go b/cmd/api/main.go index 9cad155..4b60e66 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,7 +2,9 @@ package main import ( "acc-server-manager/local/utl/cache" + "acc-server-manager/local/utl/configs" "acc-server-manager/local/utl/db" + "acc-server-manager/local/utl/jwt" "acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/server" "fmt" @@ -10,10 +12,12 @@ import ( "go.uber.org/dig" - _ "acc-server-manager/docs" + _ "acc-server-manager/swagger" ) func main() { + configs.Init() + jwt.Init() // Initialize new logging system if err := logging.InitializeLogging(); err != nil { fmt.Printf("Failed to initialize logging system: %v\n", err) diff --git a/cmd/api/swagger.go b/cmd/api/swagger.go new file mode 100644 index 0000000..0dbdeb2 --- /dev/null +++ b/cmd/api/swagger.go @@ -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/ diff --git a/local/api/api.go b/local/api/api.go index 7f0bbf2..6d28895 100644 --- a/local/api/api.go +++ b/local/api/api.go @@ -2,6 +2,7 @@ package api import ( "acc-server-manager/local/controller" + "acc-server-manager/local/middleware" "acc-server-manager/local/utl/common" "acc-server-manager/local/utl/configs" "acc-server-manager/local/utl/logging" @@ -29,8 +30,12 @@ func Init(di *dig.Container, app *fiber.App) { Lookup: groups.Group("/lookup"), StateHistory: serverIdGroup.Group("/state-history"), Membership: groups.Group("/membership"), + System: groups.Group("/system"), } + accessKeyMiddleware := middleware.NewAccessKeyMiddleware() + routeGroups.Api.Use(accessKeyMiddleware.Authenticate) + err := di.Provide(func() *common.RouteGroups { return routeGroups }) diff --git a/local/controller/api.go b/local/controller/api.go deleted file mode 100644 index b412fbe..0000000 --- a/local/controller/api.go +++ /dev/null @@ -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"` -} diff --git a/local/controller/server.go b/local/controller/server.go index 59c11c8..42351ce 100644 --- a/local/controller/server.go +++ b/local/controller/server.go @@ -31,10 +31,28 @@ func NewServerController(ss *service.ServerService, routeGroups *common.RouteGro serverRoutes.Post("/", auth.HasPermission(model.ServerCreate), ac.CreateServer) serverRoutes.Put("/:id", auth.HasPermission(model.ServerUpdate), ac.UpdateServer) serverRoutes.Delete("/:id", auth.HasPermission(model.ServerDelete), ac.DeleteServer) + + apiServerRoutes := routeGroups.Api.Group("/server") + apiServerRoutes.Get("/", auth.HasPermission(model.ServerView), ac.GetAllApi) return ac } // 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 { var filter model.ServerFilter if err := common.ParseQueryFilter(c, &filter); err != nil { diff --git a/local/controller/service_control.go b/local/controller/service_control.go new file mode 100644 index 0000000..c2ca5ee --- /dev/null +++ b/local/controller/service_control.go @@ -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"` +} diff --git a/local/controller/stateHistory.go b/local/controller/state_history.go similarity index 100% rename from local/controller/stateHistory.go rename to local/controller/state_history.go diff --git a/local/controller/system.go b/local/controller/system.go new file mode 100644 index 0000000..564a7e7 --- /dev/null +++ b/local/controller/system.go @@ -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) +} diff --git a/local/middleware/access_key.go b/local/middleware/access_key.go new file mode 100644 index 0000000..0fb76cf --- /dev/null +++ b/local/middleware/access_key.go @@ -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() +} diff --git a/local/model/filter.go b/local/model/filter.go index 374a832..13ef18c 100644 --- a/local/model/filter.go +++ b/local/model/filter.go @@ -35,9 +35,9 @@ type ConfigFilter struct { } // ApiFilter defines filtering options for Api queries -type ApiFilter struct { +type ServiceControlFilter struct { BaseFilter - Api string `query:"api"` + ServiceControl string `query:"serviceControl"` } // MembershipFilter defines filtering options for User queries diff --git a/local/model/server.go b/local/model/server.go index a8c27d7..c987be9 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -16,6 +16,25 @@ const ( 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 type Server struct { ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"` diff --git a/local/model/api.go b/local/model/service_control.go similarity index 96% rename from local/model/api.go rename to local/model/service_control.go index 26a4eda..c447294 100644 --- a/local/model/api.go +++ b/local/model/service_control.go @@ -104,6 +104,6 @@ func (s ServiceStatus) Value() (driver.Value, error) { return s.String(), nil } -type ApiModel struct { - Api string `json:"api"` +type ServiceControlModel struct { + ServiceControl string `json:"serviceControl"` } diff --git a/local/repository/api.go b/local/repository/api.go deleted file mode 100644 index cb7ced6..0000000 --- a/local/repository/api.go +++ /dev/null @@ -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{}), - } -} diff --git a/local/repository/repository.go b/local/repository/repository.go index 3292a24..ae5f84c 100644 --- a/local/repository/repository.go +++ b/local/repository/repository.go @@ -10,7 +10,7 @@ import ( // Args: // *dig.Container: Dig Container func InitializeRepositories(c *dig.Container) { - c.Provide(NewApiRepository) + c.Provide(NewServiceControlRepository) c.Provide(NewStateHistoryRepository) c.Provide(NewServerRepository) c.Provide(NewConfigRepository) diff --git a/local/repository/service_control.go b/local/repository/service_control.go new file mode 100644 index 0000000..d97788f --- /dev/null +++ b/local/repository/service_control.go @@ -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{}), + } +} diff --git a/local/service/server.go b/local/service/server.go index d76222b..04ca96b 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -26,7 +26,7 @@ const ( type ServerService struct { repository *repository.ServerRepository stateHistoryRepo *repository.StateHistoryRepository - apiService *ApiService + apiService *ServiceControlService configService *ConfigService steamService *SteamService windowsService *WindowsService @@ -63,7 +63,7 @@ func (s *ServerService) ensureLogTailing(server *model.Server, instance *trackin func NewServerService( repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, - apiService *ApiService, + apiService *ServiceControlService, configService *ConfigService, steamService *SteamService, windowsService *WindowsService, diff --git a/local/service/service.go b/local/service/service.go index afe00b7..7c81b9a 100644 --- a/local/service/service.go +++ b/local/service/service.go @@ -20,7 +20,7 @@ func InitializeServices(c *dig.Container) { // Provide services c.Provide(NewServerService) c.Provide(NewStateHistoryService) - c.Provide(NewApiService) + c.Provide(NewServiceControlService) c.Provide(NewConfigService) c.Provide(NewLookupService) c.Provide(NewSteamService) @@ -29,7 +29,7 @@ func InitializeServices(c *dig.Container) { c.Provide(NewMembershipService) 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") api.SetServerService(server) config.SetServerService(server) diff --git a/local/service/api.go b/local/service/service_control.go similarity index 80% rename from local/service/api.go rename to local/service/service_control.go index 3910966..c284959 100644 --- a/local/service/api.go +++ b/local/service/service_control.go @@ -10,17 +10,17 @@ import ( "github.com/gofiber/fiber/v2" ) -type ApiService struct { - repository *repository.ApiRepository +type ServiceControlService struct { + repository *repository.ServiceControlRepository serverRepository *repository.ServerRepository serverService *ServerService statusCache *model.ServerStatusCache windowsService *WindowsService } -func NewApiService(repository *repository.ApiRepository, - serverRepository *repository.ServerRepository) *ApiService { - return &ApiService{ +func NewServiceControlService(repository *repository.ServiceControlRepository, + serverRepository *repository.ServerRepository) *ServiceControlService { + return &ServiceControlService{ repository: repository, serverRepository: serverRepository, 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 } -func (as *ApiService) GetStatus(ctx *fiber.Ctx) (string, error) { +func (as *ServiceControlService) GetStatus(ctx *fiber.Ctx) (string, error) { serviceName, err := as.GetServiceName(ctx) if err != nil { return "", err @@ -59,7 +59,7 @@ func (as *ApiService) GetStatus(ctx *fiber.Ctx) (string, error) { 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) if err != nil { return "", err @@ -83,7 +83,7 @@ func (as *ApiService) ApiStartServer(ctx *fiber.Ctx) (string, error) { 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) if err != nil { return "", err @@ -107,7 +107,7 @@ func (as *ApiService) ApiStopServer(ctx *fiber.Ctx) (string, error) { 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) if err != nil { return "", err @@ -131,12 +131,12 @@ func (as *ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) { 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) } // 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 if status, shouldCheck := as.statusCache.GetStatus(serviceName); !shouldCheck { return status.String(), nil @@ -154,7 +154,7 @@ func (as *ApiService) GetCachedStatus(serviceName string) (string, error) { 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) if err != nil { return "", err @@ -168,7 +168,7 @@ func (as *ApiService) StartServer(serviceName string) (string, error) { 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) if err != nil { return "", err @@ -183,7 +183,7 @@ func (as *ApiService) StopServer(serviceName string) (string, error) { 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) if err != nil { return "", err @@ -197,7 +197,7 @@ func (as *ApiService) RestartServer(serviceName string) (string, error) { 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 err error serviceName, ok := ctx.Locals("service").(string) diff --git a/local/utl/common/common.go b/local/utl/common/common.go index 202dc66..8bf0e35 100644 --- a/local/utl/common/common.go +++ b/local/utl/common/common.go @@ -24,6 +24,7 @@ type RouteGroups struct { Lookup fiber.Router StateHistory fiber.Router Membership fiber.Router + System fiber.Router } func CheckError(err error) { diff --git a/local/utl/configs/configs.go b/local/utl/configs/configs.go index d699a8a..ba1c43c 100644 --- a/local/utl/configs/configs.go +++ b/local/utl/configs/configs.go @@ -13,14 +13,16 @@ var ( Secret string SecretCode string EncryptionKey string + AccessKey string ) -func init() { +func Init() { godotenv.Load() // Fail fast if critical environment variables are missing Secret = getEnvRequired("APP_SECRET") SecretCode = getEnvRequired("APP_SECRET_CODE") EncryptionKey = getEnvRequired("ENCRYPTION_KEY") + AccessKey = getEnvRequired("ACCESS_KEY") if len(EncryptionKey) != 32 { log.Fatal("ENCRYPTION_KEY must be exactly 32 bytes long for AES-256") diff --git a/local/utl/db/db.go b/local/utl/db/db.go index 47e2440..8c45bed 100644 --- a/local/utl/db/db.go +++ b/local/utl/db/db.go @@ -35,7 +35,7 @@ func Migrate(db *gorm.DB) { // Run GORM AutoMigrate for all models err := db.AutoMigrate( - &model.ApiModel{}, + &model.ServiceControlModel{}, &model.Config{}, &model.Track{}, &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 } - db.FirstOrCreate(&model.ApiModel{Api: "Works"}) + db.FirstOrCreate(&model.ServiceControlModel{ServiceControl: "Works"}) Seed(db) } diff --git a/local/utl/jwt/jwt.go b/local/utl/jwt/jwt.go index 3c7eff4..23ce4b7 100644 --- a/local/utl/jwt/jwt.go +++ b/local/utl/jwt/jwt.go @@ -22,7 +22,7 @@ type Claims struct { } // init initializes the JWT secret key from environment variable -func init() { +func Init() { jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { log.Fatal("JWT_SECRET environment variable is required and cannot be empty") diff --git a/tests/test_helper.go b/tests/test_helper.go index 0b4ba9c..9b2b07f 100644 --- a/tests/test_helper.go +++ b/tests/test_helper.go @@ -2,6 +2,8 @@ package tests import ( "acc-server-manager/local/model" + "acc-server-manager/local/utl/configs" + "acc-server-manager/local/utl/jwt" "bytes" "context" "errors" @@ -45,8 +47,12 @@ func SetTestEnv() { os.Setenv("APP_SECRET_CODE", "test-code-for-testing-123456789012") os.Setenv("ENCRYPTION_KEY", "12345678901234567890123456789012") os.Setenv("JWT_SECRET", "test-jwt-secret-key-for-testing-123456789012345678901234567890") + os.Setenv("ACCESS_KEY", "test-access-key-for-testing") // Set test-specific environment variables os.Setenv("TESTING_ENV", "true") // Used to bypass + + configs.Init() + jwt.Init() } // NewTestHelper creates a new test helper with in-memory database diff --git a/tests/unit/controller/config_controller_test.go.disabled b/tests/unit/controller/config_controller_test.go.disabled deleted file mode 100644 index b0ba338..0000000 --- a/tests/unit/controller/config_controller_test.go.disabled +++ /dev/null @@ -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 -} diff --git a/tests/unit/controller/controller_simple_test.go b/tests/unit/controller/controller_simple_test.go index 40ab323..c75aec5 100644 --- a/tests/unit/controller/controller_simple_test.go +++ b/tests/unit/controller/controller_simple_test.go @@ -14,6 +14,11 @@ import ( ) 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 app := fiber.New() @@ -55,6 +60,11 @@ func TestController_JSONParsing_Success(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 app := fiber.New() @@ -87,6 +97,11 @@ func TestController_JSONParsing_InvalidJSON(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 app := fiber.New() @@ -123,6 +138,11 @@ func TestController_UUIDValidation_Success(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 app := fiber.New() @@ -157,6 +177,11 @@ func TestController_UUIDValidation_InvalidUUID(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 app := fiber.New() @@ -194,6 +219,11 @@ func TestController_QueryParameters_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 app := fiber.New() @@ -249,6 +279,11 @@ func TestController_HTTPMethods_Success(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 app := fiber.New() @@ -293,6 +328,11 @@ func TestController_ErrorHandling_StatusCodes(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 app := fiber.New() @@ -333,6 +373,11 @@ func TestController_ConfigurationModel_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) app := fiber.New() @@ -370,6 +415,11 @@ func TestController_UserModel_JSONSerialization(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 app := fiber.New() diff --git a/tests/unit/controller/membership_controller_test.go.disabled b/tests/unit/controller/membership_controller_test.go.disabled deleted file mode 100644 index ede98ce..0000000 --- a/tests/unit/controller/membership_controller_test.go.disabled +++ /dev/null @@ -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 -} diff --git a/tests/unit/service/auth_service_test.go.disabled b/tests/unit/service/auth_service_test.go.disabled deleted file mode 100644 index c64e90d..0000000 --- a/tests/unit/service/auth_service_test.go.disabled +++ /dev/null @@ -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 -}