package middleware import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // MockAuthProvider is a mock for AuthProvider interface type MockAuthProvider struct { mock.Mock } func (m *MockAuthProvider) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) { args := m.Called(c) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.UserProfileResponse), args.Error(1) } // MockKetoService is a mock for KetoService interface type MockKetoService struct { mock.Mock } func (m *MockKetoService) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) { args := m.Called(ctx, subject, namespace, object, relation) return args.Bool(0), args.Error(1) } func (m *MockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error { args := m.Called(ctx, namespace, object, relation, subject) return args.Error(0) } func (m *MockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error { args := m.Called(ctx, namespace, object, relation, subject) return args.Error(0) } func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) { args := m.Called(ctx, namespace, object, relation, subject) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]service.RelationTuple), args.Error(1) } func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) { args := m.Called(ctx, namespace, relation, subject) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]string), args.Error(1) } func TestRequireRole_Success(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AllowedRoles: []string{domain.RoleSuperAdmin}, AuthHandler: mockAuth, } mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{ ID: "user1", Role: domain.RoleSuperAdmin, }, nil) app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/test", nil) resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) } func TestRequireRole_SetsUserIDForAuditContext(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AllowedRoles: []string{domain.RoleSuperAdmin}, AuthHandler: mockAuth, } mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{ ID: "user1", Role: domain.RoleSuperAdmin, }, nil) app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "user_id": c.Locals("user_id"), }) }) req := httptest.NewRequest("GET", "/test", nil) resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) var body map[string]string assert.NoError(t, readJSON(resp, &body)) assert.Equal(t, "user1", body["user_id"]) } func TestRequireRole_PreservesExistingUserID(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AllowedRoles: []string{domain.RoleSuperAdmin}, AuthHandler: mockAuth, } mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{ ID: "profile-user", Role: domain.RoleSuperAdmin, }, nil) app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", "existing-user") return c.Next() }) app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "user_id": c.Locals("user_id"), }) }) req := httptest.NewRequest("GET", "/test", nil) resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) var body map[string]string assert.NoError(t, readJSON(resp, &body)) assert.Equal(t, "existing-user", body["user_id"]) } func TestRequireRole_Forbidden(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AllowedRoles: []string{domain.RoleSuperAdmin}, AuthHandler: mockAuth, } mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{ ID: "user1", Role: "user", }, nil) app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/test", nil) resp, _ := app.Test(req) assert.Equal(t, 403, resp.StatusCode) } func TestRequireKetoPermission_Success(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) mockKeto := new(MockKetoService) config := RBACConfig{ AuthHandler: mockAuth, KetoService: mockKeto, } profile := &domain.UserProfileResponse{ID: "user1", Role: "user"} mockAuth.On("GetEnrichedProfile", mock.Anything).Return(profile, nil) mockKeto.On("CheckPermission", mock.Anything, "User:user1", "tenants", "tenant1", "read").Return(true, nil) app.Get("/tenants/:id", RequireKetoPermission(config, "tenants", "read"), func(c *fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/tenants/tenant1", nil) resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) } func TestRequireTenantMatch_SuperAdmin(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AuthHandler: mockAuth, } mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{ ID: "admin1", Role: domain.RoleSuperAdmin, }, nil) app.Get("/tenants/:tenantId/data", RequireTenantMatch(config), func(c *fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/tenants/any-tenant/data", nil) resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) } func TestRequireTenantMatch_Forbidden(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AuthHandler: mockAuth, } tenant1 := "tenant1" mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{ ID: "user1", Role: "user", // Formerly tenant_admin, now mapped to user which is forbidden here for non-superadmin TenantID: &tenant1, }, nil) app.Get("/tenants/:tenantId/data", RequireTenantMatch(config), func(c *fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/tenants/tenant2/data", nil) resp, _ := app.Test(req) assert.Equal(t, 403, resp.StatusCode) } func TestRequireRole_Unauthorized(t *testing.T) { app := fiber.New() mockAuth := new(MockAuthProvider) config := RBACConfig{ AuthHandler: mockAuth, } mockAuth.On("GetEnrichedProfile", mock.Anything).Return(nil, errors.New("unauthorized")) app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/test", nil) resp, _ := app.Test(req) assert.Equal(t, 401, resp.StatusCode) } func readJSON(resp *http.Response, target any) error { defer resp.Body.Close() return json.NewDecoder(resp.Body).Decode(target) }