File size: 13,287 Bytes
f0743f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | import mongoose from 'mongoose';
import { PrincipalType } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server';
import type * as t from '~/types';
import { createUserGroupMethods } from './userGroup';
import groupSchema from '~/schema/group';
import userSchema from '~/schema/user';
import roleSchema from '~/schema/role';
/** Mocking logger */
jest.mock('~/config/winston', () => ({
error: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
}));
let mongoServer: MongoMemoryServer;
let Group: mongoose.Model<t.IGroup>;
let User: mongoose.Model<t.IUser>;
let Role: mongoose.Model<t.IRole>;
let methods: ReturnType<typeof createUserGroupMethods>;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
/** Register models */
Group = mongoose.models.Group || mongoose.model<t.IGroup>('Group', groupSchema);
User = mongoose.models.User || mongoose.model<t.IUser>('User', userSchema);
Role = mongoose.models.Role || mongoose.model<t.IRole>('Role', roleSchema);
/** Initialize methods */
methods = createUserGroupMethods(mongoose);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await mongoose.connection.dropDatabase();
});
describe('Role-based Permissions Integration', () => {
describe('getUserPrincipals with roles', () => {
test('should include role principal for user with role', async () => {
const adminUser = await User.create({
name: 'Admin User',
email: 'admin@test.com',
provider: 'local',
role: 'admin',
});
const principals = await methods.getUserPrincipals({
userId: adminUser._id as mongoose.Types.ObjectId,
});
// Should have user, role, and public principals
expect(principals).toHaveLength(3);
const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER);
const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
const publicPrincipal = principals.find((p) => p.principalType === PrincipalType.PUBLIC);
expect(userPrincipal).toBeDefined();
expect(userPrincipal?.principalId?.toString()).toBe(
(adminUser._id as mongoose.Types.ObjectId).toString(),
);
expect(rolePrincipal).toBeDefined();
expect(rolePrincipal?.principalType).toBe(PrincipalType.ROLE);
expect(rolePrincipal?.principalId).toBe('admin');
expect(publicPrincipal).toBeDefined();
expect(publicPrincipal?.principalType).toBe(PrincipalType.PUBLIC);
expect(publicPrincipal?.principalId).toBeUndefined();
});
test('should not include role principal for user without role', async () => {
const regularUser = await User.create({
name: 'Regular User',
email: 'user@test.com',
provider: 'local',
role: null, // Explicitly set to null to override default
});
const principals = await methods.getUserPrincipals({
userId: regularUser._id as mongoose.Types.ObjectId,
});
// Should only have user and public principals
expect(principals).toHaveLength(2);
const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
expect(rolePrincipal).toBeUndefined();
});
test('should include all principal types for user with role and groups', async () => {
const user = await User.create({
name: 'Complete User',
email: 'complete@test.com',
provider: 'local',
role: 'moderator',
});
// Add user to groups
const group1 = await Group.create({
name: 'Group 1',
source: 'local',
memberIds: [(user._id as mongoose.Types.ObjectId).toString()],
});
const group2 = await Group.create({
name: 'Group 2',
source: 'local',
memberIds: [(user._id as mongoose.Types.ObjectId).toString()],
});
const principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
// Should have user, role, 2 groups, and public
expect(principals).toHaveLength(5);
const principalTypes = principals.map((p) => p.principalType);
expect(principalTypes).toContain(PrincipalType.USER);
expect(principalTypes).toContain(PrincipalType.ROLE);
expect(principalTypes).toContain(PrincipalType.GROUP);
expect(principalTypes).toContain(PrincipalType.PUBLIC);
// Check role principal
const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
expect(rolePrincipal?.principalId).toBe('moderator');
// Check group principals
const groupPrincipals = principals.filter((p) => p.principalType === PrincipalType.GROUP);
expect(groupPrincipals).toHaveLength(2);
const groupIds = groupPrincipals.map((p) => p.principalId?.toString());
expect(groupIds).toContain(group1._id.toString());
expect(groupIds).toContain(group2._id.toString());
});
test('should handle different role values', async () => {
const testCases = [
{ role: 'admin', expected: 'admin' },
{ role: 'moderator', expected: 'moderator' },
{ role: 'editor', expected: 'editor' },
{ role: 'viewer', expected: 'viewer' },
{ role: 'custom_role', expected: 'custom_role' },
];
for (const testCase of testCases) {
const user = await User.create({
name: `User with ${testCase.role}`,
email: `${testCase.role}@test.com`,
provider: 'local',
role: testCase.role,
});
const principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
expect(rolePrincipal).toBeDefined();
expect(rolePrincipal?.principalId).toBe(testCase.expected);
}
});
});
describe('searchPrincipals with role support', () => {
beforeEach(async () => {
// Create some roles in the database
await Role.create([
{ name: 'admin', description: 'Administrator role' },
{ name: 'moderator', description: 'Moderator role' },
{ name: 'editor', description: 'Editor role' },
{ name: 'viewer', description: 'Viewer role' },
{ name: 'guest', description: 'Guest role' },
]);
// Create some users
await User.create([
{
name: 'Admin User',
email: 'admin@test.com',
username: 'adminuser',
provider: 'local',
role: 'admin',
},
{
name: 'Moderator User',
email: 'moderator@test.com',
username: 'moduser',
provider: 'local',
role: 'moderator',
},
]);
// Create some groups
await Group.create([
{
name: 'Admin Group',
source: 'local',
memberIds: [],
},
{
name: 'Moderator Group',
source: 'local',
memberIds: [],
},
]);
});
test('should search for roles when Role model exists', async () => {
const results = await methods.searchPrincipals('admin');
const roleResults = results.filter((r) => r.type === PrincipalType.ROLE);
const userResults = results.filter((r) => r.type === PrincipalType.USER);
const groupResults = results.filter((r) => r.type === PrincipalType.GROUP);
// Should find the admin role
expect(roleResults).toHaveLength(1);
expect(roleResults[0].id).toBe('admin');
expect(roleResults[0].name).toBe('admin');
expect(roleResults[0].type).toBe(PrincipalType.ROLE);
// Should also find admin user and group
expect(userResults.some((u) => u.name === 'Admin User')).toBe(true);
expect(groupResults.some((g) => g.name === 'Admin Group')).toBe(true);
});
test('should filter search results by role type', async () => {
const results = await methods.searchPrincipals('mod', 10, [PrincipalType.ROLE]);
expect(results.every((r) => r.type === PrincipalType.ROLE)).toBe(true);
expect(results).toHaveLength(1);
expect(results[0].name).toBe('moderator');
});
test('should respect limit for role search', async () => {
// Create many roles
for (let i = 0; i < 10; i++) {
await Role.create({ name: `testrole${i}` });
}
const results = await methods.searchPrincipals('testrole', 5, [PrincipalType.ROLE]);
expect(results).toHaveLength(5);
expect(results.every((r) => r.type === PrincipalType.ROLE)).toBe(true);
});
test('should search across all principal types', async () => {
const results = await methods.searchPrincipals('mod');
// Should find moderator role, user, and group
const types = new Set(results.map((r) => r.type));
expect(types.has(PrincipalType.ROLE)).toBe(true);
expect(types.has(PrincipalType.USER)).toBe(true);
expect(types.has(PrincipalType.GROUP)).toBe(true);
// Check specific results
expect(results.some((r) => r.type === PrincipalType.ROLE && r.name === 'moderator')).toBe(
true,
);
expect(
results.some((r) => r.type === PrincipalType.USER && r.name === 'Moderator User'),
).toBe(true);
expect(
results.some((r) => r.type === PrincipalType.GROUP && r.name === 'Moderator Group'),
).toBe(true);
});
test('should handle case-insensitive role search', async () => {
const results = await methods.searchPrincipals('ADMIN', 10, [PrincipalType.ROLE]);
expect(results).toHaveLength(1);
expect(results[0].name).toBe('admin');
});
test('should return empty array for no role matches', async () => {
const results = await methods.searchPrincipals('nonexistentrole', 10, [PrincipalType.ROLE]);
expect(results).toEqual([]);
});
});
describe('Role principals in complex scenarios', () => {
test('should handle user role changes', async () => {
const user = await User.create({
name: 'Changing User',
email: 'change@test.com',
provider: 'local',
role: 'viewer',
});
// Initial principals
let principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
let rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
expect(rolePrincipal?.principalId).toBe('viewer');
// Change role
user.role = 'editor';
await user.save();
// Get principals again
principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
expect(rolePrincipal?.principalId).toBe('editor');
});
test('should handle user role removal', async () => {
const user = await User.create({
name: 'Demoted User',
email: 'demoted@test.com',
provider: 'local',
role: 'admin',
});
// Initial check
let principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
expect(principals).toHaveLength(3); // user, role, public
// Remove role
user.role = undefined;
await user.save();
// Check again
principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
expect(principals).toHaveLength(2); // user, public
const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
expect(rolePrincipal).toBeUndefined();
});
test('should handle empty or null role values', async () => {
const testCases = [
{ role: '', expected: false },
{ role: null, expected: false },
{ role: undefined, expected: true, expectedRole: 'USER' }, // undefined gets default 'USER'
{ role: ' ', expected: false }, // whitespace-only is not a valid role
{ role: 'valid_role', expected: true, expectedRole: 'valid_role' },
];
for (const testCase of testCases) {
const userData: Partial<t.IUser> = {
name: `User ${Math.random()}`,
email: `test${Math.random()}@test.com`,
provider: 'local',
};
// Only set role if it's not undefined (to test undefined case)
if (testCase.role !== undefined) {
userData.role = testCase.role as string;
}
const user = await User.create(userData);
const principals = await methods.getUserPrincipals({
userId: user._id as mongoose.Types.ObjectId,
});
const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE);
if (testCase.expected) {
expect(rolePrincipal).toBeDefined();
expect(rolePrincipal?.principalId).toBe(testCase.expectedRole || testCase.role);
} else {
expect(rolePrincipal).toBeUndefined();
}
}
});
});
});
|