158 lines
5.0 KiB
TypeScript
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',
|
|
);
|
|
});
|