Merge pull request 'fix(lint): bring the backend test suites into scope (#298)' (#299) from fix/298-lint-backend-tests into main
Reviewed-on: #299
This commit was merged in pull request #299.
This commit is contained in:
@@ -50,9 +50,6 @@ export default tseslint.config(
|
|||||||
),
|
),
|
||||||
|
|
||||||
{
|
{
|
||||||
// `tests/` is deliberately out of scope for now: tsconfig.json only includes
|
|
||||||
// `src`, so type-aware linting has no program for the test files, and
|
|
||||||
// widening it is a separate change with its own violation count.
|
|
||||||
files: ['src/**/*.ts'],
|
files: ['src/**/*.ts'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.node,
|
globals: globals.node,
|
||||||
@@ -71,5 +68,73 @@ export default tseslint.config(
|
|||||||
{ checksVoidReturn: { attributes: false } },
|
{ checksVoidReturn: { attributes: false } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// The test suites, in scope since #298. They had never been linted at all:
|
||||||
|
// this config said tests were out of scope because tsconfig.json includes
|
||||||
|
// only `src`, and that stayed true for long enough that two defects lived
|
||||||
|
// here undetected — a unit test that opened a real TLS connection to Gmail
|
||||||
|
// on every run, and integration tests that mocked the shared pg pool and
|
||||||
|
// made a suite unrunnable. Neither is something lint would necessarily have
|
||||||
|
// caught, but neither was ever looked at.
|
||||||
|
//
|
||||||
|
// `project` rather than `projectService`, for the reason the frontend's
|
||||||
|
// equivalent block records: the service resolves each file to the nearest
|
||||||
|
// tsconfig.json, which for tests/ is the one that excludes them, and every
|
||||||
|
// file then errors as not part of a project.
|
||||||
|
files: ['tests/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.node, ...globals.jest },
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.test.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// The same rule src is held to, and it matters at least as much here.
|
||||||
|
// An unawaited promise in a test does not fail the test — it passes,
|
||||||
|
// having asserted nothing, and the failure surfaces later as a suite that
|
||||||
|
// will not exit.
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/no-misused-promises': [
|
||||||
|
'error',
|
||||||
|
{ checksVoidReturn: { attributes: false } },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Everything below is switched off for tests rather than left as a
|
||||||
|
// warning, on #60's argument: bringing these files in scope produced 77
|
||||||
|
// warnings, of which 60 were rules that cannot be true in a test. A rule
|
||||||
|
// that cannot be true here is noise, and noise hides the rules that can.
|
||||||
|
// What is left is signal — unused variables, useless escapes, a regex
|
||||||
|
// worth a second look.
|
||||||
|
|
||||||
|
// 41 of the 77. Test credentials are the entire point of a test, and this
|
||||||
|
// project's own rule is that they must live only in test paths — which is
|
||||||
|
// here. Flagging them where they belong trains a reader to skip the rule
|
||||||
|
// where they do not.
|
||||||
|
'sonarjs/no-hardcoded-passwords': 'off',
|
||||||
|
|
||||||
|
// Stub servers and fixtures: `http://127.0.0.1:<port>`. There is no
|
||||||
|
// transport to secure between a test and a socket it opened itself.
|
||||||
|
'sonarjs/no-clear-text-protocols': 'off',
|
||||||
|
|
||||||
|
// 203.0.113.5 is TEST-NET-3, reserved by RFC 5737 for exactly this. A
|
||||||
|
// documentation address is the correct thing to hardcode.
|
||||||
|
'sonarjs/no-hardcoded-ip': 'off',
|
||||||
|
|
||||||
|
// `os.tmpdir()`, via mkdtemp, which is how these suites get a scratch
|
||||||
|
// uploads directory they can delete afterwards.
|
||||||
|
'sonarjs/publicly-writable-directories': 'off',
|
||||||
|
|
||||||
|
// Math.random for a run id. Nothing here is a secret; it only has to not
|
||||||
|
// collide with a parallel worker.
|
||||||
|
'sonarjs/pseudo-random': 'off',
|
||||||
|
|
||||||
|
// Sorting two string arrays to compare them is how several guards assert
|
||||||
|
// set equality. The locale-aware comparator the rule wants would change
|
||||||
|
// nothing except the reading.
|
||||||
|
'sonarjs/no-alphabetical-sort': 'off',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "eslint src scripts",
|
"lint": "eslint src scripts tests",
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/server.js",
|
||||||
"dev": "tsx watch src/server.ts",
|
"dev": "tsx watch src/server.ts",
|
||||||
"test": "npm run test:unit",
|
"test": "npm run test:unit",
|
||||||
|
|||||||
@@ -123,8 +123,16 @@ export async function resetDb(): Promise<void> {
|
|||||||
// Same reasoning, same failure, for the intake settings (#227): a ceiling of
|
// Same reasoning, same failure, for the intake settings (#227): a ceiling of
|
||||||
// 1 left behind by the ceiling suite makes every later submission refuse with
|
// 1 left behind by the ceiling suite makes every later submission refuse with
|
||||||
// a 503, in files that never mention a ceiling.
|
// a 503, in files that never mention a ceiling.
|
||||||
|
//
|
||||||
|
// ESCAPE, and not a backslash, because the backslash that used to be here did
|
||||||
|
// nothing. In a JavaScript string 'email\_%' is 'email_%', and `_` in SQL LIKE
|
||||||
|
// matches any single character — so the pattern meant "email plus any one
|
||||||
|
// character", not "email_". It happened to delete the right rows only because
|
||||||
|
// no other key begins with those letters followed by something else; a
|
||||||
|
// setting called emailing_enabled would have been swept away silently. Found
|
||||||
|
// by no-useless-escape the first time lint was pointed at tests/ (#298).
|
||||||
await testPool.query(
|
await testPool.query(
|
||||||
`DELETE FROM admin_settings WHERE key LIKE 'email\_%' OR key LIKE 'intake\_%'`
|
`DELETE FROM admin_settings WHERE key LIKE 'email!_%' ESCAPE '!' OR key LIKE 'intake!_%' ESCAPE '!'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,8 +151,6 @@ describe('what it refuses', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('leaves nothing on the volume when it refuses the content', async () => {
|
it('leaves nothing on the volume when it refuses the content', async () => {
|
||||||
const before: string[] = [];
|
|
||||||
|
|
||||||
await createItem().attach('images', HTML_BYTES, {
|
await createItem().attach('images', HTML_BYTES, {
|
||||||
filename: 'photo.jpg',
|
filename: 'photo.jpg',
|
||||||
contentType: 'image/jpeg'
|
contentType: 'image/jpeg'
|
||||||
@@ -164,8 +162,6 @@ describe('what it refuses', () => {
|
|||||||
// A rejection must take the whole request with it. Accepting the good file
|
// A rejection must take the whole request with it. Accepting the good file
|
||||||
// from a refused upload would leave a file behind that nothing references.
|
// from a refused upload would leave a file behind that nothing references.
|
||||||
it('discards the valid files from a request that also carried an invalid one', async () => {
|
it('discards the valid files from a request that also carried an invalid one', async () => {
|
||||||
const before: string[] = [];
|
|
||||||
|
|
||||||
const res = await createItem()
|
const res = await createItem()
|
||||||
.attach('images', REAL_PNG, { filename: 'good.png', contentType: 'image/png' })
|
.attach('images', REAL_PNG, { filename: 'good.png', contentType: 'image/png' })
|
||||||
.attach('images', HTML_BYTES, { filename: 'bad.jpg', contentType: 'image/jpeg' });
|
.attach('images', HTML_BYTES, { filename: 'bad.jpg', contentType: 'image/jpeg' });
|
||||||
@@ -209,8 +205,6 @@ describe('the uploads directory', () => {
|
|||||||
*/
|
*/
|
||||||
describe('what it leaves on disk', () => {
|
describe('what it leaves on disk', () => {
|
||||||
it('removes the upload when the request is refused for its other fields', async () => {
|
it('removes the upload when the request is refused for its other fields', async () => {
|
||||||
const before: string[] = [];
|
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/admin/items')
|
.post('/api/admin/items')
|
||||||
.field('name', 'Photographed thing')
|
.field('name', 'Photographed thing')
|
||||||
@@ -226,8 +220,6 @@ describe('what it leaves on disk', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('removes the upload when the tags field is refused', async () => {
|
it('removes the upload when the tags field is refused', async () => {
|
||||||
const before: string[] = [];
|
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/admin/items')
|
.post('/api/admin/items')
|
||||||
.field('name', 'Photographed thing')
|
.field('name', 'Photographed thing')
|
||||||
@@ -243,8 +235,6 @@ describe('what it leaves on disk', () => {
|
|||||||
// The accepted case must not be swept up by the same cleanup: these files are
|
// The accepted case must not be swept up by the same cleanup: these files are
|
||||||
// the ones the item now points at.
|
// the ones the item now points at.
|
||||||
it('keeps the upload when the request succeeds', async () => {
|
it('keeps the upload when the request succeeds', async () => {
|
||||||
const before: string[] = [];
|
|
||||||
|
|
||||||
const res = await createItem().attach('images', REAL_PNG, {
|
const res = await createItem().attach('images', REAL_PNG, {
|
||||||
filename: 'photo.png',
|
filename: 'photo.png',
|
||||||
contentType: 'image/png'
|
contentType: 'image/png'
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"//": "Exists so ESLint has a TypeScript program covering tests/. tsconfig.json includes only `src`, which is why tests were out of scope for linting until #298 — type-aware rules had nothing to resolve them against. Separate from tsconfig.json rather than widening its `include`, because that one drives `npm run build` and emits to dist/: pulling the suite into the build output would ship the tests. Mirrors frontend/tsconfig.test.json, which exists for the same reason (#137). The files are already type-checked at run time by ts-jest; this adds nothing to that, only to what the linter can see.",
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["tests", "src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user