routes tests
This commit is contained in:
parent
ccb05ac0f4
commit
50edefbebe
34
orchestrator/src/server/api/routes/database.test.ts
Normal file
34
orchestrator/src/server/api/routes/database.test.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Database API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('clears jobs and pipeline runs', async () => {
|
||||
const { createJob } = await import('../../repositories/jobs.js');
|
||||
await createJob({
|
||||
source: 'manual',
|
||||
title: 'Cleanup Role',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/job/cleanup',
|
||||
jobDescription: 'Test description',
|
||||
});
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/database`, { method: 'DELETE' });
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.jobsDeleted).toBe(1);
|
||||
});
|
||||
});
|
||||
98
orchestrator/src/server/api/routes/jobs.test.ts
Normal file
98
orchestrator/src/server/api/routes/jobs.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Jobs API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('lists jobs and supports status filtering', async () => {
|
||||
const { createJob } = await import('../../repositories/jobs.js');
|
||||
const job = await createJob({
|
||||
source: 'manual',
|
||||
title: 'Test Role',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/job/1',
|
||||
jobDescription: 'Test description',
|
||||
});
|
||||
|
||||
const listRes = await fetch(`${baseUrl}/api/jobs`);
|
||||
const listBody = await listRes.json();
|
||||
expect(listBody.success).toBe(true);
|
||||
expect(listBody.data.total).toBe(1);
|
||||
expect(listBody.data.jobs[0].id).toBe(job.id);
|
||||
|
||||
const filteredRes = await fetch(`${baseUrl}/api/jobs?status=skipped`);
|
||||
const filteredBody = await filteredRes.json();
|
||||
expect(filteredBody.data.total).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 404 for missing jobs', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/jobs/missing-id`);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('validates job updates and supports skip/delete flow', async () => {
|
||||
const { createJob } = await import('../../repositories/jobs.js');
|
||||
const job = await createJob({
|
||||
source: 'manual',
|
||||
title: 'Test Role',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/job/2',
|
||||
jobDescription: 'Test description',
|
||||
});
|
||||
|
||||
const badRes = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ suitabilityScore: 1000 }),
|
||||
});
|
||||
expect(badRes.status).toBe(400);
|
||||
|
||||
const skipRes = await fetch(`${baseUrl}/api/jobs/${job.id}/skip`, { method: 'POST' });
|
||||
const skipBody = await skipRes.json();
|
||||
expect(skipBody.data.status).toBe('skipped');
|
||||
|
||||
const deleteRes = await fetch(`${baseUrl}/api/jobs/status/skipped`, { method: 'DELETE' });
|
||||
const deleteBody = await deleteRes.json();
|
||||
expect(deleteBody.data.count).toBe(1);
|
||||
});
|
||||
|
||||
it('applies a job and syncs to Notion', async () => {
|
||||
const { createNotionEntry } = await import('../../services/notion.js');
|
||||
vi.mocked(createNotionEntry).mockResolvedValue({ pageId: 'page-123' });
|
||||
|
||||
const { createJob } = await import('../../repositories/jobs.js');
|
||||
const job = await createJob({
|
||||
source: 'manual',
|
||||
title: 'Test Role',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/job/3',
|
||||
jobDescription: 'Test description',
|
||||
});
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/jobs/${job.id}/apply`, { method: 'POST' });
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.status).toBe('applied');
|
||||
expect(body.data.notionPageId).toBe('page-123');
|
||||
expect(body.data.appliedAt).toBeTruthy();
|
||||
expect(createNotionEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: job.id,
|
||||
title: job.title,
|
||||
employer: job.employer,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
64
orchestrator/src/server/api/routes/manual-jobs.test.ts
Normal file
64
orchestrator/src/server/api/routes/manual-jobs.test.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Manual jobs API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('infers manual jobs and rejects empty payloads', async () => {
|
||||
const badRes = await fetch(`${baseUrl}/api/manual-jobs/infer`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(badRes.status).toBe(400);
|
||||
|
||||
const { inferManualJobDetails } = await import('../../services/manualJob.js');
|
||||
vi.mocked(inferManualJobDetails).mockResolvedValue({
|
||||
job: { title: 'Backend Engineer', employer: 'Acme' },
|
||||
warning: null,
|
||||
});
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/manual-jobs/infer`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jobDescription: 'Role description' }),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.job.title).toBe('Backend Engineer');
|
||||
});
|
||||
|
||||
it('imports manual jobs and generates a fallback URL', async () => {
|
||||
const { scoreJobSuitability } = await import('../../services/scorer.js');
|
||||
vi.mocked(scoreJobSuitability).mockResolvedValue({ score: 88, reason: 'Strong fit' });
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/manual-jobs/import`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
job: {
|
||||
title: 'Backend Engineer',
|
||||
employer: 'Acme',
|
||||
jobDescription: 'Great role',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.source).toBe('manual');
|
||||
expect(body.data.jobUrl).toMatch(/^manual:\/\//);
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
});
|
||||
});
|
||||
66
orchestrator/src/server/api/routes/pipeline.test.ts
Normal file
66
orchestrator/src/server/api/routes/pipeline.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Pipeline API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('reports pipeline status', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/pipeline/status`);
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.isRunning).toBe(false);
|
||||
expect(body.data.lastRun).toBeNull();
|
||||
});
|
||||
|
||||
it('validates pipeline run payloads', async () => {
|
||||
const badRun = await fetch(`${baseUrl}/api/pipeline/run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ minSuitabilityScore: 120 }),
|
||||
});
|
||||
expect(badRun.status).toBe(400);
|
||||
|
||||
const { runPipeline } = await import('../../pipeline/index.js');
|
||||
const runRes = await fetch(`${baseUrl}/api/pipeline/run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topN: 5, sources: ['gradcracker'] }),
|
||||
});
|
||||
const runBody = await runRes.json();
|
||||
expect(runBody.success).toBe(true);
|
||||
expect(runPipeline).toHaveBeenCalledWith({ topN: 5, sources: ['gradcracker'] });
|
||||
});
|
||||
|
||||
it('streams pipeline progress over SSE', async () => {
|
||||
const controller = new AbortController();
|
||||
const res = await fetch(`${baseUrl}/api/pipeline/progress`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toContain('text/event-stream');
|
||||
|
||||
const reader = res.body?.getReader();
|
||||
if (reader) {
|
||||
const chunk = await reader.read();
|
||||
controller.abort();
|
||||
await reader.cancel();
|
||||
const text = new TextDecoder().decode(chunk.value);
|
||||
expect(text).toContain('data:');
|
||||
} else {
|
||||
controller.abort();
|
||||
}
|
||||
});
|
||||
});
|
||||
25
orchestrator/src/server/api/routes/profile.test.ts
Normal file
25
orchestrator/src/server/api/routes/profile.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Profile API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('returns base resume projects', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/profile/projects`);
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
45
orchestrator/src/server/api/routes/settings.test.ts
Normal file
45
orchestrator/src/server/api/routes/settings.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Settings API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('returns settings with defaults', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/settings`);
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.defaultModel).toBe('test-model');
|
||||
expect(Array.isArray(body.data.searchTerms)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid settings updates and persists overrides', async () => {
|
||||
const badPatch = await fetch(`${baseUrl}/api/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jobspyResultsWanted: 9999 }),
|
||||
});
|
||||
expect(badPatch.status).toBe(400);
|
||||
|
||||
const patchRes = await fetch(`${baseUrl}/api/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ searchTerms: ['engineer'] }),
|
||||
});
|
||||
const patchBody = await patchRes.json();
|
||||
expect(patchBody.success).toBe(true);
|
||||
expect(patchBody.data.searchTerms).toEqual(['engineer']);
|
||||
expect(patchBody.data.overrideSearchTerms).toEqual(['engineer']);
|
||||
});
|
||||
});
|
||||
112
orchestrator/src/server/api/routes/test-utils.ts
Normal file
112
orchestrator/src/server/api/routes/test-utils.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { mkdtemp, rm } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import type { Server } from 'http';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('../../pipeline/index.js', () => {
|
||||
const progress = {
|
||||
step: 'idle',
|
||||
message: 'Ready',
|
||||
crawlingListPagesProcessed: 0,
|
||||
crawlingListPagesTotal: 0,
|
||||
crawlingJobCardsFound: 0,
|
||||
crawlingJobPagesEnqueued: 0,
|
||||
crawlingJobPagesSkipped: 0,
|
||||
crawlingJobPagesProcessed: 0,
|
||||
jobsDiscovered: 0,
|
||||
jobsScored: 0,
|
||||
jobsProcessed: 0,
|
||||
totalToProcess: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
runPipeline: vi.fn().mockResolvedValue({ success: true, jobsDiscovered: 0, jobsProcessed: 0 }),
|
||||
processJob: vi.fn().mockResolvedValue({ success: true }),
|
||||
summarizeJob: vi.fn().mockResolvedValue({ success: true }),
|
||||
generateFinalPdf: vi.fn().mockResolvedValue({ success: true }),
|
||||
getPipelineStatus: vi.fn(() => ({ isRunning: false })),
|
||||
subscribeToProgress: vi.fn((listener: (data: unknown) => void) => {
|
||||
listener(progress);
|
||||
return () => {};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../services/notion.js', () => ({
|
||||
createNotionEntry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/manualJob.js', () => ({
|
||||
inferManualJobDetails: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/scorer.js', () => ({
|
||||
scoreJobSuitability: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/ukvisajobs.js', () => ({
|
||||
fetchUkVisaJobsPage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/visa-sponsors/index.js', () => ({
|
||||
getStatus: vi.fn(),
|
||||
searchSponsors: vi.fn(),
|
||||
getOrganizationDetails: vi.fn(),
|
||||
downloadLatestCsv: vi.fn(),
|
||||
}));
|
||||
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
export async function startServer(options?: {
|
||||
env?: Record<string, string | undefined>;
|
||||
}): Promise<{
|
||||
server: Server;
|
||||
baseUrl: string;
|
||||
closeDb: () => void;
|
||||
tempDir: string;
|
||||
}> {
|
||||
vi.resetModules();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'job-ops-api-test-'));
|
||||
const envOverrides = options?.env ?? {};
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
DATA_DIR: tempDir,
|
||||
NODE_ENV: 'test',
|
||||
MODEL: 'test-model',
|
||||
JOBSPY_SEARCH_TERMS: 'alpha|beta',
|
||||
...envOverrides,
|
||||
};
|
||||
|
||||
await import('../../db/migrate.js');
|
||||
const { createApp } = await import('../../app.js');
|
||||
const { closeDb } = await import('../../db/index.js');
|
||||
const { getPipelineStatus } = await import('../../pipeline/index.js');
|
||||
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false });
|
||||
|
||||
const app = createApp();
|
||||
const server = app.listen(0);
|
||||
await new Promise<void>((resolve) => server.once('listening', () => resolve()));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Failed to resolve server address');
|
||||
}
|
||||
return {
|
||||
server,
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
closeDb,
|
||||
tempDir,
|
||||
};
|
||||
}
|
||||
|
||||
export async function stopServer(args: {
|
||||
server: Server;
|
||||
closeDb: () => void;
|
||||
tempDir: string;
|
||||
}) {
|
||||
await new Promise<void>((resolve) => args.server.close(() => resolve()));
|
||||
args.closeDb();
|
||||
await rm(args.tempDir, { recursive: true, force: true });
|
||||
process.env = { ...originalEnv };
|
||||
vi.clearAllMocks();
|
||||
}
|
||||
91
orchestrator/src/server/api/routes/ukvisajobs.test.ts
Normal file
91
orchestrator/src/server/api/routes/ukvisajobs.test.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('UK Visa Jobs API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('enforces pagination rules for search', async () => {
|
||||
const badRes = await fetch(`${baseUrl}/api/ukvisajobs/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ searchTerms: ['one', 'two'] }),
|
||||
});
|
||||
expect(badRes.status).toBe(400);
|
||||
});
|
||||
|
||||
it('searches UK Visa Jobs with valid payloads', async () => {
|
||||
const { fetchUkVisaJobsPage } = await import('../../services/ukvisajobs.js');
|
||||
vi.mocked(fetchUkVisaJobsPage).mockResolvedValue({
|
||||
jobs: [
|
||||
{
|
||||
source: 'ukvisajobs',
|
||||
title: 'Engineer',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/visa/1',
|
||||
},
|
||||
],
|
||||
totalJobs: 3,
|
||||
page: 1,
|
||||
pageSize: 2,
|
||||
});
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/ukvisajobs/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: 'engineer' }),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.totalPages).toBe(2);
|
||||
expect(fetchUkVisaJobsPage).toHaveBeenCalledWith({ searchKeyword: 'engineer', page: 1 });
|
||||
});
|
||||
|
||||
it('blocks search when pipeline is running', async () => {
|
||||
const { getPipelineStatus } = await import('../../pipeline/index.js');
|
||||
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: true });
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/ukvisajobs/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: 'engineer' }),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
|
||||
it('imports UK Visa Jobs and reports created vs skipped', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/ukvisajobs/import`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jobs: [
|
||||
{
|
||||
title: 'Engineer',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/visa/2',
|
||||
},
|
||||
{
|
||||
title: 'Engineer Duplicate',
|
||||
employer: 'Acme',
|
||||
jobUrl: 'https://example.com/visa/2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.created).toBe(1);
|
||||
expect(body.data.skipped).toBe(1);
|
||||
});
|
||||
});
|
||||
76
orchestrator/src/server/api/routes/visa-sponsors.test.ts
Normal file
76
orchestrator/src/server/api/routes/visa-sponsors.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Visa sponsors API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('returns status and surfaces update errors', async () => {
|
||||
const { getStatus, downloadLatestCsv } = await import('../../services/visa-sponsors/index.js');
|
||||
vi.mocked(getStatus).mockReturnValue({
|
||||
lastUpdated: null,
|
||||
csvPath: null,
|
||||
totalSponsors: 0,
|
||||
isUpdating: false,
|
||||
nextScheduledUpdate: null,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(downloadLatestCsv).mockResolvedValue({ success: false, message: 'failed' });
|
||||
|
||||
const statusRes = await fetch(`${baseUrl}/api/visa-sponsors/status`);
|
||||
const statusBody = await statusRes.json();
|
||||
expect(statusBody.success).toBe(true);
|
||||
expect(statusBody.data.totalSponsors).toBe(0);
|
||||
|
||||
const updateRes = await fetch(`${baseUrl}/api/visa-sponsors/update`, { method: 'POST' });
|
||||
expect(updateRes.status).toBe(500);
|
||||
});
|
||||
|
||||
it('validates search payloads and handles missing organizations', async () => {
|
||||
const { searchSponsors, getOrganizationDetails } = await import('../../services/visa-sponsors/index.js');
|
||||
vi.mocked(searchSponsors).mockReturnValue([
|
||||
{
|
||||
sponsor: {
|
||||
organisationName: 'Acme',
|
||||
townCity: 'London',
|
||||
county: 'London',
|
||||
typeRating: 'Worker',
|
||||
route: 'Skilled',
|
||||
},
|
||||
score: 95,
|
||||
matchedName: 'acme',
|
||||
},
|
||||
]);
|
||||
vi.mocked(getOrganizationDetails).mockReturnValue([]);
|
||||
|
||||
const badRes = await fetch(`${baseUrl}/api/visa-sponsors/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(badRes.status).toBe(400);
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/visa-sponsors/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: 'Acme' }),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.total).toBe(1);
|
||||
|
||||
const orgRes = await fetch(`${baseUrl}/api/visa-sponsors/organization/Acme`);
|
||||
expect(orgRes.status).toBe(404);
|
||||
});
|
||||
});
|
||||
35
orchestrator/src/server/api/routes/webhook.test.ts
Normal file
35
orchestrator/src/server/api/routes/webhook.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Server } from 'http';
|
||||
import { startServer, stopServer } from './test-utils.js';
|
||||
|
||||
describe.sequential('Webhook API routes', () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, baseUrl, closeDb, tempDir } = await startServer({
|
||||
env: { WEBHOOK_SECRET: 'secret' },
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer({ server, closeDb, tempDir });
|
||||
});
|
||||
|
||||
it('rejects invalid webhook credentials and accepts valid ones', async () => {
|
||||
const badRes = await fetch(`${baseUrl}/api/webhook/trigger`, {
|
||||
method: 'POST',
|
||||
});
|
||||
expect(badRes.status).toBe(401);
|
||||
|
||||
const goodRes = await fetch(`${baseUrl}/api/webhook/trigger`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: 'Bearer secret' },
|
||||
});
|
||||
const goodBody = await goodRes.json();
|
||||
expect(goodBody.success).toBe(true);
|
||||
expect(goodBody.data.message).toBe('Pipeline triggered');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user