diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml new file mode 100644 index 0000000..5d2106f --- /dev/null +++ b/.github/workflows/npm-test.yml @@ -0,0 +1,17 @@ +name: 'npm test' +on: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.1.3 + - name: Use Node.js 16 + uses: actions/setup-node@v4.0.2 + with: + node-version: 16.x + - run: npm ci + - run: npm test + \ No newline at end of file diff --git a/README.md b/README.md index 46322cc..4a4616d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The `stackql/stackql-assert` action is an composite action that runs a `stackql` # Usage -> This action uses the [setup-stackql](https://github.com/marketplace/actions/stackql-studios-setup-stackql) and [stackql-exec](https://github.com/marketplace/actions/stackql-studios-stackql-exec) actions +> This action uses the [setup-stackql](https://github.com/marketplace/actions/setup-stackql) and [stackql-exec](https://github.com/marketplace/actions/stackql-exec) actions ## Provider Authentication Authentication to StackQL providers is done via environment variables source from GitHub Actions Secrets. To learn more about authentication, see the setup instructions for your provider or providers at the [StackQL Provider Registry Docs](https://stackql.io/registry). diff --git a/action.yml b/action.yml index 4d31e12..5f12609 100644 --- a/action.yml +++ b/action.yml @@ -1,4 +1,4 @@ -name: 'StackQL Studios - StackQL Assert' +name: 'stackql-assert' description: 'Run StackQL query to test and audit your infrastructure.' author: 'Yuncheng Yang, StackQL Studios' inputs: @@ -66,4 +66,4 @@ runs: branding: icon: 'terminal' - color: 'green' + color: 'blue' diff --git a/lib/assert.js b/lib/assert.js index f82946a..7c7332e 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -46,6 +46,9 @@ function assertResult(core) { if (!RESULT) throw new Error("Result from StackQL execution is missing."); + // if no EXPECTED_RESULTS_STR, EXPECTED_RESULTS_FILE_PATH or EXPECTED_ROWS, fail the action + if (!EXPECTED_RESULTS_STR && !EXPECTED_RESULTS_FILE_PATH && !EXPECTED_ROWS) throw new Error("āŒ Cannot find expected result, file path or expected rows"); + const actualResult = parseResult(RESULT); core.info("šŸ” Checking results..."); @@ -64,4 +67,9 @@ function assertResult(core) { } } -module.exports = { assertResult }; +module.exports = { + parseResult, + getExpectedResult, + checkResult, + assertResult +}; diff --git a/lib/tests/assert.test.js b/lib/tests/assert.test.js index b9841c9..0bd4ffb 100644 --- a/lib/tests/assert.test.js +++ b/lib/tests/assert.test.js @@ -2,85 +2,14 @@ const {checkResult, parseResult, assertResult, getExpectedResult} = require('../ describe('parseResult', ()=>{ - it('should parsedResult correctly when it starts with register', ()=>{ - const resultString = `google provider, version 'v23.01.00116' successfully installed \n - [{"name":"stackql-demo-001","status":"TERMINATED"}]` - - const expected = [{"name":"stackql-demo-001","status":"TERMINATED"}] - - const actual = parseResult(resultString); - expect(actual).toEqual(expected); - }) - - it('should parsedResult correctly when it is only the object', ()=>{ + it('should parsedResult correctly', ()=>{ const resultString = `[{"name":"stackql-demo-001","status":"TERMINATED"}]` - const expected = [{"name":"stackql-demo-001","status":"TERMINATED"}] - const actual = parseResult(resultString); expect(actual).toEqual(expected); }) }) -describe('checkResult', ()=>{ - let expectedResult; - let actualResult; - beforeEach(()=>{ - expectedResult= [{"name":"stackql-demo-001","status":"TERMINATED"}] - actualResult= [{"name":"stackql-demo-001","status":"TERMINATED"}] - - }) - it('should return equality false when the result object does not match', ()=>{ - actualResult = [{"name":"stackql-demo-001","status":"RUNNING"}] - - const {equality} = checkResult(expectedResult, undefined, actualResult); - - expect(equality).toEqual(false); - }) - - it('should return equality true when the result object does match', ()=>{ - - const {equality} = checkResult(expectedResult, undefined, actualResult); - - expect(equality).toEqual(true); - }) - - it('should return equality false when expected row is not matching', ()=>{ - - const expectedRows = 2; - - const {equality} = checkResult(undefined, expectedRows, actualResult); - - expect(equality).toEqual(false); - }) - - it('should return equality true when expected row is matching', ()=>{ - const expectedRows = 1; - - const {equality} = checkResult(undefined, expectedRows, actualResult); - - expect(equality).toEqual(true); - }) - - it('should return equality false when expected row is matching, but result object does not match', ()=>{ - const expectedRows = 1; - actualResult = [{"name":"stackql-demo-001","status":"RUNNING"}] - - const {equality} = checkResult(expectedResult, expectedRows, actualResult); - - expect(equality).toEqual(false); - }) - - it('should return equality true when expected row is matching, but result object matches', ()=>{ - const expectedRows = 1; - actualResult = [{"name":"stackql-demo-001","status":"TERMINATED"}] - - const {equality} = checkResult(expectedResult, expectedRows, actualResult); - - expect(equality).toEqual(true); - }) -}) - describe('getExpectedResult', ()=>{ it('should return expectedResult when expectedResultStr is passed', ()=>{ const expectedResultStr = `[{"name":"stackql-demo-001","status":"TERMINATED"}]` @@ -102,106 +31,160 @@ describe('getExpectedResult', ()=>{ }) - describe('assertResult integration test', ()=>{ - let coreObj; - - const ACTION_ENV = { - RESULT: `google provider, version 'v23.01.00116' successfully installed \n - [{"name":"stackql-demo-001","status":"TERMINATED"}]`, - EXPECTED_RESULTS_STR: `[{"name":"stackql-demo-001","status":"TERMINATED"}]`, - EXPECTED_RESULTS_FILE_PATH: 'test.json', - EXPECTED_ROWS: 1 +describe('checkResult', () => { + const core = { + info: jest.fn(), + error: jest.fn(), + setFailed: jest.fn() + }; + + beforeEach(() => { + core.info.mockClear(); + core.error.mockClear(); + core.setFailed.mockClear(); + }); + + it('should return false and log an error when the actual length does not match expected rows', () => { + const expectedRows = "3"; + const actualResult = [{}, {}, {}, {}]; // 4 items, should not match expectedRows + + const result = checkResult(core, undefined, actualResult, expectedRows); + + expect(result).toBe(false); + expect(core.error).toHaveBeenCalledWith(`Expected rows: ${expectedRows}, got: ${actualResult.length}`); + }); + + it('should return true when the actual length matches expected rows', () => { + const expectedRows = "2"; + const actualResult = [{}, {}]; // 2 items, matches expectedRows + + const result = checkResult(core, undefined, actualResult, expectedRows); + + expect(result).toBe(true); + expect(core.error).not.toHaveBeenCalled(); + }); + + it('should return false and log an error when expected does not match actual and expectedRows is undefined', () => { + const expected = [{ name: "test1" }, { name: "test2" }]; + const actual = [{ name: "test1" }, { name: "test3" }]; + + const result = checkResult(core, expected, actual, undefined); + + expect(result).toBe(false); + expect(core.error).toHaveBeenCalledWith(`Expected results do not match actual results.\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`); + }); + + it('should return true when expected matches actual and expectedRows is undefined', () => { + const expected = [{ name: "test1" }, { name: "test2" }]; + const actual = [{ name: "test1" }, { name: "test2" }]; + + const result = checkResult(core, expected, actual, undefined); + + expect(result).toBe(true); + expect(core.error).not.toHaveBeenCalled(); + }); +}); + +describe('assertResult', ()=>{ + let coreObj; + + const ACTION_ENV = { + RESULT: `[{"name":"stackql-demo-001","status":"TERMINATED"}]`, + EXPECTED_RESULTS_STR: `[{"name":"stackql-demo-001","status":"TERMINATED"}]`, + EXPECTED_RESULTS_FILE_PATH: 'test.json', + EXPECTED_ROWS: 1 + } + + beforeEach(() => { + jest.resetModules() + process.env = {...ACTION_ENV} + coreObj = { + setFailed: jest.fn(), + info: jest.fn(), + error: jest.fn(), } - - beforeEach(() => { - jest.resetModules() - process.env = {...ACTION_ENV} - coreObj = { - setFailed: jest.fn(), - info: jest.fn().mockImplementation((message)=>{console.log(message)}) - } - }) - afterEach(() => { - process.env = ACTION_ENV - }) + }) + afterEach(() => { + process.env = ACTION_ENV + }) - it('it should setFailed when there is expected results are undefined', () => { - process.env.EXPECTED_RESULTS_FILE_PATH = undefined - process.env.EXPECTED_RESULTS_STR = undefined - process.env.EXPECTED_ROWS = undefined + it('it should setFailed when there is expected results are undefined', () => { + process.env.EXPECTED_RESULTS_FILE_PATH = undefined + process.env.EXPECTED_RESULTS_STR = undefined + process.env.EXPECTED_ROWS = undefined - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).toHaveBeenCalledWith('āŒ Cannot find expected result, file path or expected rows') - }); + expect(coreObj.setFailed).toHaveBeenCalledWith('Assertion error: āŒ Cannot find expected result, file path or expected rows') + }); - it('it should setFailed when actual result is not equal to expected result', () => { - process.env.RESULT= "[{\"name\":\"stackql-demo-001\",\"status\":\"RUNNING\"}]" + it('it should setFailed when actual result is not equal to expected result', () => { + process.env.RESULT= "[{\"name\":\"stackql-demo-001\",\"status\":\"RUNNING\"}]" - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Failed')) - }); + expect(coreObj.setFailed).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Failed')) + }); - it('it should not setFailed when actual result equal to expected result', () => { - assertResult(coreObj) + it('it should not setFailed when actual result equal to expected result', () => { + assertResult(coreObj) - expect(coreObj.setFailed).not.toHaveBeenCalled() - expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) - }); + expect(coreObj.setFailed).not.toHaveBeenCalled() + expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) + }); - it('it should setFailed when actual result is not equal to expected result', () => { - process.env.EXPECTED_RESULTS_STR= undefined; - process.env.EXPECTED_RESULTS_FILE_PATH = 'lib/tests/failed-result.json' + it('it should setFailed when actual result is not equal to expected result', () => { + process.env.EXPECTED_RESULTS_STR= undefined; + process.env.EXPECTED_RESULTS_FILE_PATH = 'lib/tests/failed-result.json' - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Failed')) - }); + expect(coreObj.setFailed).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Failed')) + }); - it('it should not setFailed when actual result equal to expected result in file', () => { - process.env.EXPECTED_RESULTS_STR= undefined; - process.env.EXPECTED_RESULTS_FILE_PATH = 'lib/tests/success-result.json' + it('it should not setFailed when actual result equal to expected result in file', () => { + process.env.EXPECTED_RESULTS_STR= undefined; + process.env.EXPECTED_RESULTS_FILE_PATH = 'lib/tests/success-result.json' - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).not.toHaveBeenCalled() - expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) - }) + expect(coreObj.setFailed).not.toHaveBeenCalled() + expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) + }) - it('it should setFailed when actual result does not match expected rows', () => { - process.env.EXPECTED_RESULTS_STR= undefined; - process.env.EXPECTED_RESULTS_FILE_PATH = undefined; - process.env.EXPECTED_ROWS = 2 + it('it should setFailed when actual result does not match expected rows', () => { + process.env.EXPECTED_RESULTS_STR= undefined; + process.env.EXPECTED_RESULTS_FILE_PATH = undefined; + process.env.EXPECTED_ROWS = 2 - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Failed')) - }); + expect(coreObj.setFailed).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Failed')) + }); - it('it should not setFailed when actual result match expected rows', ()=>{ - process.env.EXPECTED_RESULTS_STR= undefined; - process.env.EXPECTED_RESULTS_FILE_PATH = undefined; - process.env.EXPECTED_ROWS = 1 + it('it should not setFailed when actual result match expected rows', ()=>{ + process.env.EXPECTED_RESULTS_STR= undefined; + process.env.EXPECTED_RESULTS_FILE_PATH = undefined; + process.env.EXPECTED_ROWS = 1 - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).not.toHaveBeenCalled() - expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) - }) + expect(coreObj.setFailed).not.toHaveBeenCalled() + expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) + }) - it('it should not setFailed when actual result match expected rows in string number', ()=>{ - process.env.EXPECTED_RESULTS_STR= undefined; - process.env.EXPECTED_RESULTS_FILE_PATH = undefined; - process.env.EXPECTED_ROWS = '1' + it('it should not setFailed when actual result match expected rows in string number', ()=>{ + process.env.EXPECTED_RESULTS_STR= undefined; + process.env.EXPECTED_RESULTS_FILE_PATH = undefined; + process.env.EXPECTED_ROWS = '1' - assertResult(coreObj) + assertResult(coreObj) - expect(coreObj.setFailed).not.toHaveBeenCalled() - expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) - }) + expect(coreObj.setFailed).not.toHaveBeenCalled() + expect(coreObj.info).toHaveBeenCalledWith(expect.stringContaining('StackQL Assert Successful')) + }) - + - }) +}) diff --git a/lib/tests/test-auth.json b/lib/tests/test-auth.json deleted file mode 100644 index d8de066..0000000 --- a/lib/tests/test-auth.json +++ /dev/null @@ -1 +0,0 @@ -{ "google": { "type": "service_account", "credentialsfilepath": "sa-key.json" }} \ No newline at end of file diff --git a/lib/tests/utils.test.js b/lib/tests/utils.test.js deleted file mode 100644 index ab73fb7..0000000 --- a/lib/tests/utils.test.js +++ /dev/null @@ -1,79 +0,0 @@ -const { assertResult, parseResult, getExpectedResult } = require('./assert'); -const fs = require('fs'); - -jest.mock('fs'); - -describe('assert.js functions', () => { - let core; - - beforeEach(() => { - core = { - info: jest.fn(), - error: jest.fn(), - setFailed: jest.fn() - }; - process.env.RESULT = JSON.stringify([{ id: 1, value: 'test' }]); - process.env.EXPECTED_ROWS = '1'; - }); - - afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - - describe('parseResult', () => { - it('should correctly parse valid JSON', () => { - const input = JSON.stringify({ key: 'value' }); - expect(parseResult(input, 'valid JSON')).toEqual({ key: 'value' }); - }); - - it('should throw an error on invalid JSON', () => { - const input = "invalid JSON"; - expect(() => parseResult(input, 'invalid JSON')).toThrow('Failed to parse invalid JSON JSON'); - }); - }); - - describe('getExpectedResult', () => { - it('should return parsed result from string', () => { - const input = JSON.stringify({ key: 'value' }); - expect(getExpectedResult(input, null)).toEqual({ key: 'value' }); - }); - - it('should return parsed result from file', () => { - const input = JSON.stringify({ key: 'value' }); - fs.readFileSync.mockReturnValue(input); - expect(getExpectedResult(null, 'path/to/file')).toEqual({ key: 'value' }); - expect(fs.readFileSync).toHaveBeenCalledWith('path/to/file', 'utf-8'); - }); - - it('should throw an error if no input is provided', () => { - expect(() => getExpectedResult(null, null)).toThrow('No expected result provided.'); - }); - }); - - describe('assertResult', () => { - it('should log success if the expected rows and results match', () => { - process.env.EXPECTED_RESULTS_STR = process.env.RESULT; - assertResult(core); - expect(core.info).toHaveBeenCalledWith("āœ… StackQL Assert Successful"); - }); - - it('should fail if expected rows do not match', () => { - process.env.EXPECTED_ROWS = '2'; // Actual result will have only one item - assertResult(core); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining("Expected rows: 2, got: 1")); - }); - - it('should fail if expected results do not match', () => { - process.env.EXPECTED_RESULTS_STR = JSON.stringify([{ id: 1, value: 'wrong' }]); - assertResult(core); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining("Expected results do not match actual results.")); - }); - - it('should handle errors during processing', () => { - process.env.RESULT = 'invalid json'; - assertResult(core); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining("Assertion error")); - }); - }); -}); diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index 0a6f4b0..0000000 --- a/lib/utils.js +++ /dev/null @@ -1,124 +0,0 @@ -const fs = require("fs"); - -function setupAuth(core) { - let auth; - const fileName = process.env.AUTH_FILE_PATH; - const authStr = process.env.AUTH_STR; - - if (!checkEnvVarValid(fileName) && !checkEnvVarValid(authStr)) { - // core.info("Neither AUTH_FILE_PATH nor AUTH_STR is set. Proceeding using default provider environment variable names."); - return; - } - - if (checkEnvVarValid(fileName)) { - try { - // Read the contents of the JSON file into a string - auth = fs.readFileSync(fileName, "utf-8"); - } catch (error) { - core.error(error); - core.setFailed(`Cannot find auth file ${fileName}`); - return; - } - } - if (checkEnvVarValid(authStr)) { - auth = authStr - } - - core.info("Setting AUTH environment variable..."); - core.exportVariable("AUTH", auth); -} - -async function showStackQLQuery(core) { - try { - let [ - dryRunCommand, - dryRunResult, - ] = [ - process.env.STACKQL_DRYRUN_COMMAND, - process.env.DRYRUN_RESULT, - ]; - - if (!dryRunResult) { - core.setFailed("No Dryrun Output from stackql command"); - } - - core.info(`\nšŸš€ rendered stackql query:\n${dryRunResult}`); - - } catch (e) { - core.setFailed(e); - } -} - -async function getStackqlCommand(core) { - - const [query, queryFilePath, dataFilePath, vars, auth, output = "json"] = [ - process.env.QUERY, - process.env.QUERY_FILE_PATH, - process.env.DATA_FILE_PATH, - process.env.VARS, - process.env.AUTH, - process.env.OUTPUT, - ]; - - if (!checkEnvVarValid(query) && !checkEnvVarValid(queryFilePath)) { - core.setFailed("Either test_query or test_query_file_path need to be set"); - return; - } - - let args = ["exec"]; - let dryRunArgs = ["exec", "-H"]; - - if (query) { - args.push(`"${query}"`); - dryRunArgs.push(`"${query}"`); - } else { - args.push("-i", queryFilePath); - dryRunArgs.push("-i", queryFilePath); - } - - if (checkEnvVarValid(dataFilePath)) { - args.push(`--iqldata='${dataFilePath}'`); - dryRunArgs.push(`--iqldata='${dataFilePath}'`); - } - - if (checkEnvVarValid(vars)) { - args.push(`--var='${vars}'`); - dryRunArgs.push(`--var='${vars}'`); - } - - if (checkEnvVarValid(auth)) { - args.push(`--auth='${auth}'`); - dryRunArgs.push(`--auth='${auth}'`); - } - - args.push(`--output='${output}'`); - dryRunArgs.push(`--output='text'`); - dryRunArgs.push(`--dryrun`); - - try { - const stackqlQuery = `stackql ${args.join(" ")}`; - const stackqlDryRunQuery = `stackql ${dryRunArgs.join(" ")}`; - core.exportVariable('STACKQL_COMMAND', stackqlQuery); - core.exportVariable('STACKQL_DRYRUN_COMMAND', stackqlDryRunQuery); - } catch (error) { - core.error(error); - core.setFailed("Error exporting stackql command"); - } -} - -/** - * Checking if environment variable is not empty or undefined - * @param {*} variable - */ -const checkEnvVarValid = (variable) => { - if (!variable || variable === "" || variable === "undefined") { - return false; - } - return true; -}; - -module.exports = { - setupAuth, - getStackqlCommand, - showStackQLQuery, -}; diff --git a/stackql-assert.js b/stackql-assert.js deleted file mode 100644 index 9793059..0000000 --- a/stackql-assert.js +++ /dev/null @@ -1,4 +0,0 @@ -const { assertResult } = require('./lib/assert') -module.exports ={ - assertResult -} \ No newline at end of file