krona-staff/tests/unit/sync.test.ts
Ochenstarik fd1ec3d819 style: establish repository formatting baseline
Run the configured formatter across the repository before audit remediation so later security fixes produce reviewable, conflict-resistant diffs.
2026-09-26 14:22:25 +07:00

829 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import crypto from 'node:crypto';
import { describe, expect, it, vi } from 'vitest';
import { newUlid, parseUlid } from '../../src/core/ids.js';
import type { Database } from '../../src/infrastructure/database.js';
import { decodeCursor, encodeCursor, InvalidCursorError } from '../../src/domain/sync/cursor.js';
import { SyncService } from '../../src/domain/sync/service.js';
import type { SyncCommand } from '../../src/domain/sync/types.js';
import { buildApp } from '../../src/app.js';
import { loadConfig } from '../../src/config.js';
function createMockDb(
queryHandler?: (sql: string, params: unknown[]) => { rows: unknown[]; rowCount?: number },
) {
const queries: Array<{ sql: string; params: unknown[] }> = [];
const query = vi.fn(async (sql: string, params: unknown[] = []) => {
queries.push({ sql, params });
if (queryHandler) {
const res = queryHandler(sql, params);
return { rows: res.rows, rowCount: res.rowCount ?? res.rows.length };
}
return { rows: [], rowCount: 0 };
});
const database = {
ping: vi.fn(async () => undefined),
close: vi.fn(async () => undefined),
tenantTransaction: vi.fn(
async (
_tenantId: string,
_actorId: string,
run: (client: { query: typeof query }) => Promise<unknown>,
) => run({ query }),
),
} as unknown as Database;
return { database, queries };
}
describe('Sync Cursor', () => {
const tenantA = parseUlid('01J0000000000000000000000A');
const tenantB = parseUlid('01J0000000000000000000000B');
it('encodes and decodes monotonic cursor correctly', () => {
const cursor = encodeCursor(tenantA, 42n);
expect(cursor).toMatch(/^cur_42_[0-9a-f]{12}$/);
const decoded = decodeCursor(tenantA, cursor);
expect(decoded).toBe(42n);
});
it('rejects cursor with invalid format or tampered signature', () => {
expect(() => decodeCursor(tenantA, 'invalid')).toThrowError(InvalidCursorError);
expect(() => decodeCursor(tenantA, 'cur_42')).toThrowError(InvalidCursorError);
expect(() => decodeCursor(tenantA, 'cur_abc_1234567890ab')).toThrowError(InvalidCursorError);
// Tampered seq with same signature
const valid = encodeCursor(tenantA, 42n);
const tampered = valid.replace('cur_42_', 'cur_43_');
expect(() => decodeCursor(tenantA, tampered)).toThrowError(InvalidCursorError);
});
it('enforces tenant isolation in cursor verification', () => {
const cursorA = encodeCursor(tenantA, 100n);
// Passing cursor of tenantA to tenantB must fail
expect(() => decodeCursor(tenantB, cursorA)).toThrowError(InvalidCursorError);
});
});
describe('Sync Device Authentication & Tenant Isolation', () => {
const tenantA = parseUlid('01J0000000000000000000000A');
const tenantB = parseUlid('01J0000000000000000000000B');
const systemActorId = parseUlid('01J00000000000000000000001');
it('authenticates active device and updates last_seen_at', async () => {
const deviceId = newUlid();
const rawSecret = crypto.randomBytes(32).toString('hex');
const rawToken = `kstaff_dev_${tenantA}_${rawSecret}`;
const tokenHash = SyncService.hashToken(rawToken);
const { database, queries } = createMockDb((sql) => {
if (sql.includes('SELECT id, tenant_id')) {
return {
rows: [
{
id: deviceId,
tenant_id: tenantA,
device_code: 'TERM-01',
name: 'Terminal 1',
token_hash: tokenHash,
token_hint: 'kstaff_dev_01J...',
status: 'active',
allowed_scopes: ['attendance:sync'],
org_unit_id: null,
registered_at: new Date('2026-09-26T10:00:00Z'),
last_seen_at: null,
revoked_at: null,
created_by: systemActorId,
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
const device = await service.authenticateDevice(rawToken, tenantA);
expect(device.id).toBe(deviceId);
expect(device.tenant_id).toBe(tenantA);
expect(device.status).toBe('active');
expect(
queries.some(
(q) => q.sql.includes('UPDATE staff_sync_devices') && q.sql.includes('last_seen_at'),
),
).toBe(true);
});
it('rejects revoked device with 403 DEVICE_REVOKED', async () => {
const rawToken = `kstaff_dev_${tenantA}_${crypto.randomBytes(32).toString('hex')}`;
const tokenHash = SyncService.hashToken(rawToken);
const { database } = createMockDb((sql) => {
if (sql.includes('SELECT id, tenant_id')) {
return {
rows: [
{
id: newUlid(),
tenant_id: tenantA,
device_code: 'TERM-01',
name: 'Terminal 1',
token_hash: tokenHash,
token_hint: 'hint',
status: 'revoked',
allowed_scopes: ['attendance:sync'],
org_unit_id: null,
registered_at: new Date(),
last_seen_at: null,
revoked_at: new Date(),
created_by: systemActorId,
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
await expect(service.authenticateDevice(rawToken, tenantA)).rejects.toMatchObject({
name: 'SyncAuthError',
code: 'DEVICE_REVOKED',
statusCode: 403,
message: 'Устройство отозвано',
});
});
it('prevents reading or authenticating across different tenants (cross-tenant rejection)', async () => {
const rawToken = `kstaff_dev_${tenantA}_${crypto.randomBytes(32).toString('hex')}`;
const { database } = createMockDb();
const service = new SyncService(database, systemActorId);
// Explicitly providing tenantB for a token configured for tenantA
await expect(service.authenticateDevice(rawToken, tenantB)).rejects.toMatchObject({
code: 'TENANT_MISMATCH',
statusCode: 403,
});
});
it('rejects device without required sync scope', async () => {
const rawToken = `kstaff_dev_${tenantA}_${crypto.randomBytes(32).toString('hex')}`;
const { database } = createMockDb((sql) => {
if (sql.includes('SELECT id, tenant_id')) {
return {
rows: [
{
id: newUlid(),
tenant_id: tenantA,
device_code: 'TERM-VIEWER',
name: 'Viewer',
token_hash: SyncService.hashToken(rawToken),
token_hint: 'hint',
status: 'active',
allowed_scopes: ['display:read_only'],
org_unit_id: null,
registered_at: new Date(),
last_seen_at: null,
revoked_at: null,
created_by: systemActorId,
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
await expect(
service.authenticateDevice(rawToken, tenantA, 'attendance:sync'),
).rejects.toMatchObject({
code: 'SCOPE_INSUFFICIENT',
statusCode: 403,
});
});
});
describe('Sync Push & Idempotency', () => {
const tenantId = parseUlid('01J0000000000000000000000A');
const systemActorId = parseUlid('01J00000000000000000000001');
const deviceId = parseUlid('01J0000000000000000000000D');
const employeeId = parseUlid('01J0000000000000000000000E');
const testDevice = {
id: deviceId,
tenant_id: tenantId,
device_code: 'TERM-01',
name: 'Terminal 1',
token_hash: 'hash',
token_hint: 'hint',
status: 'active' as const,
allowed_scopes: ['attendance:sync'],
org_unit_id: null,
registered_at: new Date().toISOString(),
last_seen_at: null,
revoked_at: null,
created_by: systemActorId,
};
it('processes attendance_event and inserts into staff_sync_command_inbox', async () => {
const { database, queries } = createMockDb((sql) => {
if (sql.includes('SELECT status, result, conflict')) {
return { rows: [] }; // Not yet in inbox
}
if (sql.includes('SELECT id FROM staff_employees')) {
return { rows: [{ id: employeeId }] };
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
const command: SyncCommand = {
command_type: 'attendance_event',
schema_version: 1,
idempotency_key: 'cmd-test-1',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {
employee_id: employeeId,
event_type: 'check_in',
occurred_at: new Date().toISOString(),
source_event_id: 'scanner-evt-001',
},
};
const results = await service.push(tenantId, testDevice, [command]);
expect(results).toHaveLength(1);
expect(results[0]?.status).toBe('applied');
expect(results[0]?.result).toMatchObject({ status: 'accepted' });
expect(queries.some((q) => q.sql.includes('INSERT INTO staff_attendance_events'))).toBe(true);
expect(queries.some((q) => q.sql.includes('INSERT INTO staff_sync_command_inbox'))).toBe(true);
expect(queries.some((q) => q.sql.includes('INSERT INTO staff_sync_change_log'))).toBe(true);
});
it('returns cached result on repeated idempotency_key without re-executing', async () => {
const cachedResult = { event_id: newUlid(), status: 'accepted' };
const { database, queries } = createMockDb((sql) => {
if (sql.includes('SELECT status, result, conflict')) {
// Return already processed command
return {
rows: [
{
status: 'applied',
result: cachedResult,
conflict: null,
error_code: null,
error_message: null,
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
const command: SyncCommand = {
command_type: 'attendance_event',
schema_version: 1,
idempotency_key: 'cmd-duplicate',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {
employee_id: employeeId,
event_type: 'check_in',
occurred_at: new Date().toISOString(),
source_event_id: 'scanner-evt-001',
},
};
const results = await service.push(tenantId, testDevice, [command]);
expect(results).toHaveLength(1);
expect(results[0]?.status).toBe('applied');
expect(results[0]?.result).toEqual(cachedResult);
// Did NOT execute insert into staff_attendance_events again!
expect(queries.some((q) => q.sql.includes('INSERT INTO staff_attendance_events'))).toBe(false);
});
it('rejects upsert_work_session command with COMMAND_NOT_SUPPORTED', async () => {
const sessionId = newUlid();
const { database } = createMockDb();
const service = new SyncService(database, systemActorId);
const command: SyncCommand = {
command_type: 'upsert_work_session',
schema_version: 1,
idempotency_key: 'cmd-session-reject',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {
id: sessionId,
employee_id: employeeId,
org_unit_id: newUlid(),
business_date: '2026-09-26',
revision: 1,
worked_minutes: 480,
status: 'confirmed',
},
};
const results = await service.push(tenantId, testDevice, [command]);
expect(results).toHaveLength(1);
expect(results[0]?.status).toBe('rejected');
expect(results[0]?.error_code).toBe('COMMAND_NOT_SUPPORTED');
expect(results[0]?.error_message).toContain('Команда не поддерживается');
});
it('rejects upsert_timesheet_entry command with COMMAND_NOT_SUPPORTED', async () => {
const entryId = newUlid();
const { database } = createMockDb();
const service = new SyncService(database, systemActorId);
const command: SyncCommand = {
command_type: 'upsert_timesheet_entry',
schema_version: 1,
idempotency_key: 'cmd-ts-reject',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {
id: entryId,
employee_id: employeeId,
work_date: '2026-09-26',
minutes_worked: 500,
revision: 1,
},
};
const results = await service.push(tenantId, testDevice, [command]);
expect(results).toHaveLength(1);
expect(results[0]?.status).toBe('rejected');
expect(results[0]?.error_code).toBe('COMMAND_NOT_SUPPORTED');
expect(results[0]?.error_message).toContain('Команда не поддерживается');
});
it('does not write to staff_work_sessions, staff_timesheet_entries, or staff_sync_change_log on rejected shift commands', async () => {
const { database, queries } = createMockDb();
const service = new SyncService(database, systemActorId);
const commands: SyncCommand[] = [
{
command_type: 'upsert_work_session',
schema_version: 1,
idempotency_key: 'cmd-session-no-write',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {
id: newUlid(),
employee_id: employeeId,
org_unit_id: newUlid(),
business_date: '2026-09-26',
status: 'confirmed',
worked_minutes: 2880,
},
},
{
command_type: 'upsert_timesheet_entry',
schema_version: 1,
idempotency_key: 'cmd-ts-no-write',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {
id: newUlid(),
employee_id: employeeId,
work_date: '2026-09-26',
minutes_worked: 500,
},
},
];
const results = await service.push(tenantId, testDevice, commands);
expect(results).toHaveLength(2);
expect(results.every((r) => r.status === 'rejected')).toBe(true);
// Verify nothing is written to staff_work_sessions
expect(
queries.some(
(q) =>
q.sql.toLowerCase().includes('staff_work_sessions') &&
(q.sql.toUpperCase().includes('INSERT') || q.sql.toUpperCase().includes('UPDATE')),
),
).toBe(false);
// Verify nothing is written to staff_timesheet_entries
expect(
queries.some(
(q) =>
q.sql.toLowerCase().includes('staff_timesheet_entries') &&
(q.sql.toUpperCase().includes('INSERT') || q.sql.toUpperCase().includes('UPDATE')),
),
).toBe(false);
// Verify nothing is written to staff_sync_change_log
expect(
queries.some(
(q) =>
q.sql.toLowerCase().includes('staff_sync_change_log') &&
(q.sql.toUpperCase().includes('INSERT') || q.sql.toUpperCase().includes('UPDATE')),
),
).toBe(false);
// Verify commands ARE saved to inbox with status 'rejected'
const inboxInserts = queries.filter((q) =>
q.sql.includes('INSERT INTO staff_sync_command_inbox'),
);
expect(inboxInserts).toHaveLength(2);
for (const insert of inboxInserts) {
expect(insert.params).toContain('rejected');
expect(insert.params).toContain('COMMAND_NOT_SUPPORTED');
}
});
it('enforces batch size limits and payload size limits', async () => {
const { database } = createMockDb();
const service = new SyncService(database, systemActorId);
// Empty batch
await expect(service.push(tenantId, testDevice, [])).rejects.toMatchObject({
code: 'BATCH_EMPTY',
statusCode: 400,
});
// Batch > 100
const largeBatch: SyncCommand[] = Array.from({ length: 101 }, (_, i) => ({
command_type: 'attendance_event',
schema_version: 1,
idempotency_key: `key-${i}`,
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: {},
}));
await expect(service.push(tenantId, testDevice, largeBatch)).rejects.toMatchObject({
code: 'BATCH_SIZE_EXCEEDED',
statusCode: 400,
});
// Single payload > 64KB
const giantPayload = { data: 'x'.repeat(70000) };
const commandWithGiantPayload: SyncCommand = {
command_type: 'attendance_event',
schema_version: 1,
idempotency_key: 'giant-payload',
correlation_id: newUlid(),
created_at: new Date().toISOString(),
payload: giantPayload,
};
const results = await service.push(tenantId, testDevice, [commandWithGiantPayload]);
expect(results[0]?.status).toBe('rejected');
expect(results[0]?.error_code).toBe('PAYLOAD_TOO_LARGE');
});
it('rejects commands with timestamps outside the replay window', async () => {
const { database } = createMockDb();
const service = new SyncService(database, systemActorId);
// 40 days old (> 30 days)
const oldDate = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString();
const oldCmd: SyncCommand = {
command_type: 'attendance_event',
schema_version: 1,
idempotency_key: 'cmd-old',
correlation_id: newUlid(),
created_at: oldDate,
payload: {},
};
// 10 minutes in future (> 5 minutes)
const futureDate = new Date(Date.now() + 10 * 60 * 1000).toISOString();
const futureCmd: SyncCommand = {
command_type: 'attendance_event',
schema_version: 1,
idempotency_key: 'cmd-future',
correlation_id: newUlid(),
created_at: futureDate,
payload: {},
};
const results = await service.push(tenantId, testDevice, [oldCmd, futureCmd]);
expect(results[0]?.status).toBe('rejected');
expect(results[0]?.error_code).toBe('TIMESTAMP_OUT_OF_WINDOW');
expect(results[1]?.status).toBe('rejected');
expect(results[1]?.error_code).toBe('TIMESTAMP_OUT_OF_WINDOW');
});
});
describe('Sync Pull & Bootstrap Data Safety', () => {
const tenantId = parseUlid('01J0000000000000000000000A');
const systemActorId = parseUlid('01J00000000000000000000001');
const deviceId = parseUlid('01J0000000000000000000000D');
it('bootstrap (cursor = null) returns safe entities without salary, PII, or biometrics', async () => {
const empId = newUlid();
const orgId = newUlid();
const profId = newUlid();
const { database } = createMockDb((sql) => {
if (sql.includes('MAX(cursor_seq)')) {
return { rows: [{ max_seq: '50' }] };
}
if (sql.includes('FROM staff_org_units')) {
return { rows: [{ id: orgId, code: 'KITCHEN', name: 'Кухня', status: 'active' }] };
}
if (sql.includes('FROM staff_professions')) {
return { rows: [{ id: profId, code: 'CHEF', name: 'Шеф', status: 'active' }] };
}
if (sql.includes('FROM staff_employees')) {
return {
rows: [
{
id: empId,
personnel_number: 'EMP-01',
display_name: 'Петров Петр',
status: 'active',
profession_id: profId,
position: 'Шеф-повар',
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
const result = await service.pull(tenantId, deviceId, { cursor: null });
expect(result.changes).toHaveLength(3);
expect(result.next_cursor).toBe(encodeCursor(tenantId, 50n));
expect(result.has_more).toBe(false);
// Verify employee payload has NO sensitive fields
const empChange = result.changes.find((c) => c.entity_type === 'employee');
expect(empChange).toBeDefined();
const payload = empChange!.payload;
expect(payload).toHaveProperty('personnel_number');
expect(payload).toHaveProperty('display_name');
expect(payload).not.toHaveProperty('salary');
expect(payload).not.toHaveProperty('hourly_rate');
expect(payload).not.toHaveProperty('passport');
expect(payload).not.toHaveProperty('phone');
expect(payload).not.toHaveProperty('biometrics');
expect(payload).not.toHaveProperty('template');
});
it('incremental pull returns changes strictly after cursor', async () => {
const cursor = encodeCursor(tenantId, 10n);
const { database } = createMockDb((sql) => {
if (sql.includes('FROM staff_sync_change_log')) {
return {
rows: [
{
id: newUlid(),
cursor_seq: '11',
entity_type: 'employee',
entity_id: newUlid(),
operation: 'upsert',
revision: 2,
payload: { id: 'test', display_name: 'Тест' },
created_at: new Date('2026-09-26T12:00:00Z'),
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, systemActorId);
const result = await service.pull(tenantId, deviceId, { cursor });
expect(result.changes).toHaveLength(1);
expect(result.changes[0]?.cursor).toBe(encodeCursor(tenantId, 11n));
expect(result.next_cursor).toBe(encodeCursor(tenantId, 11n));
expect(result.has_more).toBe(false);
});
});
describe('Sync API HTTP Endpoints', () => {
const tenantId = '01J00000000000000000000000';
const managerToken = 'a'.repeat(64);
const config = loadConfig({
DATABASE_URL: 'postgres://localhost/staff',
STANDALONE_TENANT_ID: tenantId,
STANDALONE_ACTOR_ID: '01J00000000000000000000001',
STANDALONE_MANAGER_TOKEN: managerToken,
STANDALONE_TERMINAL_TOKEN: 'b'.repeat(64),
LOG_LEVEL: 'silent',
NODE_ENV: 'test',
});
it('registers device with manager authorization', async () => {
const deviceId = newUlid();
const { database } = createMockDb((sql) => {
if (sql.includes('INSERT INTO staff_sync_devices')) {
return {
rows: [
{
id: deviceId,
tenant_id: tenantId,
device_code: 'TERM-API-01',
name: 'Касса 1',
token_hint: 'kstaff_dev_...',
status: 'active',
allowed_scopes: ['attendance:sync'],
org_unit_id: null,
registered_at: new Date(),
created_by: config.STANDALONE_ACTOR_ID,
},
],
};
}
return { rows: [] };
});
const app = await buildApp({ config, database });
await app.ready();
const response = await app.inject({
method: 'POST',
url: '/api/v1/staff/sync/devices',
headers: {
authorization: `Bearer ${managerToken}`,
},
payload: {
device_code: 'TERM-API-01',
name: 'Касса 1',
},
});
expect(response.statusCode).toBe(201);
const body = response.json<{ device: { device_code: string }; token: string }>();
expect(body.device.device_code).toBe('TERM-API-01');
expect(body.token).toMatch(/^kstaff_dev_/);
await app.close();
});
it('rejects unauthenticated requests to device sync endpoints', async () => {
const { database } = createMockDb();
const app = await buildApp({ config, database });
await app.ready();
const response = await app.inject({
method: 'GET',
url: '/api/v1/staff/sync/status',
});
expect(response.statusCode).toBe(401);
await app.close();
});
it('handles checkpoint acknowledgment', async () => {
const rawToken = `kstaff_dev_${tenantId}_${crypto.randomBytes(32).toString('hex')}`;
const deviceId = newUlid();
const cursor = encodeCursor(tenantId, 55n);
const { database } = createMockDb((sql) => {
if (sql.includes('FROM staff_sync_devices')) {
return {
rows: [
{
id: deviceId,
tenant_id: tenantId,
device_code: 'TERM-01',
name: 'Term',
token_hash: SyncService.hashToken(rawToken),
token_hint: 'hint',
status: 'active',
allowed_scopes: ['attendance:sync'],
org_unit_id: null,
registered_at: new Date(),
last_seen_at: null,
revoked_at: null,
created_by: config.STANDALONE_ACTOR_ID,
},
],
};
}
if (sql.includes('INSERT INTO staff_sync_device_checkpoints')) {
return { rows: [{ acknowledged_at: new Date('2026-09-26T12:00:00Z') }] };
}
return { rows: [] };
});
const app = await buildApp({ config, database });
await app.ready();
const response = await app.inject({
method: 'POST',
url: '/api/v1/staff/sync/ack',
headers: {
authorization: `Bearer ${rawToken}`,
},
payload: {
cursor,
},
});
expect(response.statusCode).toBe(200);
const body = response.json<{ acknowledged: boolean; cursor: string }>();
expect(body.acknowledged).toBe(true);
expect(body.cursor).toBe(cursor);
await app.close();
});
});
describe('Security & Privacy Audit (No secrets, no PII, no biometrics in logs or state)', () => {
const tenantId = parseUlid('01J0000000000000000000000A');
const actorId = parseUlid('01J00000000000000000000001');
it('guarantees raw device token is never stored in the database, only SHA-256 hash', async () => {
const { database, queries } = createMockDb((sql) => {
if (sql.includes('INSERT INTO staff_sync_devices')) {
return {
rows: [
{
id: newUlid(),
tenant_id: tenantId,
device_code: 'DEV-SEC',
name: 'Secure Terminal',
token_hint: 'kstaff_dev_...',
status: 'active',
allowed_scopes: ['attendance:sync'],
org_unit_id: null,
registered_at: new Date(),
created_by: actorId,
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, actorId);
const regResult = await service.registerDevice(tenantId, actorId, {
device_code: 'DEV-SEC',
name: 'Secure Terminal',
});
const rawToken = regResult.token;
expect(rawToken).toMatch(/^kstaff_dev_/);
// Verify SQL query parameters
const insertQuery = queries.find((q) => q.sql.includes('INSERT INTO staff_sync_devices'));
expect(insertQuery).toBeDefined();
// Raw token MUST NOT be present in parameters
const paramsString = JSON.stringify(insertQuery!.params);
expect(paramsString).not.toContain(rawToken);
// Only the hash should be stored
const expectedHash = SyncService.hashToken(rawToken);
expect(paramsString).toContain(expectedHash);
});
it('sanitizes and excludes sensitive employee attributes from sync payloads', async () => {
const { database } = createMockDb((sql) => {
if (sql.includes('MAX(cursor_seq)')) {
return { rows: [{ max_seq: '1' }] };
}
if (sql.includes('FROM staff_employees')) {
return {
rows: [
{
id: newUlid(),
personnel_number: 'EMP-99',
display_name: 'Секретный Сотрудник',
status: 'active',
profession_id: null,
position: 'Аналитик',
},
],
};
}
return { rows: [] };
});
const service = new SyncService(database, actorId);
const result = await service.pull(tenantId, newUlid(), { cursor: null });
const serialized = JSON.stringify(result);
// Explicit forbidden terms check
const forbiddenTerms = [
'salary',
'wage',
'rate',
'passport',
'snils',
'inn',
'bank_account',
'card_number',
'phone',
'email',
'biometrics',
'fingerprint',
'face_descriptor',
];
for (const term of forbiddenTerms) {
expect(serialized.toLowerCase()).not.toContain(`"${term}"`);
}
});
});