Skip to content

Commit 321c623

Browse files
committed
add plugin manager test
1 parent ac003a1 commit 321c623

5 files changed

Lines changed: 122 additions & 69 deletions

File tree

.github/actions/file/tests/pluginManager.test.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
// - this exists because I'm not sure how to mock
3+
// the dynamic import function, so mocking this instead
4+
// - also, vitest has a limitation on mocking:
5+
// https://vitest.dev/guide/mocking/modules.html#mocking-modules-pitfalls
6+
// - basically if a function is called by another function in the same file
7+
// it can't be mocked. So this was extracted into a separate file
8+
export async function dynamicImport(path: string) {
9+
return import(path)
10+
}

.github/actions/find/src/findForUrl.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import AxeBuilder from '@axe-core/playwright'
33
import playwright from 'playwright'
44
import {AuthContext} from './AuthContext.js'
55
import {generateScreenshots} from './generateScreenshots.js'
6-
import { loadPlugins } from './pluginManager.js';
6+
import { loadPlugins } from './pluginManager.js'
77

88

99
export async function findForUrl(
@@ -21,18 +21,18 @@ export async function findForUrl(
2121
await page.goto(url)
2222
console.log(`Scanning ${page.url()}`)
2323

24-
let findings: Finding[] = [];
24+
let findings: Finding[] = []
2525
const addFinding = (findingData: Finding) => {
26-
findings.push(findingData);
27-
};
26+
findings.push(findingData)
27+
}
2828

2929
try {
3030
const rawFindings = await new AxeBuilder({page}).analyze()
3131

32-
const plugins = await loadPlugins();
32+
const plugins = await loadPlugins()
3333
for (const plugin of plugins) {
34-
console.log('Running plugin: ', plugin.name);
35-
await plugin.default({ page, addFinding, url });
34+
console.log('Running plugin: ', plugin.name)
35+
await plugin.default({ page, addFinding, url })
3636
}
3737

3838
let screenshotId: string | undefined
Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,83 @@
1-
import * as fs from 'fs';
2-
import * as path from 'path';
3-
import { fileURLToPath } from 'url';
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import { fileURLToPath } from 'url'
4+
import { dynamicImport } from './dynamicImport.js'
45

56
// Helper to get __dirname equivalent in ES Modules
6-
const __filename = fileURLToPath(import.meta.url);
7-
const __dirname = path.dirname(__filename);
7+
const __filename = fileURLToPath(import.meta.url)
8+
const __dirname = path.dirname(__filename)
89

9-
const plugins: any[] = [];
10-
let pluginsLoaded = false;
10+
const plugins: any[] = []
11+
let pluginsLoaded = false
1112

1213

1314
export async function loadPlugins() {
15+
console.log('loading plugins')
16+
1417
try {
1518
if (!pluginsLoaded) {
16-
await loadBuiltInPlugins();
17-
await loadCustomPlugins();
19+
await loadBuiltInPlugins()
20+
await loadCustomPlugins()
1821
}
1922
} catch (e) {
20-
plugins.length = 0;
21-
console.log('There was an error while loading plugins.');
22-
console.log('Clearing all plugins and aborting custom plugin scans.');
23-
console.log('Please check the logs for hints as to what may have gone wrong.');
23+
plugins.length = 0
24+
console.log(abortError)
2425
} finally {
25-
pluginsLoaded = true;
26-
return plugins;
26+
pluginsLoaded = true
27+
return plugins
2728
}
2829
}
2930

30-
async function loadBuiltInPlugins() {
31-
console.log('Loading built-in plugins');
31+
export const abortError = [
32+
'There was an error while loading plugins.',
33+
'Clearing all plugins and aborting custom plugin scans.',
34+
'Please check the logs for hints as to what may have gone wrong.'
35+
].join('\n')
36+
37+
38+
export function clearCache() {
39+
console.log('clearing plugin cache')
40+
pluginsLoaded = false
41+
plugins.length = 0
42+
}
43+
44+
45+
// exported for mocking/testing. not for actual use
46+
export async function loadBuiltInPlugins() {
47+
console.log('Loading built-in plugins')
3248

33-
const pluginsPath = '../../../scanner-plugins/';
49+
const pluginsPath = '../../../scanner-plugins/'
3450
await loadPluginsFromPath({
3551
readPath: path.join(__dirname, pluginsPath),
3652
importPath: pluginsPath,
37-
});
53+
})
3854
}
3955

40-
async function loadCustomPlugins() {
41-
console.log('Loading custom plugins');
56+
// exported for mocking/testing. not for actual use
57+
export async function loadCustomPlugins() {
58+
console.log('Loading custom plugins')
4259

43-
const pluginsPath = process.cwd() + '/.github/scanner-plugins/';
60+
const pluginsPath = process.cwd() + '/.github/scanner-plugins/'
4461
await loadPluginsFromPath({
4562
readPath: pluginsPath,
4663
importPath: pluginsPath
47-
});
64+
})
4865
}
4966

