Skip to content

Commit c30ddcc

Browse files
committed
Move check-codescanning-config/index.ts to pr-checks and add tests
1 parent a7bfa21 commit c30ddcc

4 files changed

Lines changed: 173 additions & 55 deletions

File tree

.github/actions/check-codescanning-config/action.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,15 @@ runs:
5454
env:
5555
CODEQL_ACTION_TEST_MODE: 'true'
5656

57-
- name: Install dependencies
58-
shell: bash
59-
run: npm install --location=global ts-node js-yaml
60-
6157
- name: Check config
62-
working-directory: ${{ github.action_path }}
6358
shell: bash
6459
env:
6560
EXPECTED_CONFIG_FILE_CONTENTS: '${{ inputs.expected-config-file-contents }}'
66-
run: ts-node ./index.ts "$RUNNER_TEMP/user-config.yaml" "$EXPECTED_CONFIG_FILE_CONTENTS"
61+
run: |
62+
npx tsx ts-node ../action/pr-checks/check-cs-config.ts \
63+
--file "$RUNNER_TEMP/user-config.yaml" \
64+
--expected-contents "$EXPECTED_CONFIG_FILE_CONTENTS"
65+
6766
- name: Clean up
6867
shell: bash
6968
if: always()

.github/actions/check-codescanning-config/index.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

pr-checks/check-cs-config.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Tests for `check-cs-config.ts`.
3+
*/
4+
5+
import * as assert from "node:assert/strict";
6+
import { describe, it } from "node:test";
7+
8+
import type { UserConfig } from "../src/config/db-config";
9+
10+
import { checkConfiguration } from "./check-cs-config";
11+
12+
describe("checkConfiguration", async () => {
13+
await it("passes when actual and expected configs match", () => {
14+
const actual: UserConfig = { name: "test-config", paths: ["src"] };
15+
const expected = JSON.stringify(actual);
16+
assert.doesNotThrow(() => checkConfiguration(actual, expected));
17+
});
18+
19+
await it("passes when queries arrays match after sorting", () => {
20+
const actual: UserConfig = { paths: ["b", "a", "c"] };
21+
const expected = JSON.stringify(actual);
22+
assert.doesNotThrow(() => checkConfiguration(actual, expected));
23+
});
24+
25+
await it("throws when actual config does not match expected", () => {
26+
const actual: UserConfig = { name: "actual-name" };
27+
const expected = JSON.stringify({
28+
name: "expected-name",
29+
} satisfies UserConfig);
30+
assert.throws(() => checkConfiguration(actual, expected), {
31+
message: /Expected configuration does not match actual configuration/,
32+
});
33+
});
34+
35+
await it("throws when expected contents are empty", () => {
36+
assert.throws(() => checkConfiguration({}, ""), {
37+
message: /No expected configuration provided/,
38+
});
39+
});
40+
41+
await it("throws when expected contents are only whitespace", () => {
42+
assert.throws(() => checkConfiguration({}, " "), {
43+
message: /No expected configuration provided/,
44+
});
45+
});
46+
47+
await it("passes with complex config", () => {
48+
const actual: UserConfig = {
49+
name: "complex",
50+
"disable-default-queries": true,
51+
paths: ["src", "lib"],
52+
"paths-ignore": ["test"],
53+
"threat-models": ["remote"],
54+
};
55+
const expected = JSON.stringify(actual);
56+
assert.doesNotThrow(() => checkConfiguration(actual, expected));
57+
});
58+
59+
await it("trims whitespace from expected contents before parsing", () => {
60+
const actual: UserConfig = { name: "trimmed" };
61+
const expected = ` ${JSON.stringify(actual)} `;
62+
assert.doesNotThrow(() => checkConfiguration(actual, expected));
63+
});
64+
65+
await it("passes when both configs are empty objects", () => {
66+
assert.doesNotThrow(() => checkConfiguration({}, "{}"));
67+
});
68+
});

pr-checks/check-cs-config.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env npx tsx
2+
3+
/**
4+
* Checks the code scanning configuration file generated by the
5+
* action to ensure it contains the expected contents
6+
*/
7+
8+
import * as assert from "node:assert";
9+
import * as fs from "node:fs";
10+
import { parseArgs } from "node:util";
11+
12+
import * as core from "@actions/core";
13+
import * as yaml from "js-yaml";
14+
15+
import type { UserConfig } from "../src/config/db-config";
16+
17+
import { getErrorMessage } from "./util";
18+
19+
function sortConfigArrays(config: UserConfig) {
20+
for (const key of Object.keys(config)) {
21+
const value = config[key];
22+
if (key === "queries" && Array.isArray(value)) {
23+
config[key] = value.sort();
24+
}
25+
}
26+
return config;
27+
}
28+
29+
function loadActualConfig(configPath: string) {
30+
if (!fs.existsSync(configPath)) {
31+
throw new Error("No configuration file found");
32+
} else {
33+
const rawActualConfig = fs.readFileSync(configPath, "utf8");
34+
core.startGroup("Actual generated user config");
35+
core.info(rawActualConfig);
36+
core.endGroup();
37+
38+
return yaml.load(rawActualConfig) as UserConfig;
39+
}
40+
}
41+
42+
export function checkConfiguration(
43+
actualConfig: UserConfig,
44+
expectedContents: string,
45+
) {
46+
const rawExpectedConfig = expectedContents.trim();
47+
if (!rawExpectedConfig) {
48+
throw new Error("No expected configuration provided");
49+
}
50+
51+
const expectedConfig = JSON.parse(rawExpectedConfig) as UserConfig;
52+
53+
core.startGroup("Expected generated user config");
54+
core.info(yaml.dump(expectedConfig));
55+
core.endGroup();
56+
57+
assert.deepStrictEqual(
58+
sortConfigArrays(actualConfig),
59+
sortConfigArrays(expectedConfig),
60+
"Expected configuration does not match actual configuration",
61+
);
62+
}
63+
64+
function main() {
65+
const { values } = parseArgs({
66+
options: {
67+
// The path of the configuration file to check.
68+
file: {
69+
type: "string",
70+
},
71+
// The expected contents of the file.
72+
"expected-contents": {
73+
type: "string",
74+
},
75+
},
76+
strict: true,
77+
});
78+
79+
if (values.file === undefined) {
80+
throw new Error("The '--file' input is required.");
81+
}
82+
if (values["expected-contents"] === undefined) {
83+
throw new Error("The '--expected-contents' input is required.");
84+
}
85+
86+
const actualConfig = loadActualConfig(values.file);
87+
88+
try {
89+
checkConfiguration(actualConfig, values["expected-contents"]);
90+
} catch (err) {
91+
core.error(getErrorMessage(err));
92+
return -1;
93+
}
94+
95+
return 0;
96+
}
97+
98+
if (require.main === module) {
99+
process.exit(main());
100+
}

0 commit comments

Comments
 (0)