Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
Resolve A01-A10 and C01-C07 from the backend audit. Preserve auth failures, forced candle reconciliation and forecast freshness. Validate MOEX responses and nested HTTP errors; remove unused legacy paths. Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability. Record the remaining macOS native SQLite abort in the audit.
181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import { INestApplication } from '@nestjs/common';
|
|
import { Test } from '@nestjs/testing';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { resolve } from 'node:path';
|
|
import { AppModule } from '../src/app.module';
|
|
import { configureApp } from '../src/main';
|
|
import { createOpenApiDocument } from '../src/openapi';
|
|
|
|
describe('OpenAPI contract (integration)', () => {
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
const module = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
app = module.createNestApplication();
|
|
configureApp(app);
|
|
await app.init();
|
|
});
|
|
|
|
afterAll(() => app.close());
|
|
|
|
it('distinguishes calendar screen dates from operation instants in requests and responses', () => {
|
|
const document = createOpenApiDocument(app);
|
|
const schemas = document.components?.schemas;
|
|
const screenSchema = {
|
|
oneOf: [
|
|
{ $ref: '#/components/schemas/IncomeInvestmentScreenDto' },
|
|
{ $ref: '#/components/schemas/EventsInvestmentScreenDto' },
|
|
{ $ref: '#/components/schemas/TimestampInvestmentScreenDto' },
|
|
],
|
|
discriminator: {
|
|
propertyName: 'screen',
|
|
mapping: {
|
|
income: '#/components/schemas/IncomeInvestmentScreenDto',
|
|
events: '#/components/schemas/EventsInvestmentScreenDto',
|
|
operations: '#/components/schemas/TimestampInvestmentScreenDto',
|
|
},
|
|
},
|
|
};
|
|
expect(
|
|
document.paths['/api/v1/tbank-sync'].post?.requestBody,
|
|
).toMatchObject({
|
|
content: { 'application/json': { schema: screenSchema } },
|
|
});
|
|
expect(schemas?.TbankSyncAcceptedDto).toMatchObject({
|
|
properties: { scope: screenSchema },
|
|
});
|
|
expect(schemas?.TbankSyncStatusDto).toMatchObject({
|
|
properties: { scope: { ...screenSchema, nullable: true } },
|
|
});
|
|
for (const [model, screen] of [
|
|
['IncomeInvestmentScreenDto', 'income'],
|
|
['EventsInvestmentScreenDto', 'events'],
|
|
]) {
|
|
const schema = schemas?.[model];
|
|
assert(schema && !('$ref' in schema));
|
|
expect(schema.required).toContain('screen');
|
|
expect(schema.required).toContain('accountId');
|
|
expect(schema).toMatchObject({
|
|
properties: {
|
|
screen: { enum: [screen] },
|
|
from: { type: 'string', format: 'date' },
|
|
to: { type: 'string', format: 'date' },
|
|
},
|
|
});
|
|
}
|
|
expect(schemas?.TimestampInvestmentScreenDto).toMatchObject({
|
|
properties: {
|
|
from: { type: 'string', format: 'date-time' },
|
|
to: { type: 'string', format: 'date-time' },
|
|
},
|
|
});
|
|
expect(schemas).not.toHaveProperty('InvestmentScreenDto');
|
|
for (const scope of ['all', '{accountId}']) {
|
|
for (const [resource, format] of [
|
|
['income-analytics', 'date'],
|
|
['events', 'date'],
|
|
['operations', 'date-time'],
|
|
]) {
|
|
const parameters =
|
|
document.paths[`/api/v1/brokerage-accounts/${scope}/${resource}`].get
|
|
?.parameters;
|
|
for (const name of ['from', 'to'])
|
|
expect(
|
|
parameters?.find(
|
|
(parameter) => 'name' in parameter && parameter.name === name,
|
|
),
|
|
).toMatchObject({
|
|
name,
|
|
in: 'query',
|
|
schema: { format },
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
it('exposes required semantic income dependency roles', () => {
|
|
const schemas = createOpenApiDocument(app).components?.schemas;
|
|
expect(schemas?.IncomeAnalyticsResourceDto).toMatchObject({
|
|
properties: {
|
|
meta: { $ref: '#/components/schemas/IncomeAnalyticsMetaDto' },
|
|
},
|
|
});
|
|
const meta = schemas?.IncomeAnalyticsMetaDto;
|
|
assert(meta && !('$ref' in meta));
|
|
expect(meta.required).toContain('dependencies');
|
|
expect(meta).toMatchObject({
|
|
properties: {
|
|
dependencies: {
|
|
type: 'array',
|
|
items: { $ref: '#/components/schemas/IncomeDependencyDto' },
|
|
},
|
|
},
|
|
});
|
|
const dependency = schemas?.IncomeDependencyDto;
|
|
assert(dependency && !('$ref' in dependency));
|
|
expect(dependency.required).toContain('role');
|
|
expect(dependency).toMatchObject({
|
|
properties: {
|
|
role: { type: 'string', enum: ['selected', 'previous', 'year'] },
|
|
},
|
|
});
|
|
});
|
|
|
|
it('generates the runtime contract without server startup or deployment configuration', async () => {
|
|
expect(
|
|
createOpenApiDocument(app).components?.schemas?.ProblemDetailsDto,
|
|
).toMatchObject({
|
|
properties: {
|
|
errors: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
required: ['pointer', 'detail'],
|
|
properties: {
|
|
pointer: { type: 'string' },
|
|
detail: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const directory = await mkdtemp(resolve(tmpdir(), 'wealthledger-openapi-'));
|
|
try {
|
|
const networkGuard = resolve(directory, 'no-network.cjs');
|
|
await writeFile(
|
|
networkGuard,
|
|
`
|
|
const net = require('node:net');
|
|
const forbidden = () => { throw new Error('Schema generation must not use the network'); };
|
|
net.Server.prototype.listen = forbidden;
|
|
net.Socket.prototype.connect = forbidden;
|
|
`,
|
|
);
|
|
execFileSync(
|
|
process.execPath,
|
|
[
|
|
'--require',
|
|
networkGuard,
|
|
resolve(__dirname, '../scripts/generate-openapi.cjs'),
|
|
],
|
|
{
|
|
cwd: directory,
|
|
env: { PATH: process.env.PATH },
|
|
timeout: 30_000,
|
|
},
|
|
);
|
|
const generated = JSON.parse(
|
|
await readFile(resolve(directory, 'openapi.json'), 'utf8'),
|
|
) as unknown;
|
|
expect(generated).toEqual(createOpenApiDocument(app));
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|