mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-29 22:58:51 +01:00
1085 lines
36 KiB
TypeScript
1085 lines
36 KiB
TypeScript
|
|
import mongoose, { Types, Model } from 'mongoose';
|
||
|
|
import { createModels, createMethods, RoleBits } from '@librechat/data-schemas';
|
||
|
|
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||
|
|
import { ResourceType, AccessRoleIds, PrincipalType } from 'librechat-data-provider';
|
||
|
|
import { AccessControlService } from './accessControlService';
|
||
|
|
|
||
|
|
// Mock the logger
|
||
|
|
jest.mock('@librechat/data-schemas', () => ({
|
||
|
|
...jest.requireActual('@librechat/data-schemas'),
|
||
|
|
logger: {
|
||
|
|
error: jest.fn(),
|
||
|
|
warn: jest.fn(),
|
||
|
|
debug: jest.fn(),
|
||
|
|
info: jest.fn(),
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
let mongoServer: MongoMemoryServer;
|
||
|
|
let AclEntry: Model<unknown>;
|
||
|
|
let service: AccessControlService;
|
||
|
|
let dbMethods: ReturnType<typeof createMethods>;
|
||
|
|
|
||
|
|
// Mock getUserPrincipals to control test scenarios
|
||
|
|
const mockGetUserPrincipals = jest.fn();
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
mongoServer = await MongoMemoryServer.create();
|
||
|
|
const mongoUri = mongoServer.getUri();
|
||
|
|
await mongoose.connect(mongoUri);
|
||
|
|
|
||
|
|
// Initialize all models
|
||
|
|
createModels(mongoose);
|
||
|
|
|
||
|
|
AclEntry = mongoose.models.AclEntry;
|
||
|
|
|
||
|
|
// Create methods and seed default roles
|
||
|
|
dbMethods = createMethods(mongoose);
|
||
|
|
await dbMethods.seedDefaultRoles();
|
||
|
|
|
||
|
|
// Create service instance
|
||
|
|
service = new AccessControlService(mongoose);
|
||
|
|
|
||
|
|
// Mock getUserPrincipals in the dbMethods
|
||
|
|
const originalMethods = service['_dbMethods'];
|
||
|
|
service['_dbMethods'] = {
|
||
|
|
...originalMethods,
|
||
|
|
getUserPrincipals: mockGetUserPrincipals,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await mongoose.disconnect();
|
||
|
|
await mongoServer.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Clear test data but keep seeded roles
|
||
|
|
await AclEntry.deleteMany({});
|
||
|
|
mockGetUserPrincipals.mockReset();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('AccessControlService', () => {
|
||
|
|
// Common test data
|
||
|
|
const userId = new Types.ObjectId();
|
||
|
|
const groupId = new Types.ObjectId();
|
||
|
|
const resourceId = new Types.ObjectId();
|
||
|
|
const grantedById = new Types.ObjectId();
|
||
|
|
|
||
|
|
describe('grantPermission', () => {
|
||
|
|
describe('validation', () => {
|
||
|
|
test('should throw error for invalid principal type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: 'invalid' as PrincipalType,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid principal type: invalid');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for missing principalId with user type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Principal ID is required for user, group, and role principals');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for missing principalId with group type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Principal ID is required for user, group, and role principals');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for missing principalId with role type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Principal ID is required for user, group, and role principals');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for invalid role ID (empty string)', async () => {
|
||
|
|
// Empty string is falsy, so it triggers the "principalId required" check first
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: '',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Principal ID is required for user, group, and role principals');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for invalid role ID (whitespace only)', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: ' ',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid role ID:');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for invalid user principal ID (non-ObjectId)', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: 'invalid-id',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid principal ID: invalid-id');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for invalid resource ID', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: 'invalid-id',
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resource ID: invalid-id');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for invalid resource type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: 'invalidType' as ResourceType,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resourceType: invalidType');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('role lookup', () => {
|
||
|
|
test('should throw error for non-existent role', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: 'non_existent_role' as AccessRoleIds,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Role non_existent_role not found');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for role-resource type mismatch', async () => {
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, // PromptGroup role for agent resource
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Role promptGroup_viewer is for promptGroup resources, not agent');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('successful grant', () => {
|
||
|
|
test('should grant permission to a user with a role', async () => {
|
||
|
|
const entry = await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(entry).toBeDefined();
|
||
|
|
expect(entry!.principalType).toBe(PrincipalType.USER);
|
||
|
|
expect(entry!.principalId!.toString()).toBe(userId.toString());
|
||
|
|
expect(entry!.resourceType).toBe(ResourceType.AGENT);
|
||
|
|
expect(entry!.resourceId.toString()).toBe(resourceId.toString());
|
||
|
|
expect(entry!.permBits).toBe(RoleBits.VIEWER);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should grant permission to a group with a role', async () => {
|
||
|
|
const entry = await service.grantPermission({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: groupId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(entry).toBeDefined();
|
||
|
|
expect(entry!.principalType).toBe(PrincipalType.GROUP);
|
||
|
|
expect(entry!.principalId!.toString()).toBe(groupId.toString());
|
||
|
|
expect(entry!.permBits).toBe(RoleBits.EDITOR);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should grant public permission with a role', async () => {
|
||
|
|
const entry = await service.grantPermission({
|
||
|
|
principalType: PrincipalType.PUBLIC,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(entry).toBeDefined();
|
||
|
|
expect(entry!.principalType).toBe(PrincipalType.PUBLIC);
|
||
|
|
expect(entry!.principalId).toBeUndefined();
|
||
|
|
expect(entry!.permBits).toBe(RoleBits.VIEWER);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should grant permission to a role principal', async () => {
|
||
|
|
const entry = await service.grantPermission({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: 'admin',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(entry).toBeDefined();
|
||
|
|
expect(entry!.principalType).toBe(PrincipalType.ROLE);
|
||
|
|
expect(entry!.principalId).toBe('admin');
|
||
|
|
expect(entry!.permBits).toBe(RoleBits.EDITOR);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should update existing permission when granting to same principal and resource', async () => {
|
||
|
|
// First grant with viewer role
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Then update to editor role
|
||
|
|
const updated = await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(updated!.permBits).toBe(RoleBits.EDITOR);
|
||
|
|
|
||
|
|
// Verify there's only one entry
|
||
|
|
const entries = await AclEntry.find({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId,
|
||
|
|
});
|
||
|
|
expect(entries).toHaveLength(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('findAccessibleResources', () => {
|
||
|
|
const resource1 = new Types.ObjectId();
|
||
|
|
const resource2 = new Types.ObjectId();
|
||
|
|
const resource3 = new Types.ObjectId();
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// User can view resource 1
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resource1,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// User can edit resource 2
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resource2,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Group can view resource 3
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: groupId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resource3,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validation errors', () => {
|
||
|
|
test('should throw error when requiredPermissions is not a positive number', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 0,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('requiredPermissions must be a positive number');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error when requiredPermissions is negative', async () => {
|
||
|
|
await expect(
|
||
|
|
service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: -1,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('requiredPermissions must be a positive number');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return empty array for invalid resource type (error is caught)', async () => {
|
||
|
|
// The service catches invalid resourceType errors and returns empty array
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: 'invalid' as ResourceType,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('empty principals', () => {
|
||
|
|
test('should return empty array when no principals found', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([]);
|
||
|
|
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('successful queries', () => {
|
||
|
|
test('should find resources user can view', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const viewableResources = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 1, // VIEW
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(viewableResources).toHaveLength(2);
|
||
|
|
const resourceIds = viewableResources.map((id) => id.toString());
|
||
|
|
expect(resourceIds).toContain(resource1.toString());
|
||
|
|
expect(resourceIds).toContain(resource2.toString());
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should find resources user can edit', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const editableResources = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 3, // EDIT
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(editableResources).toHaveLength(1);
|
||
|
|
expect(editableResources[0].toString()).toBe(resource2.toString());
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should find resources accessible via group membership', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.GROUP, principalId: groupId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const viewableResources = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 1, // VIEW
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(viewableResources).toHaveLength(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should pass role when provided', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
role: 'admin',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(mockGetUserPrincipals).toHaveBeenCalledWith({
|
||
|
|
userId,
|
||
|
|
role: 'admin',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('findPubliclyAccessibleResources', () => {
|
||
|
|
const publicResource1 = new Types.ObjectId();
|
||
|
|
const publicResource2 = new Types.ObjectId();
|
||
|
|
const privateResource = new Types.ObjectId();
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Public can view resource 1
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.PUBLIC,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: publicResource1,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Public can edit resource 2
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.PUBLIC,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: publicResource2,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Private resource - only user access
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: privateResource,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validation', () => {
|
||
|
|
test('should throw error when requiredPermissions is not a positive number', async () => {
|
||
|
|
await expect(
|
||
|
|
service.findPubliclyAccessibleResources({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 0,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('requiredPermissions must be a positive number');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return empty array for invalid resource type (error is caught)', async () => {
|
||
|
|
// The service catches invalid resourceType errors and returns empty array
|
||
|
|
const result = await service.findPubliclyAccessibleResources({
|
||
|
|
resourceType: 'invalid' as ResourceType,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('finding public resources', () => {
|
||
|
|
test('should find publicly viewable resources', async () => {
|
||
|
|
const publicResources = await service.findPubliclyAccessibleResources({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 1, // VIEW
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(publicResources).toHaveLength(2);
|
||
|
|
const resourceIds = publicResources.map((id) => id.toString());
|
||
|
|
expect(resourceIds).toContain(publicResource1.toString());
|
||
|
|
expect(resourceIds).toContain(publicResource2.toString());
|
||
|
|
expect(resourceIds).not.toContain(privateResource.toString());
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should find publicly editable resources', async () => {
|
||
|
|
const editableResources = await service.findPubliclyAccessibleResources({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 3, // EDIT
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(editableResources).toHaveLength(1);
|
||
|
|
expect(editableResources[0].toString()).toBe(publicResource2.toString());
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return empty array when no public permissions exist', async () => {
|
||
|
|
const noPublicResources = await service.findPubliclyAccessibleResources({
|
||
|
|
resourceType: ResourceType.PROMPTGROUP,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(noPublicResources).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getResourcePermissionsMap', () => {
|
||
|
|
const resource1 = new Types.ObjectId();
|
||
|
|
const resource2 = new Types.ObjectId();
|
||
|
|
const resource3 = new Types.ObjectId();
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// User has VIEW on resource1
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resource1,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// User has EDIT on resource2
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resource2,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Group has EDIT on resource1 (higher permission)
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: groupId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resource1,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
// resource3 has no permissions
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('empty arrays', () => {
|
||
|
|
test('should return empty map for empty resourceIds array', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const permissionsMap = await service.getResourcePermissionsMap({
|
||
|
|
userId,
|
||
|
|
role: 'user',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceIds: [],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(permissionsMap).toBeInstanceOf(Map);
|
||
|
|
expect(permissionsMap.size).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw on invalid resource type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.getResourcePermissionsMap({
|
||
|
|
userId,
|
||
|
|
role: 'user',
|
||
|
|
resourceType: 'invalid' as ResourceType,
|
||
|
|
resourceIds: [resource1],
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resourceType: invalid');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('batch queries', () => {
|
||
|
|
test('should get permissions for multiple resources in single query', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.PUBLIC },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const permissionsMap = await service.getResourcePermissionsMap({
|
||
|
|
userId,
|
||
|
|
role: 'user',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceIds: [resource1, resource2, resource3],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(permissionsMap).toBeInstanceOf(Map);
|
||
|
|
expect(permissionsMap.size).toBe(2); // resource1 and resource2
|
||
|
|
expect(permissionsMap.get(resource1.toString())).toBe(RoleBits.VIEWER);
|
||
|
|
expect(permissionsMap.get(resource2.toString())).toBe(RoleBits.EDITOR);
|
||
|
|
expect(permissionsMap.get(resource3.toString())).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should combine permissions from multiple principals', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.GROUP, principalId: groupId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const permissionsMap = await service.getResourcePermissionsMap({
|
||
|
|
userId,
|
||
|
|
role: 'user',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceIds: [resource1, resource2],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(permissionsMap.size).toBe(2);
|
||
|
|
// Resource1 should have VIEW (1) | EDIT (3) = 3 from combined user+group
|
||
|
|
expect(permissionsMap.get(resource1.toString())).toBe(RoleBits.EDITOR);
|
||
|
|
expect(permissionsMap.get(resource2.toString())).toBe(RoleBits.EDITOR);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should use role optimization when provided', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
await service.getResourcePermissionsMap({
|
||
|
|
userId,
|
||
|
|
role: 'admin',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceIds: [resource1],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(mockGetUserPrincipals).toHaveBeenCalledWith({ userId, role: 'admin' });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('removeAllPermissions', () => {
|
||
|
|
const resourceToDelete = new Types.ObjectId();
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Grant multiple permissions to the resource
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: groupId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.PUBLIC,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validation', () => {
|
||
|
|
test('should throw error for invalid resource type', async () => {
|
||
|
|
await expect(
|
||
|
|
service.removeAllPermissions({
|
||
|
|
resourceType: 'invalid' as ResourceType,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resourceType: invalid');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error for invalid resource ID', async () => {
|
||
|
|
await expect(
|
||
|
|
service.removeAllPermissions({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: 'invalid-id',
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resource ID: invalid-id');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('cleanup', () => {
|
||
|
|
test('should delete all permissions for a resource', async () => {
|
||
|
|
// Verify permissions exist
|
||
|
|
const beforeCount = await AclEntry.countDocuments({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
});
|
||
|
|
expect(beforeCount).toBe(3);
|
||
|
|
|
||
|
|
const result = await service.removeAllPermissions({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result.acknowledged).toBe(true);
|
||
|
|
expect(result.deletedCount).toBe(3);
|
||
|
|
|
||
|
|
// Verify permissions are deleted
|
||
|
|
const afterCount = await AclEntry.countDocuments({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
});
|
||
|
|
expect(afterCount).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return result even when no permissions existed', async () => {
|
||
|
|
const newResourceId = new Types.ObjectId();
|
||
|
|
|
||
|
|
const result = await service.removeAllPermissions({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: newResourceId,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result.acknowledged).toBe(true);
|
||
|
|
expect(result.deletedCount).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should not affect other resources permissions', async () => {
|
||
|
|
const otherResource = new Types.ObjectId();
|
||
|
|
|
||
|
|
// Grant permission to another resource
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: otherResource,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
await service.removeAllPermissions({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: resourceToDelete,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Verify other resource still has permissions
|
||
|
|
const otherResourcePerms = await AclEntry.countDocuments({
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: otherResource,
|
||
|
|
});
|
||
|
|
expect(otherResourcePerms).toBe(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('checkPermission', () => {
|
||
|
|
const testResource = new Types.ObjectId();
|
||
|
|
const groupResource = new Types.ObjectId();
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// User has VIEW on testResource
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Group has EDIT on groupResource
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: groupId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: groupResource,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validation', () => {
|
||
|
|
test('should throw error when requiredPermission is not a positive number', async () => {
|
||
|
|
await expect(
|
||
|
|
service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: 0,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('requiredPermission must be a positive number');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw error when requiredPermission is negative', async () => {
|
||
|
|
await expect(
|
||
|
|
service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: -1,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('requiredPermission must be a positive number');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return false for invalid resource type (error is caught)', async () => {
|
||
|
|
// The service catches invalid resourceType errors and returns false
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: 'invalid' as ResourceType,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('permission scenarios', () => {
|
||
|
|
test('should return true when user has required permission', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: 1, // VIEW
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return false when user lacks required permission', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: 3, // EDIT
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return false when no principals found', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([]);
|
||
|
|
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should check permission via group membership', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.GROUP, principalId: groupId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: groupResource,
|
||
|
|
requiredPermission: 1, // VIEW (editor includes view)
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return false for non-existent resource', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: new Types.ObjectId(),
|
||
|
|
requiredPermission: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should pass role when provided for optimization', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
role: 'admin',
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: testResource,
|
||
|
|
requiredPermission: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(mockGetUserPrincipals).toHaveBeenCalledWith({
|
||
|
|
userId: userId.toString(),
|
||
|
|
role: 'admin',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('public access', () => {
|
||
|
|
test('should check permission for public access', async () => {
|
||
|
|
const publicResource = new Types.ObjectId();
|
||
|
|
|
||
|
|
await service.grantPermission({
|
||
|
|
principalType: PrincipalType.PUBLIC,
|
||
|
|
principalId: null,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: publicResource,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
});
|
||
|
|
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.PUBLIC },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const hasPermission = await service.checkPermission({
|
||
|
|
userId: userId.toString(),
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
resourceId: publicResource,
|
||
|
|
requiredPermission: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(hasPermission).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validateResourceType (via public methods)', () => {
|
||
|
|
test('should accept AGENT resource type', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([]);
|
||
|
|
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.AGENT,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should accept PROMPTGROUP resource type', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([]);
|
||
|
|
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.PROMPTGROUP,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should accept MCPSERVER resource type', async () => {
|
||
|
|
mockGetUserPrincipals.mockResolvedValue([]);
|
||
|
|
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: ResourceType.MCPSERVER,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return empty array for unknown resource type (error caught)', async () => {
|
||
|
|
// findAccessibleResources catches invalid resource type and returns empty array
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: 'unknown_type' as ResourceType,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should return empty array for empty string resource type (error caught)', async () => {
|
||
|
|
// findAccessibleResources catches invalid resource type and returns empty array
|
||
|
|
const result = await service.findAccessibleResources({
|
||
|
|
userId,
|
||
|
|
resourceType: '' as ResourceType,
|
||
|
|
requiredPermissions: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw for invalid resource type in grantPermission', async () => {
|
||
|
|
// grantPermission throws directly for invalid resource type
|
||
|
|
await expect(
|
||
|
|
service.grantPermission({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
resourceType: 'unknown_type' as ResourceType,
|
||
|
|
resourceId,
|
||
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||
|
|
grantedBy: grantedById,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resourceType: unknown_type');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should throw for empty string resource type in removeAllPermissions', async () => {
|
||
|
|
// removeAllPermissions throws directly for invalid resource type
|
||
|
|
await expect(
|
||
|
|
service.removeAllPermissions({
|
||
|
|
resourceType: '' as ResourceType,
|
||
|
|
resourceId,
|
||
|
|
}),
|
||
|
|
).rejects.toThrow('Invalid resourceType:');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|