diff --git a/.github/workflows/ci-feat-sonar.yaml b/.github/workflows/ci-feat-sonar.yaml index d28dde2da..9f54e95a7 100644 --- a/.github/workflows/ci-feat-sonar.yaml +++ b/.github/workflows/ci-feat-sonar.yaml @@ -2,7 +2,7 @@ name: Sonar Scanner on: push: - branches: [dev, feat/*] + branches: [dev, feature/*] env: REGISTRY: ghcr.io @@ -25,8 +25,13 @@ jobs: run: | cd src npm i + + docker compose up kong-db -d + npm test + docker compose down + - name: SonarCloud Scan uses: sonarsource/sonarcloud-github-action@master with: diff --git a/src/jest.config.js b/src/jest.config.js index 9a77d4a66..76cb7f5d5 100644 --- a/src/jest.config.js +++ b/src/jest.config.js @@ -1,7 +1,7 @@ module.exports = { verbose: true, testEnvironment: 'node', - testMatch: ['**/?(*.)+(test.{ts,js,jsx})'], + testMatch: ['**/?(*.)+(test.{js,jsx})'], collectCoverageFrom: ['services/**/*.js', 'services/**/*.ts'], coveragePathIgnorePatterns: ['.*/__mocks__/.*', '.*/@types/.*'], coverageDirectory: '__coverage__', diff --git a/src/lists/Product.js b/src/lists/Product.js index 415e1d165..6719819c5 100644 --- a/src/lists/Product.js +++ b/src/lists/Product.js @@ -10,7 +10,7 @@ const { DeleteProductValidate, DeleteProductEnvironments, } = require('../services/workflow/delete-product'); -const { strict: assert } = require('assert'); +const { strict: assert, AssertionError } = require('assert'); const { StructuredActivityService } = require('../services/workflow'); const { regExprValidation } = require('../services/utils'); @@ -47,7 +47,11 @@ module.exports = { access: EnforcementPoint, hooks: { resolveInput: ({ context, operation, resolvedData }) => { - logger.debug('[List.Product] Auth %j', context['authedItem']); + logger.debug( + '[List.Product] Auth %s %j', + operation, + context['authedItem'] + ); if (operation == 'create') { if ('appId' in resolvedData && isProductID(resolvedData['appId'])) { } else { @@ -60,12 +64,20 @@ module.exports = { logger.debug('[List.Product] Resolved %j', resolvedData); return resolvedData; }, - validateInput: ({ resolvedData }) => { - regExprValidation( - '^[a-zA-Z0-9 ()&-]{3,100}$', - resolvedData['name'], - "Product name must be between 3 and 100 alpha-numeric characters (including special characters ' {}&-')" - ); + validateInput: ({ resolvedData, addValidationError }) => { + try { + regExprValidation( + '^[a-zA-Z0-9 ()&-]{3,100}$', + resolvedData['name'], + "Product name must be between 3 and 100 alpha-numeric characters (including special characters ' ()&-')" + ); + } catch (ex) { + if (ex instanceof AssertionError) { + addValidationError(ex.message); + } else { + throw ex; + } + } }, validateDelete: async function ({ existingItem, context }) { await DeleteProductValidate( diff --git a/src/package.json b/src/package.json index b2d695cde..8cfcabd3e 100644 --- a/src/package.json +++ b/src/package.json @@ -34,6 +34,7 @@ "x-prestart": "npm run build", "x-dev": "nodemon", "batch": "cross-env NODE_ENV=development node dist/server-batch.js", + "intg-build": "cross-env NODE_ENV=development NODE_OPTIONS='--openssl-legacy-provider --no-experimental-fetch --dns-result-order=ipv4first' npm-run-all delete-assets copy-assets ts-build", "dev": "cross-env NODE_ENV=development NODE_OPTIONS='--openssl-legacy-provider --no-experimental-fetch --dns-result-order=ipv4first' npm-run-all delete-assets copy-assets tsoa-gen-types tsoa-build-v1 tsoa-build-v2 ts-build ks-dev", "ks-dev": "cross-env NODE_ENV=development DISABLE_LOGGING=true keystone dev --entry=dist/server.js", "dev2": "cross-env NODE_ENV=development DISABLE_LOGGING=true keystone --entry=dist/index.js", @@ -44,7 +45,7 @@ "create-tables": "cross-env CREATE_TABLES=true keystone create-tables --entry=dist/server.js", "lint": "eslint ./nextapp --ext .ts,.tsx --quiet", "lint:ts": "tsc -p ./nextapp/tsconfig.json --noEmit", - "test": "cross-env NODE_ENV=test jest --config ./jest.config.js --coverage --detectOpenHandles", + "test": "cross-env NODE_ENV=test NODE_OPTIONS='--openssl-legacy-provider --no-experimental-fetch --dns-result-order=ipv4first' jest --config ./jest.config.js --coverage --detectOpenHandles", "test:next": "jest --config ./nextapp/jest.config.js --coverage", "test:next-watch": "jest --watch --config ./nextapp/jest.config.js", "test:watch": "jest -w --coverage", diff --git a/src/test/integrated/batchworker/testsuite/run.ts b/src/test/integrated/batchworker/testsuite/run.ts deleted file mode 100644 index 378dac54a..000000000 --- a/src/test/integrated/batchworker/testsuite/run.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* -Wire up directly with Keycloak and use the Services -To run: -npm run intg-build -npm run ts-watch -node dist/test/integrated/batchworker/testsuite/run.js -*/ - -import InitKeystone from '../../keystonejs/init'; -import { - removeKeys, - syncRecords, - getRecords, -} from '../../../../batch/feed-worker'; -import yaml from 'js-yaml'; -import { strict as assert } from 'assert'; - -import testdata from './testdata'; -import mongoose from 'mongoose'; -import { Logger } from '../../../../logger'; -import { BatchWhereClause } from '@/services/keystone/batch-service'; - -const logger = Logger('testsuite'); - -function equalPayload(a: any, e: any) { - assert.strictEqual( - yaml.dump(a, { indent: 2, lineWidth: 100 }), - yaml.dump(e, { indent: 2, lineWidth: 100 }) - ); -} - -function testHeading(index: number, name: string) { - console.log('\x1b[33m --------------------------------------------- \x1b[0m'); - console.log('\x1b[33m ' + index + ' ' + name + ' \x1b[0m'); - console.log('\x1b[33m --------------------------------------------- \x1b[0m'); -} - -async function cleanupDatabase() { - const db = new mongoose.Mongoose(); - const _mongoose = await db.connect(process.env.MONGO_URL, { - user: process.env.MONGO_USER, - pass: process.env.MONGO_PASSWORD, - }); - - for (const collection of [ - 'products', - 'environments', - 'datasets', - 'legals', - 'credentialissuers', - ]) { - await _mongoose.connection.collection(collection).deleteMany({}); - } - await _mongoose.disconnect(); -} - -(async () => { - const keystone = await InitKeystone(); - console.log('K = ' + keystone); - - const ns = 'refactortime'; - const skipAccessControl = true; - - const identity = { - id: null, - username: 'sample_username', - namespace: ns, - roles: JSON.stringify(['api-owner']), - scopes: [], - userId: null, - } as any; - - const ctx = keystone.createContext({ - skipAccessControl, - authentication: { item: identity }, - }); - - await cleanupDatabase(); - - let index = 1; - for (const test of testdata.tests) { - const json: any = test.data; - testHeading(index++, test.name); - try { - if ((test.method || 'PUT') === 'PUT') { - const res = await syncRecords( - ctx, - test.entity, - json[test.refKey], - json - ); - equalPayload(removeKeys(res, ['id', 'ownedBy']), test.expected.payload); - } else { - const where: BatchWhereClause = test.whereClause; - const records: any[] = await getRecords( - ctx, - test.entity, - null, - test.responseFields, - where - ); - const payload = records.map((o) => removeKeys(o, ['id', 'appId'])); - equalPayload(payload, test.expected.payload); - } - } catch (e) { - logger.error(e.message); - if ( - !test.expected?.exception || - test.expected?.exception != `${e.message}` - ) { - throw e; - } - } - } - - testHeading(index, 'DONE'); - - await keystone.disconnect(); -})(); diff --git a/src/test/integrated/keystonejs/init.ts b/src/test/integrated/keystonejs/init.ts index 1930b2ec8..e69f7e11e 100644 --- a/src/test/integrated/keystonejs/init.ts +++ b/src/test/integrated/keystonejs/init.ts @@ -1,8 +1,6 @@ /* node dist/test/integrated/keystonejs/test.js */ -import { syncRecords } from '../../../batch/feed-worker'; - import { loadRulesAndWatch } from '../../../authz/enforcement'; loadRulesAndWatch(false); @@ -17,6 +15,20 @@ export default async function InitKeystone( const session = require('express-session'); //const MongoStore = require('connect-mongo')(session); + const { KnexAdapter } = require('@keystonejs/adapter-knex'); + const knexAdapterConfig = { + knexOptions: { + debug: process.env.LOG_LEVEL === 'debug' ? false : false, + connection: { + host: process.env.KNEX_HOST, + port: process.env.KNEX_PORT, + user: process.env.KNEX_USER, + password: process.env.KNEX_PASSWORD, + database: process.env.KNEX_DATABASE, + }, + }, + }; + const { MongooseAdapter } = require('@keystonejs/adapter-mongoose'); const mongooseAdapterConfig = { mongoUri: process.env.MONGO_URL, @@ -24,8 +36,13 @@ export default async function InitKeystone( pass: process.env.MONGO_PASSWORD, }; + const adapter = process.env.ADAPTER ? process.env.ADAPTER : 'mongoose'; + const keystone = new Keystone({ - adapter: new MongooseAdapter(mongooseAdapterConfig), + adapter: + adapter == 'knex' + ? new KnexAdapter(knexAdapterConfig) + : new MongooseAdapter(mongooseAdapterConfig), cookieSecret: process.env.COOKIE_SECRET, cookie: { secure: process.env.COOKIE_SECURE === 'true', // Default to true in production diff --git a/src/test/services/batch/integrated-batch.test.ts b/src/test/services/batch/integrated-batch.test.ts new file mode 100644 index 000000000..0950a6d51 --- /dev/null +++ b/src/test/services/batch/integrated-batch.test.ts @@ -0,0 +1,108 @@ +/* +Wire up directly with Keycloak and use the Services +To run: +npm run intg-build +npm run ts-watch +node dist/test/integrated/batchworker/testsuite/run.js +*/ + +import InitKeystone from '../../integrated/keystonejs/init'; +import { + removeKeys, + syncRecords, + getRecords, +} from '../../../batch/feed-worker'; +import yaml from 'js-yaml'; +import { strict as assert } from 'assert'; + +import testdata from './testdata'; +import { Logger } from '../../../logger'; +import { BatchWhereClause } from '@/services/keystone/batch-service'; + +const logger = Logger('testsuite'); + +function equalPayload(a: any, e: any) { + assert.strictEqual( + yaml.dump(a, { indent: 2, lineWidth: 100 }), + yaml.dump(e, { indent: 2, lineWidth: 100 }) + ); +} + +function testHeading(index: number, name: string) { + console.log('\x1b[33m --------------------------------------------- \x1b[0m'); + console.log('\x1b[33m ' + index + ' ' + name + ' \x1b[0m'); + console.log('\x1b[33m --------------------------------------------- \x1b[0m'); +} + +describe('Batch Tests', function () { + it(`should pass all tests`, async function () { + await (async () => { + const keystone = await InitKeystone(); + console.log('K = ' + keystone); + + const ns = 'refactortime'; + const skipAccessControl = true; + + const identity = { + id: null, + username: 'sample_username', + namespace: ns, + roles: JSON.stringify(['api-owner']), + scopes: [], + userId: null, + } as any; + + const ctx = keystone.createContext({ + skipAccessControl, + authentication: { item: identity }, + }); + + //await cleanupDatabase(); + + let index = 1; + for (const test of testdata.tests) { + const json: any = test.data; + testHeading(index++, test.name); + try { + if ((test.method || 'PUT') === 'PUT') { + const res = await syncRecords( + ctx, + test.entity, + json[test.refKey], + json + ); + equalPayload( + removeKeys(res, ['id', 'ownedBy']), + test.expected.payload + ); + } else { + const where: BatchWhereClause = test.whereClause; + const records: any[] = await getRecords( + ctx, + test.entity, + null, + test.responseFields, + where + ); + const payload = records.map((o) => removeKeys(o, ['id', 'appId'])); + equalPayload(payload, test.expected.payload); + } + } catch (e) { + logger.error(e.message); + if ( + !test.expected?.exception || + test.expected?.exception != `${e.message}` + ) { + await keystone.disconnect(); + + throw e; + } + } + } + + testHeading(index, 'DONE'); + + await keystone.disconnect(); + })(); + }); +}); diff --git a/src/test/integrated/batchworker/testsuite/testdata.js b/src/test/services/batch/testdata.js similarity index 89% rename from src/test/integrated/batchworker/testsuite/testdata.js rename to src/test/services/batch/testdata.js index d74468070..93c4124ae 100644 --- a/src/test/integrated/batchworker/testsuite/testdata.js +++ b/src/test/services/batch/testdata.js @@ -1,5 +1,51 @@ export default { tests: [ + { + name: 'create an organization', + entity: 'Organization', + refKey: 'extForeignKey', + data: { + name: 'ministry-of-health', + title: 'Ministry of Health', + extForeignKey: '01', + extSource: 'ckan', + extRecordHash: '', + orgUnits: [ + { + id: '319b3297-846d-4b97-8095-ceb3ec505fb8', + name: 'planning-and-innovation-division', + title: 'Planning and Innovation Division', + extSource: 'ckan', + extRecordHash: '', + }, + { + id: '319b3297-846d-4b97-8095-ceb3ec505fb7', + name: 'public-health', + title: 'Public Health', + extSource: 'ckan', + extRecordHash: '', + }, + ], + }, + expected: { + payload: { + status: 200, + result: 'created', + childResults: [ + { + status: 200, + result: 'created', + childResults: [], + }, + { + status: 200, + result: 'created', + childResults: [], + }, + ], + }, + }, + }, { name: 'create a new product', entity: 'Product', @@ -269,8 +315,9 @@ export default { payload: { status: 400, result: 'create-failed', - reason: - "Product name must be between 3 and 100 alpha-numeric characters (including special characters ' {}&-')", + reason: 'You attempted to perform an invalid mutation', + // reason: + // "Product name must be between 3 and 100 alpha-numeric characters (including special characters ' {}&-')", childResults: [], }, }, @@ -286,8 +333,9 @@ export default { payload: { status: 400, result: 'create-failed', - reason: - "Product name must be between 3 and 100 alpha-numeric characters (including special characters ' {}&-')", + reason: 'You attempted to perform an invalid mutation', + // reason: + // "Product name must be between 3 and 100 alpha-numeric characters (including special characters ' {}&-')", childResults: [], }, }, @@ -449,8 +497,8 @@ export default { entity: 'DraftDataset', refKey: 'name', data: { - name: 'delete-auto-test-product', - title: 'Delete-Auto Test Product', + name: 'my-draft-product', + title: 'My Draft Product', notes: 'API Gateway Services provides a way to configure services on the API Gateway, manage access to APIs and get insight into the use of them.', tags: ['gateway', 'kong', 'openapi'], @@ -470,12 +518,12 @@ export default { }, }, { - name: 'update DraftDataset', + name: 'update security class in existing DraftDataset', entity: 'DraftDataset', refKey: 'name', data: { - name: 'delete-auto-test-product', - title: 'Delete-Auto Test Product', + name: 'my-draft-product', + title: 'My Draft Product', notes: 'API Gateway Services provides a way to configure services on the API Gateway, manage access to APIs and get insight into the use of them.', tags: ['gateway', 'kong', 'openapi'], @@ -499,8 +547,8 @@ export default { entity: 'DraftDataset', refKey: 'name', data: { - name: 'delete-auto-test-product', - title: 'Delete-Auto Test Product', + name: 'my-draft-product', + title: 'My Draft Product', notes: 'API Gateway Services provides a way to configure services on the API Gateway, manage access to APIs and get insight into the use of them.', tags: ['gateway', 'kong', 'openapi'],