diff --git a/.vscode/launch.json b/.vscode/launch.json index 18f442c..85815a2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,22 +6,24 @@ "version": "0.2.0", "configurations": [ { - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "name": "Launch Extension VSCE", - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: compile", + "name": "Launch Extension", + "type": "extensionHost", "request": "launch", - "type": "extensionHost" + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/out/src/**/*.js"], + "preLaunchTask": "npm: watch", + "env": { + "USE_NEW_CONFIG": "true" + }, }, { "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], - "name": "Launch Extension", + "name": "Launch Extension xxxxx", "outFiles": [ "${workspaceFolder}/dist/**/*.js" ], @@ -38,7 +40,10 @@ ], "outFiles": [ "${workspaceFolder}/dist/**/*.js" - ] + ], + "env": { + "USE_NEW_CONFIG": "true" + }, }, { "name": "Extension Tests", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6f3f982..244546d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -42,7 +42,7 @@ "group": "build", "problemMatcher": [], "label": "npm: compile", - "detail": "webpack --config ./build/node-extension.webpack.config.js" + "detail": "npm run build" }, { "type": "npm", diff --git a/package-lock.json b/package-lock.json index 310ea21..2c1a2cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,11 @@ "license": "AGPL-3.0", "dependencies": { "convert-array-to-csv": "^2.0.0", - "cosmiconfig": "^9.0.0", "lightning-flow-scanner-core": "4.48.0", "tabulator-tables": "^6.3.1", "uuid": "^11.0.5", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "yaml": "^2.8.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^28.0.2", @@ -367,6 +367,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -649,6 +650,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -9624,7 +9626,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "5.3.0", @@ -10470,6 +10473,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11429,50 +11433,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -12798,15 +12758,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/envinfo": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", @@ -12836,6 +12787,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -14865,6 +14817,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -15045,6 +14998,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-binary-path": { @@ -16379,12 +16333,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -16420,7 +16376,8 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-ref-resolver": { "version": "1.0.1", @@ -16955,7 +16912,8 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/linkify-it": { "version": "5.0.0", @@ -18332,6 +18290,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -18679,6 +18638,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -20054,6 +20014,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -23055,7 +23016,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -24912,7 +24873,6 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 6892a84..1c61317 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "scripts": { "vscode:prepublish": "npm run package", "compile": "webpack --config ./build/node-extension.webpack.config.js", - "watch": "concurrently \"rollup -c -w\" \"webpack --watch --config ./build/node-extension.webpack.config.js\"", + "watch": "npm run v:update && concurrently \"rollup -c -w\" \"webpack --watch --config ./build/node-extension.webpack.config.js\"", "build-webapp": "rollup -c", "build": "npm run v:update && rollup -c && vsce package", "build:beta": "npm run v:update && rollup -c && vsce package --pre-release", @@ -182,10 +182,10 @@ }, "dependencies": { "convert-array-to-csv": "^2.0.0", - "cosmiconfig": "^9.0.0", "lightning-flow-scanner-core": "4.48.0", "tabulator-tables": "^6.3.1", "uuid": "^11.0.5", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "yaml": "^2.8.0" } } diff --git a/src/commands/handlers.ts b/src/commands/handlers.ts index 946b0e7..bdebb0c 100644 --- a/src/commands/handlers.ts +++ b/src/commands/handlers.ts @@ -8,6 +8,7 @@ import { findFlowCoverage } from '../libs/FindFlowCoverage'; import { CacheProvider } from '../providers/cache-provider'; import { testdata } from '../store/testdata'; import { OutputChannel } from '../providers/outputChannel'; +import { ConfigProvider } from '../providers/config-provider'; const { USE_NEW_CONFIG: isUseNewConfig } = process.env; @@ -32,7 +33,7 @@ export default class Commands { } private async configRules() { - if (isUseNewConfig) { + if (isUseNewConfig === 'true') { await this.ruleConfiguration(); return; } @@ -100,7 +101,14 @@ export default class Commands { ); } - private async ruleConfiguration() {} + private async ruleConfiguration() { + const configProvider = new ConfigProvider(); + const config = await configProvider.discover( + vscode.workspace.workspaceFolders?.[0].uri.path + ); + const document = await vscode.workspace.openTextDocument(config.fspath); + await vscode.window.showTextDocument(document); + } private async debugView() { let results = testdata as unknown as core.ScanResult[]; @@ -160,11 +168,16 @@ export default class Commands { OutputChannel.getInstance().logChannel.trace('reset configurations'); await this.configRules(); } - const ruleConfig = CacheProvider.instance.get('ruleconfig'); + let ruleConfig = CacheProvider.instance.get('ruleconfig'); OutputChannel.getInstance().logChannel.debug( 'load stored rule configurations', ruleConfig ); + if (isUseNewConfig === 'true') { + // load and use config + const configProvider = new ConfigProvider(); + ruleConfig = await configProvider.loadConfig(rootPath.fsPath); + } results = core.scan(await core.parse(selectedUris), ruleConfig); OutputChannel.getInstance().logChannel.debug('Scan Results', ...results); await CacheProvider.instance.set('results', results); diff --git a/src/providers/config-provider.ts b/src/providers/config-provider.ts index d6b2d83..98f2eca 100644 --- a/src/providers/config-provider.ts +++ b/src/providers/config-provider.ts @@ -1,28 +1,110 @@ -import { cosmiconfig, CosmiconfigResult } from 'cosmiconfig'; -import { IRulesConfig } from 'lightning-flow-scanner-core'; +import { + IRulesConfig, + getRules, + getBetaRules, + IRuleDefinition, +} from 'lightning-flow-scanner-core'; +import * as vsce from 'vscode'; +import { Document, parse } from 'yaml'; + +type Configuration = { + fspath: string; + config: unknown; +}; export class ConfigProvider { - public async loadConfig(configPath?: string): Promise { - const moduleName = 'flow-scanner'; - const searchPlaces = [ - 'package.json', - `.${moduleName}.yaml`, - `.${moduleName}.yml`, - `.${moduleName}.json`, - `config/.${moduleName}.yaml`, - `config/.${moduleName}.yml`, - `.flow-scanner`, + public async discover(configPath: string): Promise { + const configurationName = 'flow-scanner'; + + const findInJson = [`.${configPath}.json`, `${configurationName}.json`]; + + const findInYml = [ + `.${configurationName}.yml`, + `.${configurationName}.yaml`, + `${configurationName}.yaml`, + `${configurationName}.yml`, + configurationName, ]; - const explorer = cosmiconfig(moduleName, { - searchPlaces, - }); - let explorerResults: CosmiconfigResult; - if (configPath) { - // Forced config file name - explorerResults = await explorer.load(configPath); + + let configFile = await this.attemptToReadConfig( + configPath, + findInJson, + JSON.parse + ); + + if (!configFile) { + configFile = await this.attemptToReadConfig(configPath, findInYml, parse); + } + + if (!configFile) { + // if at this point there's still nothing. create a new file + configFile = await this.writeConfigFile(configurationName, configPath); } - // Let cosmiconfig look for a config file - explorerResults = explorerResults ?? (await explorer.search()); + + return configFile; + } + + private async writeConfigFile( + configurationName: string, + configPath: string + ): Promise { + const allRules: Record = [ + ...getRules(), + ...getBetaRules(), + ].reduce( + (acc, rule: IRuleDefinition) => { + acc[rule.name] = { severity: 'error' }; + return acc; + }, + {} as Record + ); + + const config = { + rules: allRules, + }; + + const configFile = { + fspath: `${configPath}/.${configurationName}.yml`, + config, + }; + + await vsce.workspace.fs.writeFile( + vsce.Uri.file(configFile.fspath), + new TextEncoder().encode(String(new Document(config))) + ); + + return configFile; + } + + private async attemptToReadConfig( + basePath: string, + potentialFileNames: string[], + parser: Function + ): Promise { + let foundConfig: Configuration; + await Promise.all( + potentialFileNames.map(async (fileName) => { + if (foundConfig) return; + const file = vsce.Uri.file(`${basePath}/${fileName}`); + try { + const doesFileExist = await vsce.workspace.fs.stat(file); + if (doesFileExist) { + foundConfig = { fspath: file.fsPath, config: undefined }; + const fileContent = Buffer.from( + await vsce.workspace.fs.readFile(file) + ).toString(); + foundConfig.config = parser(fileContent); + } + } catch (e) { + // File does not exist, ignore + } + }) + ); + return foundConfig; + } + + public async loadConfig(configPath?: string): Promise { + const explorerResults = await this.discover(configPath); return explorerResults?.config ?? {}; } } diff --git a/tests/commands/handlers.spec.ts b/tests/commands/handlers.spec.ts index 0fe44b9..4b3b798 100644 --- a/tests/commands/handlers.spec.ts +++ b/tests/commands/handlers.spec.ts @@ -1,14 +1,17 @@ import { describe, it, expect, jest } from '@jest/globals'; import * as cmd from '../../src/commands/handlers'; -import { window, ExtensionContext } from 'vscode'; +import * as vsce from 'vscode'; import * as core from 'lightning-flow-scanner-core'; import { CacheProvider } from '../../src/providers/cache-provider'; import { OutputChannel } from '../../src/providers/outputChannel'; +import * as cp from '../../src/providers/config-provider'; jest.mock('lightning-flow-scanner-core'); jest.mock('../../src/providers/cache-provider'); jest.mock('../../src/providers/outputChannel'); +jest.mock('../../src/providers/config-provider'); +jest.mock('vscode'); describe('Commands', () => { it('should be defined', () => { @@ -43,7 +46,7 @@ describe('Commands', () => { ]), } as any; - const spy = jest.spyOn(window, 'showQuickPick').mockReturnValue([ + const spy = jest.spyOn(vsce.window, 'showQuickPick').mockReturnValue([ { label: 'Flow Name', value: 'FlowName' }, { label: 'API Version', value: 'APIVersion' }, ] as any); @@ -57,7 +60,7 @@ describe('Commands', () => { const extensionContext = jest.fn(); const command = new cmd.default( - extensionContext as unknown as ExtensionContext + extensionContext as unknown as vsce.ExtensionContext ); await expect(async () => await command['configRules']()).not.toThrow(); @@ -65,11 +68,31 @@ describe('Commands', () => { it('should read from configuration', async () => { const command = new cmd.default({} as any); + jest + .spyOn(cp.ConfigProvider.prototype, 'discover') + .mockImplementation(() => + Promise.resolve({ + fspath: '/fake/path', + config: { rules: { APIName: { severity: 'error' } } }, + } as any) + ); + (vsce.workspace as any).openTextDocument = jest.fn(); + (vsce.window as any).showTextDocument = jest.fn(); await expect(command['ruleConfiguration']()).resolves.not.toThrow(); }); it('should write to configuration', async () => { const command = new cmd.default({} as any); + jest + .spyOn(cp.ConfigProvider.prototype, 'discover') + .mockImplementation(() => + Promise.resolve({ + fspath: '/fake/path', + config: { rules: { APIName: { severity: 'error' } } }, + } as any) + ); + (vsce.workspace as any).openTextDocument = jest.fn(); + (vsce.window as any).showTextDocument = jest.fn(); await expect(command['ruleConfiguration']()).resolves.not.toThrow(); }); }); diff --git a/tests/providers/config-provider.spec.ts b/tests/providers/config-provider.spec.ts index e7361a0..5ef35a8 100644 --- a/tests/providers/config-provider.spec.ts +++ b/tests/providers/config-provider.spec.ts @@ -1,86 +1,116 @@ -import { describe, it, expect, jest } from '@jest/globals'; +import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { ConfigProvider } from '../../src/providers/config-provider'; -import * as cosmi from 'cosmiconfig'; -jest.mock('cosmiconfig'); +import * as yml from 'yaml'; +import * as vsce from 'vscode'; + +jest.mock('yaml'); +jest.mock('vscode'); describe('Config-Provider', () => { + let provider: ConfigProvider; + const mockFs: any = {}; + const mockWorkspace: any = {}; + const mockUri = (path: string) => ({ fsPath: path, path }); + + beforeEach(() => { + provider = new ConfigProvider(); + jest.clearAllMocks(); + // Mock VSCE + mockFs.stat = jest.fn(); + mockFs.readFile = jest.fn(); + mockFs.writeFile = jest.fn(); + (vsce.workspace.fs as any).stat = mockFs.stat; + (vsce.workspace.fs as any).readFile = mockFs.readFile; + (vsce.workspace.fs as any).writeFile = mockFs.writeFile; + if (vsce.Uri) { + (vsce.Uri as any).file = mockUri; + } + // Mock YAML + jest.spyOn(yml, 'parse').mockImplementation(() => jest.fn()); + jest.spyOn(yml, 'Document').mockImplementation((c: any) => c); + // TextEncoder + (global as any).TextEncoder = class { + encode(str: string) { + return Buffer.from(str); + } + }; + }); + it('should be defined', () => { expect(ConfigProvider).toBeDefined(); }); - it('should not error when no config', async () => { - const cosmiMock = { - load: jest.fn(), - search: jest.fn(), - }; - const cosmiSpy = jest.spyOn(cosmi, 'cosmiconfig'); - cosmiSpy.mockReturnValue(cosmiMock as any); + it('discover: finds JSON config', async () => { + mockFs.stat.mockResolvedValueOnce(true); + mockFs.readFile.mockResolvedValueOnce(Buffer.from('{"foo":1}')); + (yml as any).parse.mockReturnValue({ foo: 1 }); + const config = await provider.discover('/path'); + expect(config).toBeDefined(); + expect(config.config).toEqual({ foo: 1 }); + }); - const configProvider = new ConfigProvider(); - await expect(configProvider.loadConfig()).resolves.toStrictEqual({}); + it('discover: finds YAML config', async () => { + mockFs.stat.mockRejectedValueOnce(new Error('not found')); + mockFs.stat.mockRejectedValueOnce(new Error('not found')); + mockFs.stat.mockResolvedValueOnce(true); + mockFs.readFile.mockResolvedValueOnce(Buffer.from('bar: 2')); + (yml as any).parse.mockReturnValue({ bar: 2 }); + const config = await provider.discover('/path'); + expect(config).toBeDefined(); + expect(config.config).toEqual({ bar: 2 }); }); - it('should load config when directly passed from settings', async () => { - const cosmiMock = { - load: jest.fn().mockImplementation(() => ({ - config: { - rules: { - FlowName: { - severity: 'error', - }, - }, - exceptions: {}, - }, - filepath: '', - })), - search: jest.fn(), - }; - const cosmiSpy = jest.spyOn(cosmi, 'cosmiconfig'); - cosmiSpy.mockReturnValue(cosmiMock as any); + it('discover: creates new config if not found', async () => { + mockFs.stat.mockRejectedValue(new Error('not found')); + mockFs.writeFile.mockResolvedValueOnce(undefined); + const config = await provider.discover('/path'); + expect(config).toBeDefined(); + expect(config.fspath).toContain('/path/.flow-scanner.yml'); + expect(mockFs.writeFile).toHaveBeenCalled(); + }); - const configProvider = new ConfigProvider(); - await expect( - configProvider.loadConfig('some config') - ).resolves.toStrictEqual({ - rules: { - FlowName: { - severity: 'error', - }, - }, - exceptions: {}, - }); - expect(cosmiMock.search).not.toHaveBeenCalled(); + it('writeConfigFile: writes a new config file', async () => { + mockFs.writeFile.mockResolvedValueOnce(undefined); + const result = await (provider as any).writeConfigFile( + 'flow-scanner', + '/base' + ); + expect(result.fspath).toContain('/base/.flow-scanner.yml'); + expect(mockFs.writeFile).toHaveBeenCalled(); }); - it('should resolve a config via workspace directory', async () => { - const cosmiMock = { - load: jest.fn(), - search: jest.fn().mockImplementation(() => ({ - config: { - rules: { - APIVersion: { - severity: 'error', - }, - }, - exceptions: {}, - }, - filepath: '', - })), - }; - const cosmiSpy = jest.spyOn(cosmi, 'cosmiconfig'); - cosmiSpy.mockReturnValue(cosmiMock as any); + it('attemptToReadConfig: returns config if file exists', async () => { + mockFs.stat.mockResolvedValueOnce(true); + mockFs.readFile.mockResolvedValueOnce(Buffer.from('{"a":3}')); + const parser = jest.fn().mockReturnValue({ a: 3 }); + const result = await (provider as any).attemptToReadConfig( + '/foo', + ['bar.json'], + parser + ); + expect(result).toBeDefined(); + expect(result.config).toEqual({ a: 3 }); + }); + + it('attemptToReadConfig: returns null if file does not exist', async () => { + mockFs.stat.mockRejectedValue(new Error('not found')); + const parser = jest.fn(); + const result = await (provider as any).attemptToReadConfig( + '/foo', + ['bar.json'], + parser + ); + expect(result).toBeUndefined(); + }); - const configProvider = new ConfigProvider(); - await expect(configProvider.loadConfig()).resolves.toStrictEqual({ - rules: { - APIVersion: { - severity: 'error', - }, - }, - exceptions: {}, - }); - expect(cosmiMock.load).not.toHaveBeenCalled(); + it('loadConfig: returns config from discover', async () => { + const fakeConfig = { rules: { test: { severity: 'error' } } }; + jest + .spyOn(provider, 'discover') + .mockResolvedValueOnce({ fspath: '/f', config: fakeConfig }); + const result = await provider.loadConfig('/some'); + expect(result).toEqual(fakeConfig); }); });