diff --git a/api/server/services/PermissionService.spec.js b/api/server/services/PermissionService.spec.js index 207527ed34..53a0e5e4a8 100644 --- a/api/server/services/PermissionService.spec.js +++ b/api/server/services/PermissionService.spec.js @@ -1,5 +1,5 @@ const mongoose = require('mongoose'); -const { RoleBits } = require('@librechat/data-schemas'); +const { RoleBits, createModels } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { AccessRoleIds, ResourceType } = require('librechat-data-provider'); const { @@ -10,13 +10,13 @@ const { grantPermission, checkPermission, } = require('./PermissionService'); -const { findRoleByIdentifier, getUserPrincipals } = require('~/models'); -const { AclEntry, AccessRole } = require('~/db/models'); +const { findRoleByIdentifier, getUserPrincipals, seedDefaultRoles } = require('~/models'); // Mock the getTransactionSupport function for testing jest.mock('@librechat/data-schemas', () => ({ ...jest.requireActual('@librechat/data-schemas'), getTransactionSupport: jest.fn().mockResolvedValue(false), + createModels: jest.requireActual('@librechat/data-schemas').createModels, })); // Mock GraphApiService to prevent config loading issues @@ -32,11 +32,24 @@ jest.mock('~/config', () => ({ })); let mongoServer; +let AclEntry; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); 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 () => { @@ -45,59 +58,8 @@ afterAll(async () => { }); beforeEach(async () => { - await mongoose.connection.dropDatabase(); - // Seed some roles for testing - 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, - }, - ]); + // Clear test data but keep seeded roles + await AclEntry.deleteMany({}); }); // Mock getUserPrincipals to avoid depending on the actual implementation @@ -227,10 +189,10 @@ describe('PermissionService', () => { principalId: userId, resourceType: ResourceType.AGENT, resourceId, - accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Project role for agent resource + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, // PromptGroup role for agent resource 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 () => { @@ -452,15 +414,15 @@ describe('PermissionService', () => { }); // Setup a resource with inherited permission - const projectId = new mongoose.Types.ObjectId(); + const parentResourceId = new mongoose.Types.ObjectId(); const childResourceId = new mongoose.Types.ObjectId(); await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'project', - resourceId: projectId, - accessRoleId: AccessRoleIds.PROJECT_VIEWER, + resourceType: ResourceType.PROMPTGROUP, + resourceId: parentResourceId, + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: grantedById, }); @@ -474,7 +436,7 @@ describe('PermissionService', () => { roleId: (await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER))._id, grantedBy: grantedById, grantedAt: new Date(), - inheritedFrom: projectId, + inheritedFrom: parentResourceId, }); }); @@ -673,12 +635,12 @@ describe('PermissionService', () => { ); }); - test('should return empty array for non-existent resource type', async () => { - const roles = await getAvailableRoles({ - resourceType: 'non_existent_type', - }); - - expect(roles).toEqual([]); + test('should throw error for non-existent resource type', async () => { + await expect( + getAvailableRoles({ + resourceType: 'non_existent_type', + }), + ).rejects.toThrow('Invalid resourceType: non_existent_type. Valid types: agent, promptGroup'); }); }); @@ -686,6 +648,8 @@ describe('PermissionService', () => { const otherUserId = new mongoose.Types.ObjectId(); beforeEach(async () => { + // Ensure roles are properly seeded + await seedDefaultRoles(); // Setup existing permissions for testing await grantPermission({ principalType: 'user', @@ -906,7 +870,7 @@ describe('PermissionService', () => { { type: 'group', 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 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 () => { @@ -1020,24 +984,24 @@ describe('PermissionService', () => { }); test('should work with different resource types', async () => { - // Test with project resources - const projectResourceId = new mongoose.Types.ObjectId(); + // Test with promptGroup resources + const promptGroupResourceId = new mongoose.Types.ObjectId(); const updatedPrincipals = [ { type: 'user', id: userId, - accessRoleId: AccessRoleIds.PROJECT_VIEWER, + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, }, { type: 'group', id: groupId, - accessRoleId: AccessRoleIds.PROJECT_EDITOR, + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, }, ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'project', - resourceId: projectResourceId, + resourceType: ResourceType.PROMPTGROUP, + resourceId: promptGroupResourceId, updatedPrincipals, grantedBy: grantedById, }); @@ -1048,12 +1012,14 @@ describe('PermissionService', () => { expect(results.errors).toHaveLength(0); // Verify permissions were created with correct resource type - const projectEntries = await AclEntry.find({ - resourceType: 'project', - resourceId: projectResourceId, + const promptGroupEntries = await AclEntry.find({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: promptGroupResourceId, }); - expect(projectEntries).toHaveLength(2); - expect(projectEntries.every((e) => e.resourceType === 'project')).toBe(true); + expect(promptGroupEntries).toHaveLength(2); + expect(promptGroupEntries.every((e) => e.resourceType === ResourceType.PROMPTGROUP)).toBe( + true, + ); }); }); }); diff --git a/client/src/components/Agents/tests/Accessibility.spec.tsx b/client/src/components/Agents/tests/Accessibility.spec.tsx index 768d27471d..37baab47c2 100644 --- a/client/src/components/Agents/tests/Accessibility.spec.tsx +++ b/client/src/components/Agents/tests/Accessibility.spec.tsx @@ -96,16 +96,13 @@ jest.mock('~/hooks/useLocalize', () => ({ jest.mock('~/hooks', () => ({ useLocalize: () => mockLocalize, useDebounce: jest.fn(), + useAgentCategories: jest.fn(), })); jest.mock('~/data-provider/Agents', () => ({ useMarketplaceAgentsInfiniteQuery: jest.fn(), })); -jest.mock('~/hooks/Agents', () => ({ - useAgentCategories: jest.fn(), -})); - // Mock utility functions jest.mock('~/utils/agents', () => ({ renderAgentAvatar: jest.fn(() =>
), @@ -120,8 +117,7 @@ jest.mock('../SmartLoader', () => ({ // Import the actual modules to get the mocked functions import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; -import { useAgentCategories } from '~/hooks/Agents'; -import { useDebounce } from '~/hooks'; +import { useAgentCategories, useDebounce } from '~/hooks'; // Get typed mock functions const mockUseMarketplaceAgentsInfiniteQuery = jest.mocked(useMarketplaceAgentsInfiniteQuery); diff --git a/client/src/components/Agents/tests/AgentDetail.spec.tsx b/client/src/components/Agents/tests/AgentDetail.spec.tsx index b9c1fd7302..2a514a58c1 100644 --- a/client/src/components/Agents/tests/AgentDetail.spec.tsx +++ b/client/src/components/Agents/tests/AgentDetail.spec.tsx @@ -10,7 +10,6 @@ import type t from 'librechat-data-provider'; import { Constants, EModelEndpoint } from 'librechat-data-provider'; import AgentDetail from '../AgentDetail'; -import { useToast } from '~/hooks'; // Mock dependencies jest.mock('react-router-dom', () => ({ @@ -19,11 +18,15 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('~/hooks', () => ({ - useToast: jest.fn(), useMediaQuery: jest.fn(() => false), // Mock as desktop by default useLocalize: jest.fn(), })); +jest.mock('@librechat/client', () => ({ + ...jest.requireActual('@librechat/client'), + useToastContext: jest.fn(), +})); + jest.mock('~/utils/agents', () => ({ renderAgentAvatar: jest.fn((agent, options) => ( @@ -101,7 +104,8 @@ describe('AgentDetail', () => { beforeEach(() => { jest.clearAllMocks(); (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'); (useLocalize as jest.Mock).mockReturnValue(mockLocalize); diff --git a/client/src/components/Agents/tests/SearchBar.spec.tsx b/client/src/components/Agents/tests/SearchBar.spec.tsx index 5977680baf..c7fa163e51 100644 --- a/client/src/components/Agents/tests/SearchBar.spec.tsx +++ b/client/src/components/Agents/tests/SearchBar.spec.tsx @@ -4,12 +4,10 @@ import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import SearchBar from '../SearchBar'; -// Mock useLocalize hook -jest.mock('~/hooks/useLocalize', () => () => (key: string) => key); - -// Mock useDebounce hook +// Mock hooks jest.mock('~/hooks', () => ({ useDebounce: (value: string) => value, // Return value immediately for testing + useLocalize: () => (key: string) => key, })); describe('SearchBar', () => { diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx index 59ef13c114..d861baee9f 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx @@ -274,7 +274,7 @@ describe('AgentFooter', () => { expect(screen.getByTestId('delete-button')).toBeInTheDocument(); expect(screen.queryByTestId('admin-settings')).not.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(); }); @@ -284,8 +284,11 @@ describe('AgentFooter', () => { ); expect(document.querySelector('.spinner')).toBeInTheDocument(); expect(screen.queryByText('Save')).not.toBeInTheDocument(); - expect(screen.getByRole('button')).toBeDisabled(); - expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true'); + // Find the submit button (the one with aria-busy attribute) + const buttons = screen.getAllByRole('button'); + const submitButton = buttons.find((button) => button.getAttribute('type') === 'submit'); + expect(submitButton).toBeDisabled(); + expect(submitButton).toHaveAttribute('aria-busy', 'true'); unmount(); }); @@ -408,7 +411,7 @@ describe('AgentFooter', () => { expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument(); expect(screen.queryByTestId('grant-access-dialog')).not.toBeInTheDocument(); // 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(); }); });