mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🧪 ci: Update PermissionService tests for PromptGroup resource type
- Refactor tests to use PromptGroup roles instead of Project roles. - Initialize models and seed default roles in test setup. - Update error handling for non-existent resource types. - Ensure proper cleanup of test data while retaining seeded roles.
This commit is contained in:
parent
fc8fd489d6
commit
90b037a67f
5 changed files with 66 additions and 99 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const { RoleBits } = require('@librechat/data-schemas');
|
const { RoleBits, createModels } = require('@librechat/data-schemas');
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
|
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
|
|
@ -10,13 +10,13 @@ const {
|
||||||
grantPermission,
|
grantPermission,
|
||||||
checkPermission,
|
checkPermission,
|
||||||
} = require('./PermissionService');
|
} = require('./PermissionService');
|
||||||
const { findRoleByIdentifier, getUserPrincipals } = require('~/models');
|
const { findRoleByIdentifier, getUserPrincipals, seedDefaultRoles } = require('~/models');
|
||||||
const { AclEntry, AccessRole } = require('~/db/models');
|
|
||||||
|
|
||||||
// Mock the getTransactionSupport function for testing
|
// Mock the getTransactionSupport function for testing
|
||||||
jest.mock('@librechat/data-schemas', () => ({
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
...jest.requireActual('@librechat/data-schemas'),
|
...jest.requireActual('@librechat/data-schemas'),
|
||||||
getTransactionSupport: jest.fn().mockResolvedValue(false),
|
getTransactionSupport: jest.fn().mockResolvedValue(false),
|
||||||
|
createModels: jest.requireActual('@librechat/data-schemas').createModels,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock GraphApiService to prevent config loading issues
|
// Mock GraphApiService to prevent config loading issues
|
||||||
|
|
@ -32,11 +32,24 @@ jest.mock('~/config', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mongoServer;
|
let mongoServer;
|
||||||
|
let AclEntry;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mongoServer = await MongoMemoryServer.create();
|
mongoServer = await MongoMemoryServer.create();
|
||||||
const mongoUri = mongoServer.getUri();
|
const mongoUri = mongoServer.getUri();
|
||||||
await mongoose.connect(mongoUri);
|
await mongoose.connect(mongoUri);
|
||||||
|
|
||||||
|
// Initialize all models
|
||||||
|
createModels(mongoose);
|
||||||
|
|
||||||
|
// Register models on mongoose.models so methods can access them
|
||||||
|
const dbModels = require('~/db/models');
|
||||||
|
Object.assign(mongoose.models, dbModels);
|
||||||
|
|
||||||
|
AclEntry = dbModels.AclEntry;
|
||||||
|
|
||||||
|
// Seed default roles
|
||||||
|
await seedDefaultRoles();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
@ -45,59 +58,8 @@ afterAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await mongoose.connection.dropDatabase();
|
// Clear test data but keep seeded roles
|
||||||
// Seed some roles for testing
|
await AclEntry.deleteMany({});
|
||||||
await AccessRole.create([
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
|
||||||
name: 'Agent Viewer',
|
|
||||||
description: 'Can view agents',
|
|
||||||
resourceType: ResourceType.AGENT,
|
|
||||||
permBits: RoleBits.VIEWER, // VIEW permission
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
|
||||||
name: 'Agent Editor',
|
|
||||||
description: 'Can edit agents',
|
|
||||||
resourceType: ResourceType.AGENT,
|
|
||||||
permBits: RoleBits.EDITOR, // VIEW + EDIT permissions
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
||||||
name: 'Agent Owner',
|
|
||||||
description: 'Full control over agents',
|
|
||||||
resourceType: ResourceType.AGENT,
|
|
||||||
permBits: RoleBits.OWNER, // VIEW + EDIT + DELETE + SHARE permissions
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.PROJECT_VIEWER,
|
|
||||||
name: 'Project Viewer',
|
|
||||||
description: 'Can view projects',
|
|
||||||
resourceType: 'project',
|
|
||||||
permBits: RoleBits.VIEWER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.PROJECT_EDITOR,
|
|
||||||
name: 'Project Editor',
|
|
||||||
description: 'Can edit projects',
|
|
||||||
resourceType: 'project',
|
|
||||||
permBits: RoleBits.EDITOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.PROJECT_MANAGER,
|
|
||||||
name: 'Project Manager',
|
|
||||||
description: 'Can manage projects',
|
|
||||||
resourceType: 'project',
|
|
||||||
permBits: RoleBits.MANAGER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessRoleId: AccessRoleIds.PROJECT_OWNER,
|
|
||||||
name: 'Project Owner',
|
|
||||||
description: 'Full control over projects',
|
|
||||||
resourceType: 'project',
|
|
||||||
permBits: RoleBits.OWNER,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock getUserPrincipals to avoid depending on the actual implementation
|
// Mock getUserPrincipals to avoid depending on the actual implementation
|
||||||
|
|
@ -227,10 +189,10 @@ describe('PermissionService', () => {
|
||||||
principalId: userId,
|
principalId: userId,
|
||||||
resourceType: ResourceType.AGENT,
|
resourceType: ResourceType.AGENT,
|
||||||
resourceId,
|
resourceId,
|
||||||
accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Project role for agent resource
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, // PromptGroup role for agent resource
|
||||||
grantedBy: grantedById,
|
grantedBy: grantedById,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow('Role project_viewer is for project resources, not agent');
|
).rejects.toThrow('Role promptGroup_viewer is for promptGroup resources, not agent');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update existing permission when granting to same principal and resource', async () => {
|
test('should update existing permission when granting to same principal and resource', async () => {
|
||||||
|
|
@ -452,15 +414,15 @@ describe('PermissionService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup a resource with inherited permission
|
// Setup a resource with inherited permission
|
||||||
const projectId = new mongoose.Types.ObjectId();
|
const parentResourceId = new mongoose.Types.ObjectId();
|
||||||
const childResourceId = new mongoose.Types.ObjectId();
|
const childResourceId = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
await grantPermission({
|
await grantPermission({
|
||||||
principalType: 'user',
|
principalType: 'user',
|
||||||
principalId: userId,
|
principalId: userId,
|
||||||
resourceType: 'project',
|
resourceType: ResourceType.PROMPTGROUP,
|
||||||
resourceId: projectId,
|
resourceId: parentResourceId,
|
||||||
accessRoleId: AccessRoleIds.PROJECT_VIEWER,
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
||||||
grantedBy: grantedById,
|
grantedBy: grantedById,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -474,7 +436,7 @@ describe('PermissionService', () => {
|
||||||
roleId: (await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER))._id,
|
roleId: (await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER))._id,
|
||||||
grantedBy: grantedById,
|
grantedBy: grantedById,
|
||||||
grantedAt: new Date(),
|
grantedAt: new Date(),
|
||||||
inheritedFrom: projectId,
|
inheritedFrom: parentResourceId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -673,12 +635,12 @@ describe('PermissionService', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return empty array for non-existent resource type', async () => {
|
test('should throw error for non-existent resource type', async () => {
|
||||||
const roles = await getAvailableRoles({
|
await expect(
|
||||||
resourceType: 'non_existent_type',
|
getAvailableRoles({
|
||||||
});
|
resourceType: 'non_existent_type',
|
||||||
|
}),
|
||||||
expect(roles).toEqual([]);
|
).rejects.toThrow('Invalid resourceType: non_existent_type. Valid types: agent, promptGroup');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -686,6 +648,8 @@ describe('PermissionService', () => {
|
||||||
const otherUserId = new mongoose.Types.ObjectId();
|
const otherUserId = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// Ensure roles are properly seeded
|
||||||
|
await seedDefaultRoles();
|
||||||
// Setup existing permissions for testing
|
// Setup existing permissions for testing
|
||||||
await grantPermission({
|
await grantPermission({
|
||||||
principalType: 'user',
|
principalType: 'user',
|
||||||
|
|
@ -906,7 +870,7 @@ describe('PermissionService', () => {
|
||||||
{
|
{
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: groupId,
|
id: groupId,
|
||||||
accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Wrong resource type
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, // Wrong resource type
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -924,7 +888,7 @@ describe('PermissionService', () => {
|
||||||
|
|
||||||
// Check error details
|
// Check error details
|
||||||
expect(results.errors[0].error).toContain('Role non_existent_role not found');
|
expect(results.errors[0].error).toContain('Role non_existent_role not found');
|
||||||
expect(results.errors[1].error).toContain('Role project_viewer not found');
|
expect(results.errors[1].error).toContain('Role promptGroup_viewer not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle empty arrays (no operations)', async () => {
|
test('should handle empty arrays (no operations)', async () => {
|
||||||
|
|
@ -1020,24 +984,24 @@ describe('PermissionService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work with different resource types', async () => {
|
test('should work with different resource types', async () => {
|
||||||
// Test with project resources
|
// Test with promptGroup resources
|
||||||
const projectResourceId = new mongoose.Types.ObjectId();
|
const promptGroupResourceId = new mongoose.Types.ObjectId();
|
||||||
const updatedPrincipals = [
|
const updatedPrincipals = [
|
||||||
{
|
{
|
||||||
type: 'user',
|
type: 'user',
|
||||||
id: userId,
|
id: userId,
|
||||||
accessRoleId: AccessRoleIds.PROJECT_VIEWER,
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: groupId,
|
id: groupId,
|
||||||
accessRoleId: AccessRoleIds.PROJECT_EDITOR,
|
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await bulkUpdateResourcePermissions({
|
const results = await bulkUpdateResourcePermissions({
|
||||||
resourceType: 'project',
|
resourceType: ResourceType.PROMPTGROUP,
|
||||||
resourceId: projectResourceId,
|
resourceId: promptGroupResourceId,
|
||||||
updatedPrincipals,
|
updatedPrincipals,
|
||||||
grantedBy: grantedById,
|
grantedBy: grantedById,
|
||||||
});
|
});
|
||||||
|
|
@ -1048,12 +1012,14 @@ describe('PermissionService', () => {
|
||||||
expect(results.errors).toHaveLength(0);
|
expect(results.errors).toHaveLength(0);
|
||||||
|
|
||||||
// Verify permissions were created with correct resource type
|
// Verify permissions were created with correct resource type
|
||||||
const projectEntries = await AclEntry.find({
|
const promptGroupEntries = await AclEntry.find({
|
||||||
resourceType: 'project',
|
resourceType: ResourceType.PROMPTGROUP,
|
||||||
resourceId: projectResourceId,
|
resourceId: promptGroupResourceId,
|
||||||
});
|
});
|
||||||
expect(projectEntries).toHaveLength(2);
|
expect(promptGroupEntries).toHaveLength(2);
|
||||||
expect(projectEntries.every((e) => e.resourceType === 'project')).toBe(true);
|
expect(promptGroupEntries.every((e) => e.resourceType === ResourceType.PROMPTGROUP)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -96,16 +96,13 @@ jest.mock('~/hooks/useLocalize', () => ({
|
||||||
jest.mock('~/hooks', () => ({
|
jest.mock('~/hooks', () => ({
|
||||||
useLocalize: () => mockLocalize,
|
useLocalize: () => mockLocalize,
|
||||||
useDebounce: jest.fn(),
|
useDebounce: jest.fn(),
|
||||||
|
useAgentCategories: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/data-provider/Agents', () => ({
|
jest.mock('~/data-provider/Agents', () => ({
|
||||||
useMarketplaceAgentsInfiniteQuery: jest.fn(),
|
useMarketplaceAgentsInfiniteQuery: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/hooks/Agents', () => ({
|
|
||||||
useAgentCategories: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock utility functions
|
// Mock utility functions
|
||||||
jest.mock('~/utils/agents', () => ({
|
jest.mock('~/utils/agents', () => ({
|
||||||
renderAgentAvatar: jest.fn(() => <div data-testid="agent-avatar" />),
|
renderAgentAvatar: jest.fn(() => <div data-testid="agent-avatar" />),
|
||||||
|
|
@ -120,8 +117,7 @@ jest.mock('../SmartLoader', () => ({
|
||||||
|
|
||||||
// Import the actual modules to get the mocked functions
|
// Import the actual modules to get the mocked functions
|
||||||
import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
|
import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
|
||||||
import { useAgentCategories } from '~/hooks/Agents';
|
import { useAgentCategories, useDebounce } from '~/hooks';
|
||||||
import { useDebounce } from '~/hooks';
|
|
||||||
|
|
||||||
// Get typed mock functions
|
// Get typed mock functions
|
||||||
const mockUseMarketplaceAgentsInfiniteQuery = jest.mocked(useMarketplaceAgentsInfiniteQuery);
|
const mockUseMarketplaceAgentsInfiniteQuery = jest.mocked(useMarketplaceAgentsInfiniteQuery);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import type t from 'librechat-data-provider';
|
||||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
|
||||||
import AgentDetail from '../AgentDetail';
|
import AgentDetail from '../AgentDetail';
|
||||||
import { useToast } from '~/hooks';
|
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
|
|
@ -19,11 +18,15 @@ jest.mock('react-router-dom', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/hooks', () => ({
|
jest.mock('~/hooks', () => ({
|
||||||
useToast: jest.fn(),
|
|
||||||
useMediaQuery: jest.fn(() => false), // Mock as desktop by default
|
useMediaQuery: jest.fn(() => false), // Mock as desktop by default
|
||||||
useLocalize: jest.fn(),
|
useLocalize: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('@librechat/client', () => ({
|
||||||
|
...jest.requireActual('@librechat/client'),
|
||||||
|
useToastContext: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('~/utils/agents', () => ({
|
jest.mock('~/utils/agents', () => ({
|
||||||
renderAgentAvatar: jest.fn((agent, options) => (
|
renderAgentAvatar: jest.fn((agent, options) => (
|
||||||
<div data-testid="agent-avatar" data-size={options?.size} />
|
<div data-testid="agent-avatar" data-size={options?.size} />
|
||||||
|
|
@ -101,7 +104,8 @@ describe('AgentDetail', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
|
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
|
||||||
(useToast as jest.Mock).mockReturnValue({ showToast: mockShowToast });
|
const { useToastContext } = require('@librechat/client');
|
||||||
|
(useToastContext as jest.Mock).mockReturnValue({ showToast: mockShowToast });
|
||||||
const { useLocalize } = require('~/hooks');
|
const { useLocalize } = require('~/hooks');
|
||||||
(useLocalize as jest.Mock).mockReturnValue(mockLocalize);
|
(useLocalize as jest.Mock).mockReturnValue(mockLocalize);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,10 @@ import userEvent from '@testing-library/user-event';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import SearchBar from '../SearchBar';
|
import SearchBar from '../SearchBar';
|
||||||
|
|
||||||
// Mock useLocalize hook
|
// Mock hooks
|
||||||
jest.mock('~/hooks/useLocalize', () => () => (key: string) => key);
|
|
||||||
|
|
||||||
// Mock useDebounce hook
|
|
||||||
jest.mock('~/hooks', () => ({
|
jest.mock('~/hooks', () => ({
|
||||||
useDebounce: (value: string) => value, // Return value immediately for testing
|
useDebounce: (value: string) => value, // Return value immediately for testing
|
||||||
|
useLocalize: () => (key: string) => key,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('SearchBar', () => {
|
describe('SearchBar', () => {
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ describe('AgentFooter', () => {
|
||||||
expect(screen.getByTestId('delete-button')).toBeInTheDocument();
|
expect(screen.getByTestId('delete-button')).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('admin-settings')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('admin-settings')).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId('grant-access-dialog')).toBeInTheDocument();
|
expect(screen.getByTestId('grant-access-dialog')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('duplicate-agent')).toBeInTheDocument();
|
expect(screen.getByTestId('duplicate-button')).toBeInTheDocument();
|
||||||
expect(document.querySelector('.spinner')).not.toBeInTheDocument();
|
expect(document.querySelector('.spinner')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -284,8 +284,11 @@ describe('AgentFooter', () => {
|
||||||
);
|
);
|
||||||
expect(document.querySelector('.spinner')).toBeInTheDocument();
|
expect(document.querySelector('.spinner')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Save')).not.toBeInTheDocument();
|
expect(screen.queryByText('Save')).not.toBeInTheDocument();
|
||||||
expect(screen.getByRole('button')).toBeDisabled();
|
// Find the submit button (the one with aria-busy attribute)
|
||||||
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
|
const buttons = screen.getAllByRole('button');
|
||||||
|
const submitButton = buttons.find((button) => button.getAttribute('type') === 'submit');
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
expect(submitButton).toHaveAttribute('aria-busy', 'true');
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -408,7 +411,7 @@ describe('AgentFooter', () => {
|
||||||
expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('grant-access-dialog')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('grant-access-dialog')).not.toBeInTheDocument();
|
||||||
// Duplicate button should still show as it doesn't depend on permissions loading
|
// Duplicate button should still show as it doesn't depend on permissions loading
|
||||||
expect(screen.getByTestId('duplicate-agent')).toBeInTheDocument();
|
expect(screen.getByTestId('duplicate-button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue