2026-07-16 18:51:58 +08:00

129 lines
4.5 KiB
Go

package middleware
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
"hyapp-admin-server/internal/service"
"github.com/gin-gonic/gin"
)
type fakeAppAccessStore struct {
access repository.AppAccess
}
type fakeAuthorizationStore struct {
authorization repository.UserAuthorization
err error
}
func (store fakeAuthorizationStore) CurrentAuthorizationForUser(uint) (repository.UserAuthorization, error) {
return store.authorization, store.err
}
func (store fakeAppAccessStore) AppAccessForUser(uint) (repository.AppAccess, error) {
return store.access, nil
}
func TestRequireAppScopeAllowsOnlySelectedApp(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(ContextUserID, uint(7))
})
router.GET("/app", RequireAppScope(fakeAppAccessStore{access: repository.AppAccess{
Mode: model.UserAppScopeModeSelected,
AppCodes: []string{"lalu"},
}}), func(c *gin.Context) { c.Status(http.StatusNoContent) })
allowed := httptest.NewRequest(http.MethodGet, "/app", nil)
allowed.Header.Set("X-App-Code", "LALU")
allowedResponse := httptest.NewRecorder()
router.ServeHTTP(allowedResponse, allowed)
if allowedResponse.Code != http.StatusNoContent {
t.Fatalf("allowed status = %d body=%s", allowedResponse.Code, allowedResponse.Body.String())
}
for _, header := range []string{"", "huwaa"} {
request := httptest.NewRequest(http.MethodGet, "/app", nil)
if header != "" {
request.Header.Set("X-App-Code", header)
}
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("header %q status = %d body=%s", header, response.Code, response.Body.String())
}
}
}
func TestAuthRequiredUsesCurrentPermissionsInsteadOfJWTClaim(t *testing.T) {
gin.SetMode(gin.TestMode)
auth := service.NewAuthService("live-authorization-test-secret", time.Hour)
// This models an access token issued before a role migration: finance audit
// existed in the claim, while operations audit was absent at issue time.
token, _, err := auth.GenerateAccessToken(7, "stale-name", []string{"finance-withdrawal:audit"})
if err != nil {
t.Fatalf("generate access token: %v", err)
}
router := gin.New()
router.Use(AuthRequired(auth, fakeAuthorizationStore{authorization: repository.UserAuthorization{
Username: "current-name",
Status: model.UserStatusActive,
Permissions: []string{"operations-withdrawal:audit"},
}}))
router.GET("/finance", RequirePermission("finance-withdrawal:audit"), func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
router.GET("/operations", RequirePermission("operations-withdrawal:audit"), func(c *gin.Context) {
if CurrentUsername(c) != "current-name" {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusNoContent)
})
financeRequest := httptest.NewRequest(http.MethodGet, "/finance", nil)
financeRequest.Header.Set("Authorization", "Bearer "+token)
financeResponse := httptest.NewRecorder()
router.ServeHTTP(financeResponse, financeRequest)
if financeResponse.Code != http.StatusForbidden {
t.Fatalf("removed JWT permission status = %d body=%s", financeResponse.Code, financeResponse.Body.String())
}
operationsRequest := httptest.NewRequest(http.MethodGet, "/operations", nil)
operationsRequest.Header.Set("Authorization", "Bearer "+token)
operationsResponse := httptest.NewRecorder()
router.ServeHTTP(operationsResponse, operationsRequest)
if operationsResponse.Code != http.StatusNoContent {
t.Fatalf("current database permission status = %d body=%s", operationsResponse.Code, operationsResponse.Body.String())
}
}
func TestAuthRequiredFailsClosedWhenAuthorizationCannotBeResolved(t *testing.T) {
gin.SetMode(gin.TestMode)
auth := service.NewAuthService("authorization-failure-test-secret", time.Hour)
token, _, err := auth.GenerateAccessToken(7, "admin", []string{"role:manage"})
if err != nil {
t.Fatalf("generate access token: %v", err)
}
router := gin.New()
router.Use(AuthRequired(auth, fakeAuthorizationStore{err: errors.New("database unavailable")}))
router.GET("/protected", func(c *gin.Context) { c.Status(http.StatusNoContent) })
request := httptest.NewRequest(http.MethodGet, "/protected", nil)
request.Header.Set("Authorization", "Bearer "+token)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusInternalServerError {
t.Fatalf("authorization database failure status = %d body=%s", response.Code, response.Body.String())
}
}