Files
backend/test/eslint-config.cjs
T
ksv741 a7e98c0ec3
Backend verify / commits (pull_request) Successful in 1m41s
Backend verify / security (pull_request) Successful in 1m13s
Backend verify / verify (pull_request) Successful in 6m50s
refactor: enforce repository boundary in application (WealthLedger/backend#171)
2026-11-04 16:53:41 +03:00

148 lines
8.6 KiB
JavaScript

const { ESLint } = require('eslint');
const assert = require('node:assert/strict');
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs');
const { basename, join } = require('node:path');
const { test } = require('node:test');
const eslint = new ESLint();
test('runtime scripts resolve build imports against sources before dist exists', async () => {
const directory = mkdtempSync(join(process.cwd(), '.eslint-require-'));
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Test-owned temporary source with no compiled counterpart.
writeFileSync(join(directory, 'entry.ts'), 'export const value = true;');
for (const [filePath, prefix] of [
['scripts/generate-openapi.cjs', '../'],
['test/runtime-check.cjs', '../'],
['test/fixtures/tbank-sync-crash.cjs', '../../'],
]) {
const source = `${prefix}dist/${basename(directory)}/entry`;
const [result] = await eslint.lintText(
`require('${source}.js'); require('${source}'); require('node:fs'); require('eslint'); require('${source}-missing.js'); require('${prefix}missing-local.cjs'); require('wealthledger-missing-package');`,
{ filePath },
);
assert.equal(result.fatalErrorCount, 0, filePath);
const missing = result.messages.filter(({ ruleId }) => ruleId === 'n/no-missing-require');
assert.equal(missing.length, 3, filePath);
for (const target of [`${source}-missing.js`, `${prefix}missing-local.cjs`, 'wealthledger-missing-package']) {
assert.ok(missing.some(({ message }) => message.includes(`'${target}'`)), target);
}
}
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
// Protect the quality gate's file scopes when configs or plugin versions change.
test('typed application code enforces async and unsafe-type checks', async () => {
const [result] = await eslint.lintText(
'export const unsafe: any = 1; Promise.resolve(unsafe);',
{ filePath: 'src/main.ts' },
);
const rules = new Set(result.messages.map(({ ruleId }) => ruleId));
assert.ok(rules.has('@typescript-eslint/no-explicit-any'));
assert.ok(rules.has('@typescript-eslint/no-floating-promises'));
});
test('framework and runtime rules reach their intended files', async () => {
const app = await eslint.calculateConfigForFile('src/main.ts');
const spec = await eslint.calculateConfigForFile('test/app.e2e-spec.ts');
const script = await eslint.calculateConfigForFile('scripts/generate-openapi.cjs');
const config = await eslint.calculateConfigForFile('eslint.config.mjs');
assert.equal(app.rules['@darraghor/nestjs-typed/validation-pipe-should-use-forbid-unknown'][0], 2);
assert.equal(spec.rules['jest/no-focused-tests'][0], 2);
assert.equal(app.rules['jest/no-focused-tests'], undefined);
assert.equal(app.languageOptions.globals.expect, undefined);
assert.equal(config.languageOptions.globals.require, 'off');
for (const profile of [app, spec, script, config]) {
assert.equal(profile.rules['n/no-deprecated-api'][0], 2);
assert.equal(profile.rules['security/detect-eval-with-expression'][0], 1);
assert.equal(profile.rules['regexp/no-super-linear-backtracking'][0], 2);
assert.equal(profile.rules['sonarjs/cognitive-complexity'][0], 2);
assert.equal(profile.rules['import-x/no-unresolved'][0], 2);
assert.equal(profile.rules['perfectionist/sort-imports'][0], 2);
assert.equal(profile.rules['@stylistic/semi'][0], 2);
}
});
test('focused tests are rejected and generated code is excluded', async () => {
const [result] = await eslint.lintText(
'it.only(\'example\', () => { expect(1).toBe(1); });',
{ filePath: 'test/app.e2e-spec.ts' },
);
assert.ok(result.messages.some(({ ruleId }) => ruleId === 'jest/no-focused-tests'));
assert.ok(await eslint.isPathIgnored('generated/prisma/client.ts'));
assert.ok(await eslint.isPathIgnored('dist/src/main.js'));
});
test('numeric literals require names, including previously ignored numbers and bigints', async () => {
const literals = [-60, -24, -10, -2, -1, 0, 1, 2, 10, 24, 60].map(String);
for (const literal of [...literals, ...literals.map((value) => `${value}n`)]) {
const [inline] = await eslint.lintText(
`export function value() { return ${literal}; }`,
{ filePath: 'src/main.ts' },
);
assert.ok(inline.messages.some(({ ruleId }) => ruleId === '@typescript-eslint/no-magic-numbers'), literal);
const [named] = await eslint.lintText(
`const boundary = ${literal}; export function value() { return boundary; }`,
{ filePath: 'src/main.ts' },
);
assert.ok(!named.messages.some(({ ruleId }) => ruleId === '@typescript-eslint/no-magic-numbers'), literal);
}
});
test('architecture imports enforce direct boundaries without banning DTOs or pure protocol inputs', async () => {
const domain = 'src/modules/banking/bank-cards/domain/rub-money.ts';
const controller = 'src/modules/auth/presentation/auth.controller.ts';
const useCase = 'src/modules/banking/bank-cards/application/get-bank-summary.use-case.ts';
for (const [filePath, code] of [
[domain, 'import { Injectable } from "@nestjs/common";'],
[domain, 'import { MoneyDto } from "../../../../common/money.dto";'],
[domain, 'import { PrismaService } from "../../../../prisma/prisma.service";'],
[controller, 'import { PrismaService } from "../../../prisma/prisma.service";'],
[controller, 'import { TbankClient } from "../../../integrations/tbank/tbank.api";'],
[useCase, 'import { PrismaService } from "../../../../prisma/prisma.service";'],
['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import { PrismaService } from "../../../../prisma/prisma.service";'],
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0);
assert.ok(result.messages.some(({ ruleId }) => ruleId === 'no-restricted-imports'), code);
}
for (const [filePath, code] of [
[domain, 'import type { Money } from "../../../../common/money.types";'],
[domain, 'import { TbankOperationState } from "../../../../integrations/tbank/tbank-protocol";'],
[controller, 'import { AuthService } from "../application/auth.service";'],
[useCase, 'import { BankCardsRepository } from "../infrastructure/bank-cards.repository";'],
[useCase, 'import { BankSummaryQueryDto } from "../presentation/bank-card.dto";'],
['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import type { Prisma } from "../../../../../generated/prisma/client";'],
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0);
assert.ok(result.messages.every(({ ruleId }) => ruleId !== 'no-restricted-imports'), code);
}
});
// Single-use codes must be caught too; fixture literals remain independent wire assertions.
test('machine codes require enum members without banning ordinary text or Prisma selects', async () => {
const eslint = new ESLint();
const filePath = 'src/main.ts';
for (const code of ['export function check(code: string) { return code === "one-off-code"; }', 'export const result = `MOEX_UNAVAILABLE`;', 'export const result = { code: "one-off-code" };', 'export const result = { "errorCode": "one-off-code" };', 'export const result = { code: `dynamic-${Date.now()}` };', 'export const result = "MOEX_UNAVAILABLE";', 'export const result = ["INSTRUMENT_ID_TYPE_FIGI"];', 'export const result = { OPERATION_TYPE_COUPON: "income" };']) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0, code);
assert.ok(result.messages.some(({ ruleId }) => ruleId === 'no-restricted-syntax'), code);
}
for (const code of [
'enum Code { Unavailable = "MOEX_UNAVAILABLE" } export const result = { code: Code.Unavailable };',
'export const select = { code: true, errorCode: true };',
'export const label = "Please try again"; export const key = "JWT_SECRET";',
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0, code);
assert.ok(!result.messages.some(({ ruleId }) => ruleId === 'no-restricted-syntax'), code);
}
const [fixture] = await eslint.lintText('export const result = { code: "MOEX_UNAVAILABLE" };', { filePath: 'src/integrations/moex/moex.client.spec.ts' });
assert.ok(!fixture.messages.some(({ ruleId }) => ruleId === 'no-restricted-syntax'));
const [comparison] = await eslint.lintText('enum Code { Ready = "ready" } export function check(code: Code) { return code === "ready"; }', { filePath });
assert.ok(comparison.messages.some(({ ruleId }) => ruleId === '@typescript-eslint/no-unsafe-enum-comparison'));
});