Files
ftb-project-management/apps/web/lib/v22-api.test.ts

206 lines
7.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { __resetApiAvailabilityForTests, resolveApiBase } from './api';
import {
loadV22RequirementsPage,
loadV22VersionDetailData,
loadV22WorkspaceData,
loadV22XiaobaoWarnings,
} from './v22-api';
test('V2.2 version detail API maps relation rows to existing frontend scope types', async () => {
const originalFetch = globalThis.fetch;
const apiBase = resolveApiBase();
const calls: Array<{ url: string; method: string }> = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
calls.push({ url, method: init?.method ?? 'GET' });
if (url === `${apiBase}/config/ai`) {
return jsonResponse({});
}
if (url === `${apiBase}/v2.2/versions/version-1/detail-data`) {
return jsonResponse({
version: {
id: 'version-1',
productId: 'product-1',
projectId: 'project-1',
name: 'Project A V1.0',
releaseDate: '2026-01-20T00:00:00.000Z',
createdAt: '2026-01-01T00:00:00.000Z',
},
requirements: [{
id: 'req-1',
productId: 'product-1',
projectId: 'project-1',
versionId: 'version-1',
code: 'REQ-001',
title: 'Login',
description: 'Password login',
status: 'adopted',
priority: 1,
type: 'feature',
sourceType: 'customer',
sourceTarget: 'ACME',
platform: 'web, ios',
creatorId: 'member-1',
creator: { id: 'member-1', name: 'Product manager' },
createdAt: '2026-01-01T00:00:00.000Z',
}],
versionPlans: [{
id: 'plan-1',
versionId: 'version-1',
type: 'research',
title: 'Research login',
status: 'pending',
ownerId: 'member-1',
expectedStartAt: '2026-01-01',
expectedEndAt: '2026-01-05T18:00:00.000Z',
requirementCoverage: [{ requirementId: 'req-1', status: 'completed', updatedAt: '2026-01-02T00:00:00.000Z', updatedBy: 'member-1' }],
logs: [],
createdAt: '2026-01-01T00:00:00.000Z',
}],
devTasks: [{
id: 'dev-1',
versionId: 'version-1',
requirementId: 'req-1',
categoryId: 'cat-dev',
code: 'DEV-001',
title: 'Build login',
description: 'Implement login',
status: 'in_progress',
priority: 2,
assigneeId: 'member-2',
creatorId: 'member-1',
isBlocked: false,
expectedStartAt: '2026-01-02T09:00:00.000Z',
expectedEndAt: '2026-01-03T18:00:00.000Z',
startDate: '2026-01-02T09:00:00.000Z',
completedAt: null,
estimateHours: 8,
references: [{ type: 'requirement', id: 'req-1', label: 'REQ-001' }],
aiDraft: false,
createdAt: '2026-01-02T08:00:00.000Z',
updatedAt: '2026-01-02T09:00:00.000Z',
}],
testCases: [{
id: 'tc-1',
versionId: 'version-1',
requirementId: 'req-1',
categoryId: 'cat-test',
code: 'TC-001',
title: 'Verify login',
description: 'Check password login',
status: 'pending',
roundNo: 1,
priority: 3,
assigneeId: 'member-3',
creatorId: 'member-1',
references: [],
aiDraft: false,
createdAt: '2026-01-03T08:00:00.000Z',
updatedAt: '2026-01-03T08:00:00.000Z',
}],
bugs: [{
id: 'bug-1',
versionId: 'version-1',
testCaseId: 'tc-1',
code: 'BUG-001',
title: 'Login error',
description: 'Wrong prompt',
status: 'open',
severity: 'major',
priority: 0,
assigneeId: 'member-2',
reporterId: 'member-3',
createdAt: '2026-01-04T08:00:00.000Z',
updatedAt: '2026-01-04T08:00:00.000Z',
}],
});
}
throw new Error(`Unexpected fetch ${url}`);
}) as typeof fetch;
try {
__resetApiAvailabilityForTests();
const result = await loadV22VersionDetailData('version-1');
assert.equal(calls[1].url, `${apiBase}/v2.2/versions/version-1/detail-data`);
assert.equal(result.scope.requirements[0].typeId, 'feature');
assert.deepEqual(result.scope.requirements[0].platforms, ['web', 'ios']);
assert.equal(result.scope.requirements[0].priority, 'P1');
assert.equal(result.scope.requirements[0].creator, 'Product manager');
assert.equal(result.scope.devTasks[0].taskNo, 'DEV-001');
assert.equal(result.scope.devTasks[0].actualStartAt, '2026-01-02T09:00:00.000Z');
assert.equal(result.scope.devTasks[0].actualEndAt, undefined);
assert.equal(result.scope.devTasks[0].createdBy, 'member-1');
assert.equal(result.scope.testCases[0].caseNo, 'TC-001');
assert.equal(result.scope.testCases[0].createdBy, 'member-1');
assert.equal(result.scope.bugs[0].bugNo, 'BUG-001');
assert.equal(result.scope.bugs[0].reportedBy, 'member-3');
assert.equal(result.scope.plans[0].owner, 'member-1');
assert.equal(result.scope.plans[0].startTime, '2026-01-01T00:00:00.000Z');
assert.equal(result.scope.plansByType.research[0].id, 'plan-1');
assert.equal(result.scope.requirementIdSet.has('req-1'), true);
} finally {
globalThis.fetch = originalFetch;
}
});
test('V2.2 list APIs build scoped query strings without full-table reads', async () => {
const originalFetch = globalThis.fetch;
const apiBase = resolveApiBase();
const urls: string[] = [];
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
urls.push(url);
if (url === `${apiBase}/config/ai`) return jsonResponse({});
if (url.startsWith(`${apiBase}/v2.2/requirements?`)) return jsonResponse({ items: [], nextCursor: 'req-2' });
if (url.startsWith(`${apiBase}/v2.2/workspace?`)) return jsonResponse({ versionPlans: [], devTasks: [], testCases: [], bugs: [] });
if (url.startsWith(`${apiBase}/v2.2/xiaobao-warning?`)) return jsonResponse([]);
throw new Error(`Unexpected fetch ${url}`);
}) as typeof fetch;
try {
__resetApiAvailabilityForTests();
await loadV22RequirementsPage({
productId: 'product-1',
projectId: 'project-1',
status: 'adopted',
priority: 'P1',
type: 'feature',
q: '登录',
sort: 'created_at_asc',
cursor: 'req-1',
limit: 50,
});
await loadV22WorkspaceData('member-1');
await loadV22XiaobaoWarnings({ userId: 'member-1' });
const requirementUrl = new URL(urls[1]);
assert.equal(requirementUrl.pathname.endsWith('/v2.2/requirements'), true);
assert.equal(requirementUrl.searchParams.get('productId'), 'product-1');
assert.equal(requirementUrl.searchParams.get('projectId'), 'project-1');
assert.equal(requirementUrl.searchParams.get('status'), 'adopted');
assert.equal(requirementUrl.searchParams.get('priority'), 'P1');
assert.equal(requirementUrl.searchParams.get('type'), 'feature');
assert.equal(requirementUrl.searchParams.get('q'), '登录');
assert.equal(requirementUrl.searchParams.get('sort'), 'created_at_asc');
assert.equal(requirementUrl.searchParams.get('cursor'), 'req-1');
assert.equal(requirementUrl.searchParams.get('limit'), '50');
assert.equal(new URL(urls[2]).searchParams.get('userId'), 'member-1');
assert.equal(new URL(urls[3]).searchParams.get('userId'), 'member-1');
} finally {
globalThis.fetch = originalFetch;
}
});
function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}