package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/config" "github.com/treytartt/honeydue-api/internal/models" ) func TestGetAuthUser_NilContext_ReturnsNil(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) // No user set in context user := GetAuthUser(c) assert.Nil(t, user) } func TestGetAuthUser_WrongType_ReturnsNil(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) // Set wrong type in context — should NOT panic c.Set(AuthUserKey, "not-a-user") user := GetAuthUser(c) assert.Nil(t, user) } func TestGetAuthUser_ValidUser_ReturnsUser(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) expected := &models.User{Username: "testuser"} c.Set(AuthUserKey, expected) user := GetAuthUser(c) require.NotNil(t, user) assert.Equal(t, "testuser", user.Username) } func TestMustGetAuthUser_Nil_Returns401(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) user, err := MustGetAuthUser(c) assert.Nil(t, user) assert.Error(t, err) } func TestMustGetAuthUser_WrongType_Returns401(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.Set(AuthUserKey, 12345) user, err := MustGetAuthUser(c) assert.Nil(t, user) assert.Error(t, err) } func TestTokenTruncation_ShortToken_NoPanic(t *testing.T) { // Ensure truncateToken does not panic on short tokens assert.NotPanics(t, func() { result := truncateToken("ab") assert.Equal(t, "ab...", result) }) } func TestTokenTruncation_EmptyToken_NoPanic(t *testing.T) { assert.NotPanics(t, func() { result := truncateToken("") assert.Equal(t, "...", result) }) } func TestTokenTruncation_LongToken_Truncated(t *testing.T) { result := truncateToken("abcdefghijklmnop") assert.Equal(t, "abcdefgh...", result) } func TestAdminAuth_QueryParamToken_Rejected(t *testing.T) { // SEC-20: Admin JWT via query parameter must be rejected. // Tokens in URLs leak into server logs and browser history. cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } mw := AdminAuthMiddleware(cfg, nil) handler := mw(func(c echo.Context) error { return c.String(http.StatusOK, "should not reach here") }) e := echo.New() // Request with token only in query param, no Authorization header req := httptest.NewRequest(http.MethodGet, "/admin/test?token=some-jwt-token", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.NoError(t, err) // handler writes JSON directly, no Echo error assert.Equal(t, http.StatusUnauthorized, rec.Code, "query param token must be rejected") assert.Contains(t, rec.Body.String(), "Authorization required") } // requireVerifiedContext builds an Echo context primed as the Authenticate // middleware would leave it: an auth_user and the verified flag. func requireVerifiedContext(user *models.User, verified bool) (echo.Context, *httptest.ResponseRecorder) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/api/residences/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) if user != nil { c.Set(AuthUserKey, user) } c.Set(AuthVerifiedKey, verified) return c, rec } // TestRequireVerified_VerifiedUser_Passes confirms a verified user reaches the // wrapped handler. This is the default tier for all app-data routes now that // RequireVerified is applied at the `verified` group level in the router. func TestRequireVerified_VerifiedUser_Passes(t *testing.T) { m := &KratosAuth{} c, _ := requireVerifiedContext(&models.User{Username: "v"}, true) reached := false handler := m.RequireVerified()(func(c echo.Context) error { reached = true return c.NoContent(http.StatusOK) }) err := handler(c) assert.NoError(t, err) assert.True(t, reached, "verified user should reach the handler") } // TestRequireVerified_UnverifiedUser_403 is the core gating assertion for the // new policy: an authenticated-but-unverified user is rejected with 403 on a // data route, NOT allowed through. func TestRequireVerified_UnverifiedUser_403(t *testing.T) { m := &KratosAuth{} c, _ := requireVerifiedContext(&models.User{Username: "u"}, false) reached := false handler := m.RequireVerified()(func(c echo.Context) error { reached = true return c.NoContent(http.StatusOK) }) err := handler(c) require.Error(t, err) assert.False(t, reached, "unverified user must NOT reach the handler") var appErr *apperrors.AppError require.ErrorAs(t, err, &appErr) assert.Equal(t, http.StatusForbidden, appErr.Code) } // TestRequireVerified_NoUser_401 confirms RequireVerified rejects an // unauthenticated request with 401 (defense-in-depth even though Authenticate // runs first in the router). func TestRequireVerified_NoUser_401(t *testing.T) { m := &KratosAuth{} c, _ := requireVerifiedContext(nil, false) handler := m.RequireVerified()(func(c echo.Context) error { return c.NoContent(http.StatusOK) }) err := handler(c) require.Error(t, err) var appErr *apperrors.AppError require.ErrorAs(t, err, &appErr) assert.Equal(t, http.StatusUnauthorized, appErr.Code) }