import { RuleTester } from '@typescript-eslint/rule-tester'; import { NoUnsafeConnectionTypeCastRule } from './no-unsafe-connection-type-cast.js'; const ruleTester = new RuleTester(); function createNodeCode(inputs: string, outputs: string): string { return ` import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionTypes } from 'n8n-workflow'; export class TestNode implements INodeType { description: INodeTypeDescription = { displayName: 'Test Node', name: 'testNode', group: ['input'], version: 1, description: 'A test node', defaults: { name: 'Test Node' }, inputs: ${inputs}, outputs: ${outputs}, properties: [], }; }`; } /** Lets a test spell the connection key itself, e.g. `'inputs'` or `['inputs']`. */ function createNodeCodeWithProperty(property: string): string { return ` import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionTypes } from 'n8n-workflow'; export class TestNode implements INodeType { description: INodeTypeDescription = { displayName: 'Test Node', name: 'testNode', group: ['input'], version: 1, description: 'A test node', defaults: { name: 'Test Node' }, ${property}, outputs: [NodeConnectionTypes.Main], properties: [], }; }`; } const MAIN = '[NodeConnectionTypes.Main]'; ruleTester.run('no-unsafe-connection-type-cast', NoUnsafeConnectionTypeCastRule, { valid: [ { name: 'class that does not implement INodeType', code: 'export class RegularClass {\n\tsomeProperty = "value";\n}', }, { name: 'node without casts', code: createNodeCode(MAIN, MAIN), }, { name: 'node with empty inputs and outputs', code: createNodeCode('[]', '[]'), }, { name: 'dynamic inputs as a template literal expression', code: createNodeCode('`={{ (() => [NodeConnectionTypes.Main])() }}`', MAIN), }, { name: 'as const narrows rather than erases', code: createNodeCode(`${MAIN} as const`, MAIN), }, { name: 'satisfies keeps the type checked', code: createNodeCode(`${MAIN} satisfies NodeConnectionType[]`, MAIN), }, { name: 'a named type assertion is still checked structurally', code: createNodeCode(`${MAIN} as NodeConnectionType[]`, MAIN), }, { name: 'casts elsewhere in the description are untouched', code: createNodeCode(MAIN, MAIN).replace('version: 1,', 'version: 1 as never,'), }, ], invalid: [ { name: 'as never on inputs', code: createNodeCode("['main'] as never", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main']", MAIN), }, ], }, ], }, { name: 'as never on outputs', code: createNodeCode(MAIN, "['main'] as never"), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'outputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode(MAIN, "['main']"), }, ], }, ], }, { name: 'as never on both inputs and outputs', code: createNodeCode("['main'] as never", "['main'] as never"), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main']", "['main'] as never"), }, ], }, { messageId: 'erasingCast', data: { typeName: 'never', property: 'outputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main'] as never", "['main']"), }, ], }, ], }, { name: 'as any', code: createNodeCode("['main'] as any", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'any', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main']", MAIN), }, ], }, ], }, { name: 'as unknown', code: createNodeCode("['main'] as unknown", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'unknown', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main']", MAIN), }, ], }, ], }, { name: 'double cast through unknown removes the whole chain', code: createNodeCode("['main'] as unknown as NodeConnectionType[]", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'unknown', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main']", MAIN), }, ], }, ], }, { name: 'angle-bracket assertion', code: createNodeCode("['main']", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main']", MAIN), }, ], }, ], }, { name: 'erasing cast hidden behind an outer satisfies', code: createNodeCode("['main'] as never satisfies NodeConnectionType[]", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN), }, ], }, ], }, { name: 'erasing cast wrapped around an inner satisfies', code: createNodeCode("['main'] satisfies NodeConnectionType[] as never", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN), }, ], }, ], }, { name: 'cast nested under satisfies, with a second cast outside it', code: createNodeCode("['main'] as never satisfies NodeConnectionType[] as unknown", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'unknown', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', // Both casts go, the satisfies between them stays. output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN), }, ], }, ], }, { name: 'quoted connection key', code: createNodeCodeWithProperty("'inputs': ['main'] as never"), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCodeWithProperty("'inputs': ['main']"), }, ], }, ], }, { name: 'computed connection key', code: createNodeCodeWithProperty("['inputs']: ['main'] as never"), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCodeWithProperty("['inputs']: ['main']"), }, ], }, ], }, { name: 'cast hiding a malformed declaration', code: createNodeCode("'not-an-array' as never", MAIN), errors: [ { messageId: 'erasingCast', data: { typeName: 'never', property: 'inputs' }, suggestions: [ { messageId: 'removeCast', output: createNodeCode("'not-an-array'", MAIN), }, ], }, ], }, ], });