40227-vm/backend/src/db/api/users-search.test.ts

158 lines
5.0 KiB
TypeScript

import { afterEach, mock, test } from 'node:test';
import assert from 'node:assert/strict';
import { inspect } from 'node:util';
import { Op, type FindAndCountOptions } from 'sequelize';
import db from '@/db/models';
import UsersDBApi from '@/db/api/users';
afterEach(() => {
mock.restoreAll();
});
test('UsersDBApi query search uses correlated subqueries for related names', async () => {
let capturedOptions: FindAndCountOptions | null = null;
mock.method(
db.users,
'findAndCountAll',
(async (options: FindAndCountOptions) => {
capturedOptions = options;
return { rows: [], count: 0 };
}) as unknown as typeof db.users.findAndCountAll,
);
await UsersDBApi.findAll({ query: 'John', limit: 10, page: 0 }, true, {});
assert.ok(capturedOptions, 'expected findAndCountAll to be called');
const options = capturedOptions as FindAndCountOptions;
const where = options.where as Record<string | symbol, unknown>;
const andConditions = where[Op.and];
assert.ok(Array.isArray(andConditions), 'expected query search conditions in Op.and');
const serializedWhere = inspect(where, { depth: 10 });
assert.ok(
serializedWhere.includes('FROM "organizations"'),
'expected organization name search to use a correlated subquery',
);
assert.ok(
serializedWhere.includes('FROM "schools"'),
'expected school name search to use a correlated subquery',
);
assert.ok(
serializedWhere.includes('FROM "campuses"'),
'expected campus name search to use a correlated subquery',
);
assert.ok(
serializedWhere.includes('FROM "classes"'),
'expected class name search to use a correlated subquery',
);
assert.ok(
serializedWhere.includes('FROM "roles"'),
'expected role name search to use a correlated subquery',
);
assert.equal(
serializedWhere.includes('lower("school"."name")')
|| serializedWhere.includes('lower("campus"."name")')
|| serializedWhere.includes('lower("class"."name")')
|| serializedWhere.includes('lower("app_role"."name")'),
false,
'expected no direct joined-alias name references in the where clause',
);
});
test('UsersDBApi ignores removed user filters', async () => {
let capturedOptions: FindAndCountOptions | null = null;
mock.method(
db.users,
'findAndCountAll',
(async (options: FindAndCountOptions) => {
capturedOptions = options;
return { rows: [], count: 0 };
}) as unknown as typeof db.users.findAndCountAll,
);
const removedFilter = {
active: 'true',
password: 'secret',
emailVerificationToken: 'verify-token',
passwordResetToken: 'reset-token',
disabled: 'false',
limit: 10,
page: 0,
};
await UsersDBApi.findAll(removedFilter, true, {});
assert.ok(capturedOptions, 'expected findAndCountAll to be called');
const options = capturedOptions as FindAndCountOptions;
const where = options.where as Record<string | symbol, unknown>;
const serializedWhere = inspect(where, { depth: 10 });
assert.equal(where.disabled, false);
assert.equal(serializedWhere.includes('active'), false);
assert.equal(serializedWhere.includes('password'), false);
assert.equal(serializedWhere.includes('emailVerificationToken'), false);
assert.equal(serializedWhere.includes('passwordResetToken'), false);
});
test('UsersDBApi campus filter includes class-scoped users inside that campus', async () => {
let capturedOptions: FindAndCountOptions | null = null;
mock.method(
db.users,
'findAndCountAll',
(async (options: FindAndCountOptions) => {
capturedOptions = options;
return { rows: [], count: 0 };
}) as unknown as typeof db.users.findAndCountAll,
);
await UsersDBApi.findAll(
{ campusId: '00000000-0000-4000-8000-000000000001', limit: 10, page: 0 },
true,
{},
);
assert.ok(capturedOptions, 'expected findAndCountAll to be called');
const options = capturedOptions as FindAndCountOptions;
const where = options.where as Record<string | symbol, unknown>;
const serializedWhere = inspect(where, { depth: 10 });
assert.ok(
serializedWhere.includes('FROM "classes" WHERE "campusId"'),
'expected campus filtering to include class-scoped users',
);
});
test('UsersDBApi class filter includes enrolled students', async () => {
let capturedOptions: FindAndCountOptions | null = null;
mock.method(
db.users,
'findAndCountAll',
(async (options: FindAndCountOptions) => {
capturedOptions = options;
return { rows: [], count: 0 };
}) as unknown as typeof db.users.findAndCountAll,
);
await UsersDBApi.findAll(
{ classId: '00000000-0000-4000-8000-000000000002', limit: 10, page: 0 },
true,
{},
);
assert.ok(capturedOptions, 'expected findAndCountAll to be called');
const options = capturedOptions as FindAndCountOptions;
const where = options.where as Record<string | symbol, unknown>;
const serializedWhere = inspect(where, { depth: 10 });
assert.ok(
serializedWhere.includes('FROM "class_enrollments"'),
'expected class filtering to include enrolled students',
);
});