ae1831e61f
- Rename user_id → users_id across all models, handlers, services, and tests
- Add custom RoleIDs type supporting string/int/array unmarshaling (e.g., "1", 1, [1])
- Implement flexible JSON unmarshaling for JWT Claims to handle field name variants
- Support both user_id/users_id and email/email_address field names
- Enable role_id as string ("1"), int (1), or array ([1,2])
- Update AuthorizationContext to handle role_id type flexibility
- Add comprehensive logging to repository, service, and handler layers
- Entry/exit logs with full context
- Success (✓) and failure (✗) indicators
- Step-by-step authorization flow tracking
- Add containsRole helper for multi-role membership checks
- Fix database queries: user_id → users_id, id → permissions_id
- Update all tests to use models.RoleIDs{} syntax
- Change GetRole middleware return type: string → []int
- Maintain backward compatibility with legacy JWT tokens
This change improves integration with external services (MIS) that may send
role_id in different formats and standardizes field naming conventions
throughout the authorization microservice.
378 lines
9.9 KiB
Go
378 lines
9.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"authorization/models"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestInitAuthService(t *testing.T) {
|
|
// Test that InitAuthService can be called
|
|
// It may panic if DB is not available, which is expected behavior
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Logf("InitAuthService panicked (expected without DB): %v", r)
|
|
// This is acceptable - the function requires a DB connection
|
|
}
|
|
}()
|
|
|
|
// This will initialize with whatever DB is available
|
|
// If DB is nil, it will panic which is caught above
|
|
InitAuthService()
|
|
|
|
t.Log("InitAuthService completed successfully")
|
|
}
|
|
|
|
func TestAuthorizeHandlerNoJWTClaims(t *testing.T) {
|
|
// Setup
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Execute
|
|
AuthorizeHandler(w, req)
|
|
|
|
// Assert
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf(ExpectedStatusMessage, http.StatusUnauthorized, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerInvalidJSON(t *testing.T) {
|
|
// Setup - no need to init service, we're testing JSON parsing before auth
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBufferString("invalid json"))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Execute
|
|
AuthorizeHandler(w, req)
|
|
|
|
// Assert
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf(ExpectedStatusMessage, http.StatusBadRequest, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerMissingRequiredFields(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
payload models.AuthorizationContext
|
|
}{
|
|
{
|
|
name: "Missing UserID",
|
|
payload: models.AuthorizationContext{Resource: "document", Action: "read"},
|
|
},
|
|
{
|
|
name: "Missing Resource",
|
|
payload: models.AuthorizationContext{UsersID: "user123", Action: "read"},
|
|
},
|
|
{
|
|
name: "Missing Action",
|
|
payload: models.AuthorizationContext{UsersID: "user123", Resource: "document"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
body, _ := json.Marshal(tc.payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf(ExpectedStatusMessage, http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerUserIDMismatch(t *testing.T) {
|
|
// Setup
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
payload := models.AuthorizationContext{
|
|
UsersID: "differentUser",
|
|
Resource: "document",
|
|
Action: "read",
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Execute
|
|
AuthorizeHandler(w, req)
|
|
|
|
// Assert
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf(ExpectedStatusMessage, http.StatusForbidden, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerNilMaps(t *testing.T) {
|
|
// Test that nil maps don't cause additional panics beyond missing authService
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
payload := models.AuthorizationContext{
|
|
UsersID: "user123",
|
|
Resource: "document",
|
|
Action: "read",
|
|
ResourceData: nil, // nil map
|
|
Environment: nil, // nil map
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Execute - may panic if authService is nil (which is expected without DB)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Logf("Handler panicked (expected without authService): %v", r)
|
|
}
|
|
}()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
// Verify handler set a response code if it didn't panic
|
|
if w.Code != 0 {
|
|
t.Logf("Handler completed with status code: %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Additional comprehensive test cases
|
|
|
|
func TestAuthorizeHandlerEmptyUserID(t *testing.T) {
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
payload := models.AuthorizationContext{
|
|
UsersID: "",
|
|
Resource: "document",
|
|
Action: "read",
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status %d for empty UserID, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerEmptyResource(t *testing.T) {
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
payload := models.AuthorizationContext{
|
|
UsersID: "user123",
|
|
Resource: "",
|
|
Action: "read",
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status %d for empty Resource, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerEmptyAction(t *testing.T) {
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
payload := models.AuthorizationContext{
|
|
UsersID: "user123",
|
|
Resource: "document",
|
|
Action: "",
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status %d for empty Action, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerInvalidClaimsType(t *testing.T) {
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBufferString(`{"userId":"user123","resource":"doc","action":"read"}`))
|
|
|
|
// Set claims as wrong type
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), "invalid_claims_type")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("Expected status %d for invalid claims type, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerMalformedJSON(t *testing.T) {
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
payload string
|
|
}{
|
|
{"Incomplete JSON", `{"userId":"user123","resource":"doc"`},
|
|
{"Invalid quotes", `{userId:"user123"}`},
|
|
{"Trailing comma", `{"userId":"user123",}`},
|
|
{"Just whitespace", ` `},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBufferString(tc.payload))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status %d for malformed JSON, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
userID string
|
|
resource string
|
|
action string
|
|
}{
|
|
{"Special chars in resource", "user123", "document/file-name_v1.2", "read"},
|
|
{"Unicode in resource", "user123", "文档", "read"},
|
|
{"Spaces in action", "user123", "document", "read write"},
|
|
{"Special chars in userID", "user-123_test", "document", "read"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload := models.AuthorizationContext{
|
|
UsersID: tc.userID,
|
|
Resource: tc.resource,
|
|
Action: tc.action,
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
|
|
// Update claims to match userID
|
|
testClaims := &models.Claims{
|
|
UsersID: tc.userID,
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), testClaims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
// May panic if authService is nil (expected without DB)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Logf("Handler panicked (expected without authService): %v", r)
|
|
}
|
|
}()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
// If it didn't panic, verify it set a response status
|
|
if w.Code != 0 {
|
|
t.Logf("Handler completed with status: %d", w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestAuthorizeHandlerWithResourceData(t *testing.T) {
|
|
// Test that ResourceData is properly passed through to authorization
|
|
claims := &models.Claims{
|
|
UsersID: "user123",
|
|
RoleID: models.RoleIDs{1},
|
|
}
|
|
|
|
payload := models.AuthorizationContext{
|
|
UsersID: "user123",
|
|
Resource: "personnel",
|
|
Action: "assign_role",
|
|
ResourceData: map[string]string{
|
|
"region": "01",
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBuffer(body))
|
|
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), claims)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Logf("Handler panicked (expected without DB): %v", r)
|
|
}
|
|
}()
|
|
|
|
AuthorizeHandler(w, req)
|
|
|
|
// ResourceData should not cause any parsing errors
|
|
if w.Code == http.StatusBadRequest {
|
|
t.Errorf("Handler returned bad request with valid ResourceData")
|
|
}
|
|
}
|