50-
async function loadPluginsFromPath({ readPath, importPath }: { readPath: string, importPath: string }) {
67+
// exported for mocking/testing. not for actual use
68+
export async function loadPluginsFromPath({ readPath, importPath }: { readPath: string, importPath: string }) {
5169
try {
52-
const res = fs.readdirSync(readPath);
70+
const res = fs.readdirSync(readPath)
5371
for (const pluginFolder of res) {
54-
console.log('Found plugin: ', pluginFolder);
72+
console.log('Found plugin: ', pluginFolder)
5573
// @ts-ignore
56-
plugins.push(await import(path.join(importPath, pluginFolder, '/index.js')));
74+
plugins.push(await dynamicImport(path.join(importPath, pluginFolder, '/index.js')))
5775
}
5876
} catch (e) {
5977
// - log errors here for granular info
60-
console.log('error: ');
61-
console.log(e);
78+
console.log('error: ')
79+
console.log(e)
6280
// - throw error to handle aborting the plugin scans
63-
throw(e);
81+
throw(e)
6482
}
6583
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {describe, it, expect, vi, beforeEach} from 'vitest'
2+
3+
import * as fs from 'fs'
4+
import * as dynamicImportModule from '../src/dynamicImport.js'
5+
import * as pluginManager from '../src/pluginManager.js'
6+
7+
8+
// - enable spying on fs
9+
// https://vitest.dev/guide/browser/#limitations
10+
vi.mock('fs', { spy: true })
11+
vi.mock('../src/pluginManager.js', { spy: true })
12+
13+
describe('loadPlugins', () => {
14+
vi.spyOn(dynamicImportModule, 'dynamicImport').mockImplementation((path) => (
15+
Promise.resolve(path)
16+
))
17+
beforeEach(() => {
18+
vi.spyOn(fs, 'readdirSync').mockImplementation((readPath) => {
19+
return [readPath + '/plugin-1', readPath + '/plugin-2']
20+
})
21+
})
22+
23+
describe('when plugins are not loaded', () => {
24+
it('loads them', async () => {
25+
pluginManager.clearCache()
26+
const plugins = await pluginManager.loadPlugins()
27+
expect(dynamicImportModule.dynamicImport).toHaveBeenCalledTimes(4)
28+
expect(plugins.length).toBe(4)
29+
})
30+
})
31+
32+
describe('when plugins are already loaded', () => {
33+
it('caches them and doesnt load them again', async () => {
34+
pluginManager.clearCache()
35+
await pluginManager.loadPlugins()
36+
await pluginManager.loadPlugins()
37+
expect(pluginManager.loadBuiltInPlugins).toHaveBeenCalledTimes(0)
38+
expect(pluginManager.loadCustomPlugins).toHaveBeenCalledTimes(0)
39+
})
40+
})
41+
42+
describe('when there is an error loading plugins', () => {
43+
beforeEach(() => {
44+
vi.spyOn(fs, 'readdirSync').mockImplementation(() => {
45+
throw new Error('test error')
46+
})
47+
})
48+
49+
it('Aborts loading all plugins', async () => {
50+
pluginManager.clearCache()
51+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
52+
const plugins = await pluginManager.loadPlugins()
53+
expect(plugins.length).toBe(0)
54+
expect(consoleLogSpy).toHaveBeenCalledWith(pluginManager.abortError)
55+
})
56+
})
57+
})

0 commit comments

Comments
 (0)