From 9b7dcfd86f1d44e2548d86cda821eb1e01d029b6 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 13:28:27 +0300 Subject: [PATCH] tests: add user controller tests --- .../controller/context_controller_test.go | 97 +++--- internal/controller/user_controller_test.go | 283 ++++++++++++++++++ 2 files changed, 328 insertions(+), 52 deletions(-) create mode 100644 internal/controller/user_controller_test.go diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index ecfdf51..61d2e8b 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -11,19 +11,49 @@ import ( "gotest.tools/v3/assert" ) -func TestAppContextHandler(t *testing.T) { - // Setup - controllerCfg := controller.ContextControllerConfig{ - ConfiguredProviders: []string{"github", "google", "generic"}, - Title: "Test App", - GenericName: "Generic", - AppURL: "http://localhost:8080", - RootDomain: "localhost", - ForgotPasswordMessage: "Contact admin to reset your password.", - BackgroundImage: "/assets/bg.jpg", - OAuthAutoRedirect: "google", - } +var controllerCfg = controller.ContextControllerConfig{ + ConfiguredProviders: []string{"github", "google", "generic"}, + Title: "Test App", + GenericName: "Generic", + AppURL: "http://localhost:8080", + RootDomain: "localhost", + ForgotPasswordMessage: "Contact admin to reset your password.", + BackgroundImage: "/assets/bg.jpg", + OAuthAutoRedirect: "google", +} +var userContext = config.UserContext{ + Username: "testuser", + Name: "testuser", + Email: "test@example.com", + IsLoggedIn: true, + OAuth: false, + Provider: "username", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: false, +} + +func setupContextController() (*gin.Engine, *httptest.ResponseRecorder) { + // Setup + gin.SetMode(gin.TestMode) + router := gin.Default() + recorder := httptest.NewRecorder() + + router.Use(func(c *gin.Context) { + c.Set("context", &userContext) + c.Next() + }) + + group := router.Group("/api") + + ctrl := controller.NewContextController(controllerCfg, group) + ctrl.SetupRoutes() + + return router, recorder +} + +func TestAppContextHandler(t *testing.T) { expectedRes := controller.AppContextResponse{ Status: 200, Message: "Success", @@ -37,15 +67,7 @@ func TestAppContextHandler(t *testing.T) { OAuthAutoRedirect: controllerCfg.OAuthAutoRedirect, } - gin.SetMode(gin.TestMode) - router := gin.Default() - group := router.Group("/api") - recorder := httptest.NewRecorder() - - // Test - ctrl := controller.NewContextController(controllerCfg, group) - ctrl.SetupRoutes() - + router, recorder := setupContextController() req := httptest.NewRequest("GET", "/api/context/app", nil) router.ServeHTTP(recorder, req) @@ -59,22 +81,7 @@ func TestAppContextHandler(t *testing.T) { assert.DeepEqual(t, expectedRes, ctrlRes) } -func TestUserContextController(t *testing.T) { - // Setup - controllerCfg := controller.ContextControllerConfig{} - - userContext := config.UserContext{ - Username: "testuser", - Name: "testuser", - Email: "test@example.com", - IsLoggedIn: true, - OAuth: false, - Provider: "username", - TotpPending: false, - OAuthGroups: "", - TotpEnabled: false, - } - +func TestUserContextHandler(t *testing.T) { expectedRes := controller.UserContextResponse{ Status: 200, Message: "Success", @@ -87,21 +94,7 @@ func TestUserContextController(t *testing.T) { TotpPending: userContext.TotpPending, } - gin.SetMode(gin.TestMode) - router := gin.Default() - recorder := httptest.NewRecorder() - - router.Use(func(c *gin.Context) { - c.Set("context", &userContext) - c.Next() - }) - - group := router.Group("/api") - - // Test - ctrl := controller.NewContextController(controllerCfg, group) - ctrl.SetupRoutes() - + router, recorder := setupContextController() req := httptest.NewRequest("GET", "/api/context/user", nil) router.ServeHTTP(recorder, req) diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go new file mode 100644 index 0000000..c22a949 --- /dev/null +++ b/internal/controller/user_controller_test.go @@ -0,0 +1,283 @@ +package controller_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + "tinyauth/internal/config" + "tinyauth/internal/controller" + "tinyauth/internal/service" + + "github.com/gin-gonic/gin" + "github.com/pquerna/otp/totp" + "gotest.tools/v3/assert" +) + +var cookieValue string +var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ" + +func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { + // Setup + gin.SetMode(gin.TestMode) + router := gin.Default() + + if middlewares != nil { + for _, m := range *middlewares { + router.Use(m) + } + } + + group := router.Group("/api") + recorder := httptest.NewRecorder() + + // Database + databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ + DatabasePath: "/tmp/tinyauth_test.db", + }) + + assert.NilError(t, databaseService.Init()) + + database := databaseService.GetDatabase() + + // Auth service + authService := service.NewAuthService(service.AuthServiceConfig{ + Users: []config.User{ + { + Username: "testuser", + Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test + }, + { + Username: "totpuser", + Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test + TotpSecret: totpSecret, + }, + }, + OauthWhitelist: "", + SessionExpiry: 3600, + SecureCookie: false, + RootDomain: "localhost", + LoginTimeout: 300, + LoginMaxRetries: 3, + SessionCookieName: "tinyauth-session", + }, nil, nil, database) + + // Controller + ctrl := controller.NewUserController(controller.UserControllerConfig{ + RootDomain: "localhost", + }, group, authService) + ctrl.SetupRoutes() + + return router, recorder +} + +func TestLoginHandler(t *testing.T) { + // Setup + router, recorder := setupUserController(t, nil) + + loginReq := controller.LoginRequest{ + Username: "testuser", + Password: "test", + } + + loginReqJson, err := json.Marshal(loginReq) + assert.NilError(t, err) + + // Test + req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + cookie := recorder.Result().Cookies()[0] + + assert.Equal(t, "tinyauth-session", cookie.Name) + assert.Assert(t, cookie.Value != "") + + cookieValue = cookie.Value + + // Test invalid credentials + loginReq = controller.LoginRequest{ + Username: "testuser", + Password: "invalid", + } + + loginReqJson, err = json.Marshal(loginReq) + assert.NilError(t, err) + + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) + + // Test totp required + loginReq = controller.LoginRequest{ + Username: "totpuser", + Password: "test", + } + + loginReqJson, err = json.Marshal(loginReq) + assert.NilError(t, err) + + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + loginResJson, err := json.Marshal(map[string]any{ + "message": "TOTP required", + "status": 200, + "totpPending": true, + }) + + assert.NilError(t, err) + assert.Equal(t, string(loginResJson), recorder.Body.String()) + + // Test rate limiting + loginReq = controller.LoginRequest{ + Username: "testuser", + Password: "invalid", + } + + loginReqJson, err = json.Marshal(loginReq) + assert.NilError(t, err) + + for range 5 { + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson))) + router.ServeHTTP(recorder, req) + } + + assert.Equal(t, 429, recorder.Code) +} + +func TestLogoutHandler(t *testing.T) { + // Setup + router, recorder := setupUserController(t, nil) + + // Test + req := httptest.NewRequest("POST", "/api/user/logout", nil) + + req.AddCookie(&http.Cookie{ + Name: "tinyauth-session", + Value: cookieValue, + }) + + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + cookie := recorder.Result().Cookies()[0] + + assert.Equal(t, "tinyauth-session", cookie.Name) + assert.Equal(t, "", cookie.Value) + assert.Equal(t, -1, cookie.MaxAge) +} + +func TestTotpHandler(t *testing.T) { + // Setup + router, recorder := setupUserController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "totpuser", + Email: "totpuser@example.com", + IsLoggedIn: false, + OAuth: false, + Provider: "username", + TotpPending: true, + OAuthGroups: "", + TotpEnabled: true, + }) + c.Next() + }, + }) + + // Test + code, err := totp.GenerateCode(totpSecret, time.Now()) + + assert.NilError(t, err) + + totpReq := controller.TotpRequest{ + Code: code, + } + + totpReqJson, err := json.Marshal(totpReq) + assert.NilError(t, err) + + req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + + cookie := recorder.Result().Cookies()[0] + + assert.Equal(t, "tinyauth-session", cookie.Name) + assert.Assert(t, cookie.Value != "") + + // Test rate limiting + totpReq = controller.TotpRequest{ + Code: "000000", + } + + totpReqJson, err = json.Marshal(totpReq) + assert.NilError(t, err) + + for range 5 { + recorder = httptest.NewRecorder() + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + } + + assert.Equal(t, 429, recorder.Code) + + // Test invalid code + router, recorder = setupUserController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "totpuser", + Email: "totpuser@example.com", + IsLoggedIn: false, + OAuth: false, + Provider: "username", + TotpPending: true, + OAuthGroups: "", + TotpEnabled: true, + }) + c.Next() + }, + }) + + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) + + // Test no totp pending + router, recorder = setupUserController(t, &[]gin.HandlerFunc{ + func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "totpuser", + Email: "totpuser@example.com", + IsLoggedIn: false, + OAuth: false, + Provider: "username", + TotpPending: false, + OAuthGroups: "", + TotpEnabled: false, + }) + c.Next() + }, + }) + + req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 401, recorder.Code) +}