464 lines
15 KiB
JavaScript
464 lines
15 KiB
JavaScript
import test, { beforeEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import request from 'supertest';
|
|
import { createApp } from '../src/app.js';
|
|
import { __resetLlmRateLimitForTests } from '../src/services/llm-rate-limit.js';
|
|
import {
|
|
__resetVerificationJobsForTests,
|
|
createVerificationJob,
|
|
getVerificationJob,
|
|
} from '../src/services/verification-jobs.js';
|
|
|
|
beforeEach(async () => {
|
|
process.env.AUTH_BYPASS = 'true';
|
|
process.env.LLM_MOCK = 'true';
|
|
process.env.SIGNED_URL_MOCK = 'true';
|
|
process.env.UPLOAD_MOCK = 'true';
|
|
process.env.MAX_SIGNED_URL_SECONDS = '900';
|
|
process.env.LLM_RATE_LIMIT_PER_MINUTE = '20';
|
|
process.env.VERIFICATION_REQUIRE_FILE_EXISTS = 'false';
|
|
process.env.VERIFICATION_ACCESS_MODE = 'tenant';
|
|
process.env.VERIFICATION_ATTIRE_PROVIDER = 'mock';
|
|
process.env.VERIFICATION_STORE = 'memory';
|
|
__resetLlmRateLimitForTests();
|
|
await __resetVerificationJobsForTests();
|
|
});
|
|
|
|
async function waitForMachineStatus(app, verificationId, maxAttempts = 30) {
|
|
let last;
|
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
last = await request(app)
|
|
.get(`/core/verifications/${verificationId}`)
|
|
.set('Authorization', 'Bearer test-token');
|
|
if (
|
|
last.body?.status === 'AUTO_PASS'
|
|
|| last.body?.status === 'AUTO_FAIL'
|
|
|| last.body?.status === 'NEEDS_REVIEW'
|
|
|| last.body?.status === 'ERROR'
|
|
) {
|
|
return last;
|
|
}
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
return last;
|
|
}
|
|
|
|
test('GET /healthz returns healthy response', async () => {
|
|
const app = createApp();
|
|
const res = await request(app).get('/healthz');
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.ok, true);
|
|
assert.equal(typeof res.body.requestId, 'string');
|
|
assert.equal(typeof res.headers['x-request-id'], 'string');
|
|
});
|
|
|
|
test('GET /readyz reports database not configured when env is absent', async () => {
|
|
delete process.env.DATABASE_URL;
|
|
delete process.env.DB_HOST;
|
|
delete process.env.DB_NAME;
|
|
delete process.env.DB_USER;
|
|
delete process.env.DB_PASSWORD;
|
|
delete process.env.INSTANCE_CONNECTION_NAME;
|
|
delete process.env.VERIFICATION_STORE;
|
|
|
|
const app = createApp();
|
|
const res = await request(app).get('/readyz');
|
|
|
|
assert.equal(res.status, 503);
|
|
assert.equal(res.body.status, 'DATABASE_NOT_CONFIGURED');
|
|
});
|
|
|
|
test('createApp fails fast in protected env when unsafe core flags are enabled', async () => {
|
|
process.env.APP_ENV = 'staging';
|
|
process.env.AUTH_BYPASS = 'true';
|
|
|
|
assert.throws(() => createApp(), /AUTH_BYPASS must be disabled/);
|
|
|
|
delete process.env.APP_ENV;
|
|
process.env.AUTH_BYPASS = 'true';
|
|
});
|
|
|
|
test('POST /core/create-signed-url requires auth', async () => {
|
|
process.env.AUTH_BYPASS = 'false';
|
|
const app = createApp();
|
|
const res = await request(app).post('/core/create-signed-url').send({
|
|
fileUri: 'gs://krow-workforce-dev-private/foo.pdf',
|
|
});
|
|
|
|
assert.equal(res.status, 401);
|
|
assert.equal(res.body.code, 'UNAUTHENTICATED');
|
|
process.env.AUTH_BYPASS = 'true';
|
|
});
|
|
|
|
test('POST /core/create-signed-url returns signed URL', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/create-signed-url')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/test-user/foo.pdf',
|
|
expiresInSeconds: 300,
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(typeof res.body.signedUrl, 'string');
|
|
assert.equal(typeof res.body.expiresAt, 'string');
|
|
assert.equal(typeof res.body.requestId, 'string');
|
|
});
|
|
|
|
test('POST /core/create-signed-url rejects non-owned path', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/create-signed-url')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/other-user/foo.pdf',
|
|
expiresInSeconds: 300,
|
|
});
|
|
|
|
assert.equal(res.status, 403);
|
|
assert.equal(res.body.code, 'FORBIDDEN');
|
|
});
|
|
|
|
test('POST /core/create-signed-url enforces expiry cap', async () => {
|
|
process.env.MAX_SIGNED_URL_SECONDS = '300';
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/create-signed-url')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/test-user/foo.pdf',
|
|
expiresInSeconds: 301,
|
|
});
|
|
|
|
assert.equal(res.status, 400);
|
|
assert.equal(res.body.code, 'VALIDATION_ERROR');
|
|
});
|
|
|
|
test('POST /invokeLLM legacy alias works', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/invokeLLM')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
prompt: 'hello',
|
|
responseJsonSchema: { type: 'object' },
|
|
fileUrls: [],
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(typeof res.body.result, 'object');
|
|
assert.equal(typeof res.body.model, 'string');
|
|
});
|
|
|
|
test('POST /core/invoke-llm enforces per-user rate limit', async () => {
|
|
process.env.LLM_RATE_LIMIT_PER_MINUTE = '1';
|
|
const app = createApp();
|
|
|
|
const first = await request(app)
|
|
.post('/core/invoke-llm')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
prompt: 'hello',
|
|
responseJsonSchema: { type: 'object' },
|
|
fileUrls: [],
|
|
});
|
|
|
|
const second = await request(app)
|
|
.post('/core/invoke-llm')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
prompt: 'hello again',
|
|
responseJsonSchema: { type: 'object' },
|
|
fileUrls: [],
|
|
});
|
|
|
|
assert.equal(first.status, 200);
|
|
assert.equal(second.status, 429);
|
|
assert.equal(second.body.code, 'RATE_LIMITED');
|
|
assert.equal(typeof second.headers['retry-after'], 'string');
|
|
});
|
|
|
|
test('POST /core/upload-file accepts audio/webm for rapid transcription', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/upload-file')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.field('visibility', 'private')
|
|
.attach('file', Buffer.from('fake-audio-data'), {
|
|
filename: 'rapid-request.webm',
|
|
contentType: 'audio/webm',
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.contentType, 'audio/webm');
|
|
assert.equal(typeof res.body.fileUri, 'string');
|
|
});
|
|
|
|
test('POST /core/rapid-orders/transcribe returns transcript in mock mode', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/rapid-orders/transcribe')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
audioFileUri: 'gs://krow-workforce-dev-private/uploads/test-user/request.webm',
|
|
locale: 'en-US',
|
|
promptHints: ['server', 'urgent'],
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(typeof res.body.transcript, 'string');
|
|
assert.ok(res.body.transcript.length > 0);
|
|
assert.equal(typeof res.body.confidence, 'number');
|
|
assert.equal(typeof res.body.model, 'string');
|
|
assert.equal(typeof res.body.requestId, 'string');
|
|
});
|
|
|
|
test('POST /core/rapid-orders/transcribe rejects non-owned file URI', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/rapid-orders/transcribe')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
audioFileUri: 'gs://krow-workforce-dev-private/uploads/other-user/request.webm',
|
|
locale: 'en-US',
|
|
});
|
|
|
|
assert.equal(res.status, 403);
|
|
assert.equal(res.body.code, 'FORBIDDEN');
|
|
});
|
|
|
|
test('POST /core/rapid-orders/parse returns structured rapid order draft', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/rapid-orders/parse')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
text: 'Need 2 servers ASAP for 4 hours',
|
|
locale: 'en-US',
|
|
timezone: 'America/New_York',
|
|
now: '2026-02-27T12:00:00.000Z',
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.parsed.orderType, 'ONE_TIME');
|
|
assert.equal(res.body.parsed.isRapid, true);
|
|
assert.equal(Array.isArray(res.body.parsed.positions), true);
|
|
assert.equal(res.body.parsed.positions[0].role, 'server');
|
|
assert.equal(res.body.parsed.positions[0].count, 2);
|
|
assert.equal(res.body.parsed.durationMinutes, 240);
|
|
assert.equal(typeof res.body.confidence.overall, 'number');
|
|
assert.equal(typeof res.body.requestId, 'string');
|
|
});
|
|
|
|
test('POST /core/rapid-orders/parse validates timezone', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/rapid-orders/parse')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
text: 'Need 2 servers ASAP',
|
|
timezone: 'Mars/OlympusMons',
|
|
});
|
|
|
|
assert.equal(res.status, 400);
|
|
assert.equal(res.body.code, 'VALIDATION_ERROR');
|
|
});
|
|
|
|
test('POST /core/rapid-orders/parse rejects unknown fields', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/rapid-orders/parse')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
text: 'Need 2 servers ASAP',
|
|
unexpected: 'not-allowed',
|
|
});
|
|
|
|
assert.equal(res.status, 400);
|
|
assert.equal(res.body.code, 'VALIDATION_ERROR');
|
|
});
|
|
|
|
test('POST /core/rapid-orders/process accepts text-only flow', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/rapid-orders/process')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
text: 'Need 2 servers ASAP for 4 hours',
|
|
locale: 'en-US',
|
|
timezone: 'America/New_York',
|
|
now: '2026-02-27T12:00:00.000Z',
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(typeof res.body.transcript, 'string');
|
|
assert.equal(res.body.parsed.orderType, 'ONE_TIME');
|
|
assert.equal(Array.isArray(res.body.parsed.positions), true);
|
|
assert.equal(Array.isArray(res.body.catalog.roles), true);
|
|
});
|
|
|
|
test('POST /core/rapid-orders/parse enforces per-user model rate limit', async () => {
|
|
process.env.LLM_RATE_LIMIT_PER_MINUTE = '1';
|
|
const app = createApp();
|
|
|
|
const first = await request(app)
|
|
.post('/core/rapid-orders/parse')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
text: 'Need 2 servers ASAP for 4 hours',
|
|
});
|
|
|
|
const second = await request(app)
|
|
.post('/core/rapid-orders/parse')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
text: 'Need 3 bartenders tonight',
|
|
});
|
|
|
|
assert.equal(first.status, 200);
|
|
assert.equal(second.status, 429);
|
|
assert.equal(second.body.code, 'RATE_LIMITED');
|
|
assert.equal(typeof second.headers['retry-after'], 'string');
|
|
});
|
|
|
|
test('POST /core/verifications creates async job and GET returns status', async () => {
|
|
const app = createApp();
|
|
const created = await request(app)
|
|
.post('/core/verifications')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
type: 'attire',
|
|
subjectType: 'staff',
|
|
subjectId: 'staff_1',
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/test-user/attire.jpg',
|
|
rules: { attireType: 'shoes', expectedColor: 'black' },
|
|
});
|
|
|
|
assert.equal(created.status, 202);
|
|
assert.equal(created.body.type, 'attire');
|
|
assert.equal(created.body.status, 'PENDING');
|
|
assert.equal(typeof created.body.verificationId, 'string');
|
|
|
|
const status = await waitForMachineStatus(app, created.body.verificationId);
|
|
assert.equal(status.status, 200);
|
|
assert.equal(status.body.verificationId, created.body.verificationId);
|
|
assert.equal(status.body.type, 'attire');
|
|
assert.ok(['NEEDS_REVIEW', 'AUTO_PASS', 'AUTO_FAIL', 'ERROR'].includes(status.body.status));
|
|
});
|
|
|
|
test('POST /core/verifications accepts tax_form verification jobs', async () => {
|
|
const app = createApp();
|
|
const created = await request(app)
|
|
.post('/core/verifications')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
type: 'tax_form',
|
|
subjectType: 'worker',
|
|
subjectId: 'document-tax-i9',
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/test-user/i9.pdf',
|
|
rules: { formType: 'I-9' },
|
|
});
|
|
|
|
assert.equal(created.status, 202);
|
|
assert.equal(created.body.type, 'tax_form');
|
|
|
|
const status = await waitForMachineStatus(app, created.body.verificationId);
|
|
assert.equal(status.status, 200);
|
|
assert.equal(status.body.type, 'tax_form');
|
|
assert.ok(['NEEDS_REVIEW', 'AUTO_PASS', 'AUTO_FAIL', 'ERROR'].includes(status.body.status));
|
|
});
|
|
|
|
test('POST /core/verifications rejects file paths not owned by actor', async () => {
|
|
const app = createApp();
|
|
const res = await request(app)
|
|
.post('/core/verifications')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
type: 'attire',
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/other-user/not-allowed.jpg',
|
|
rules: { attireType: 'shoes' },
|
|
});
|
|
|
|
assert.equal(res.status, 403);
|
|
assert.equal(res.body.code, 'FORBIDDEN');
|
|
});
|
|
|
|
test('POST /core/verifications/:id/review finalizes verification', async () => {
|
|
const app = createApp();
|
|
const created = await request(app)
|
|
.post('/core/verifications')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
type: 'certification',
|
|
subjectType: 'staff',
|
|
subjectId: 'staff_1',
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/test-user/cert.pdf',
|
|
rules: { certType: 'food_safety' },
|
|
});
|
|
|
|
const status = await waitForMachineStatus(app, created.body.verificationId);
|
|
assert.equal(status.status, 200);
|
|
|
|
const reviewed = await request(app)
|
|
.post(`/core/verifications/${created.body.verificationId}/review`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
decision: 'APPROVED',
|
|
note: 'Looks good',
|
|
reasonCode: 'MANUAL_REVIEW',
|
|
});
|
|
|
|
assert.equal(reviewed.status, 200);
|
|
assert.equal(reviewed.body.status, 'APPROVED');
|
|
assert.equal(reviewed.body.review.decision, 'APPROVED');
|
|
});
|
|
|
|
test('POST /core/verifications/:id/retry requeues verification', async () => {
|
|
const app = createApp();
|
|
const created = await request(app)
|
|
.post('/core/verifications')
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({
|
|
type: 'government_id',
|
|
subjectType: 'staff',
|
|
subjectId: 'staff_1',
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/test-user/id-front.jpg',
|
|
rules: {},
|
|
});
|
|
|
|
const status = await waitForMachineStatus(app, created.body.verificationId);
|
|
assert.equal(status.status, 200);
|
|
|
|
const retried = await request(app)
|
|
.post(`/core/verifications/${created.body.verificationId}/retry`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.send({});
|
|
|
|
assert.equal(retried.status, 202);
|
|
assert.equal(retried.body.status, 'PENDING');
|
|
});
|
|
|
|
test('verification access is denied to a different actor by default', async () => {
|
|
const created = await createVerificationJob({
|
|
actorUid: 'owner-user',
|
|
payload: {
|
|
type: 'attire',
|
|
subjectType: 'staff',
|
|
subjectId: 'staff_1',
|
|
fileUri: 'gs://krow-workforce-dev-private/uploads/owner-user/attire.jpg',
|
|
rules: { attireType: 'shoes' },
|
|
},
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => getVerificationJob(created.verificationId, 'foreign-user'),
|
|
(error) => {
|
|
assert.equal(error.code, 'FORBIDDEN');
|
|
return true;
|
|
}
|
|
);
|
|
});
|