diff --git a/.env.local b/.env.local index 6f01ff65..28676cab 100644 --- a/.env.local +++ b/.env.local @@ -1,9 +1,9 @@ -DB_URI=postgres://reporting_dashboard_role:reportingdashboard123@localhost:5432/reporting_dashboard_dev -POSTGRES_HOST=0.0.0.0 -POSTGRES_DB=reporting_dashboard_dev -POSTGRES_USER=reporting_dashboard_role -POSTGRES_PASSWORD=reportingdashboard123 +DB_URI=postgres://postgres:root@host.docker.internal:5432/negt +POSTGRES_HOST=host.docker.internal +POSTGRES_DB=negt +POSTGRES_USER=postgres +POSTGRES_PASSWORD=root NODE_ENV=local ACCESS_TOKEN_SECRET=4cd7234152590dcfe77e1b6fc52e84f4d30c06fddadd0dd2fb42cbc51fa14b1bb195bbe9d72c9599ba0c6b556f9bd1607a8478be87e5a91b697c74032e0ae7af -REDIS_HOST=localhost +REDIS_HOST=host.docker.internal REDIS_PORT=6379 \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..d88c2380 --- /dev/null +++ b/.env.test @@ -0,0 +1,12 @@ +ENVIRONMENT_NAME=test +NODE_ENV=test + +POSTGRES_DB=test_mock +POSTGRES_USER=test_mock +POSTGRES_PASSWORD=test123 +POSTGRES_PORT=54320 + +DB_URI=postgres://test_mock:test123@localhost:54320/test_mock?sslmode=disable + +REDIS_DOMAIN=test +REDIS_PORT=6380 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f61d625c..669f681f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: run: yarn lint - name: Test run: yarn test + - name: Run Integration Test + run: yarn test:integration - name: Build run: yarn build:local - name: SonarQube Scan diff --git a/.gitignore b/.gitignore index e977e742..4b13fbb8 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,7 @@ typings/ # DS_Store .DS_Store report.json -reports/test-report.xml \ No newline at end of file +reports/test-report.xml + +# Docker slim +slim.report.json \ No newline at end of file diff --git a/__tests__/server/gql/model/address.test.js b/__tests__/server/gql/model/address.test.js new file mode 100644 index 00000000..b32bbb0a --- /dev/null +++ b/__tests__/server/gql/model/address.test.js @@ -0,0 +1,65 @@ +const { closeRedisConnection } = require('@server/services/redis'); +const { getMockDBEnv, getResponse } = require('@server/utils/testUtils'); +const { get } = require('lodash'); + +const createAddressMutation = ` + mutation { + createAddress( + latitude: 123.456, + longitude: -78.90, + address1: "123 Main St", + address2: "France", + city: "Sample City", + country: "Sample Country" + ) { + id + address1 + address2 + city + country + latitude + longitude + createdAt + updatedAt + deletedAt + } + } +`; + +describe('Integration test for createAddress mutation', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + jest.unmock('@database'); + jest.unmock('@database/models'); + jest.unmock('ioredis'); + process.env = { ...OLD_ENV }; + process.env = { ...process.env, ...getMockDBEnv(), REDIS_PORT: 6380 }; + }); + + afterAll(async () => { + process.env = OLD_ENV; // Restore old environment + await closeRedisConnection(); // avoid jest open handle error + }); + + it('should create an address', async () => { + const response = await getResponse(createAddressMutation); + + // Assuming your mutation returns the created address + const createdAddress = get(response, 'body.data.createAddress'); + + // Perform assertions based on the response + expect(createdAddress).toBeTruthy(); + expect(createdAddress.id).toBeTruthy(); + expect(createdAddress.address1).toBe('123 Main St'); + expect(createdAddress.address2).toBe('France'); + expect(createdAddress.city).toBe('Sample City'); + expect(createdAddress.country).toBe('Sample Country'); + expect(createdAddress.latitude).toBe(123.456); + expect(createdAddress.longitude).toBe(-78.9); + expect(createdAddress.createdAt).toBeTruthy(); + expect(createdAddress.updatedAt).toBeTruthy(); + expect(createdAddress.deletedAt).toBeNull(); + }); +}); diff --git a/__tests__/server/gql/model/products.test.js b/__tests__/server/gql/model/products.test.js new file mode 100644 index 00000000..f2170429 --- /dev/null +++ b/__tests__/server/gql/model/products.test.js @@ -0,0 +1,54 @@ +const { setRedisData, getRedisData, closeRedisConnection } = require('@server/services/redis'); +const { getMockDBEnv, getResponse } = require('@server/utils/testUtils'); +const { get } = require('lodash'); + +const getProductsQueryWhere = ` +query products{ + products(limit: 10, offset: 1){ + edges{ + node{ + id + name + } + } + } +} +`; + +describe('Integration test for products', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + jest.unmock('@database'); + jest.unmock('@database/models'); + jest.unmock('ioredis'); + process.env = { ...OLD_ENV }; + process.env = { ...process.env, ...getMockDBEnv(), REDIS_PORT: 6380 }; + }); + + afterAll(async () => { + process.env = OLD_ENV; // Restore old environment + await closeRedisConnection(); // avoid jest open handle error + const actualRedis = jest.requireActual('@server/services/redis').redis; + await actualRedis.quit(); + }); + + it('should check and get products', async () => { + const response = await getResponse(getProductsQueryWhere); + const productResult = get(response, 'body.data.products.edges'); + expect(productResult?.length).toBeGreaterThan(0); + expect(productResult[0].node).toMatchObject({ + id: expect.anything(), + name: expect.any(String) + }); + }); + + it('should set and get data from redis', async () => { + const testKey = 'product'; + const testValue = 'test'; + await setRedisData(testKey, testValue); + const result = await getRedisData(testKey); + expect(result).toBe(testValue); + }); +}); diff --git a/jest.config.integration.json b/jest.config.integration.json new file mode 100644 index 00000000..0d79e374 --- /dev/null +++ b/jest.config.integration.json @@ -0,0 +1,44 @@ +{ + "testEnvironment": "node", + "setupFilesAfterEnv": ["./jest.setup.integration.js"], + "roots": ["__tests__"], + "reporters": [ + "default", + [ + "jest-sonar", + { + "outputDirectory": "reports", + "outputName": "test-report.xml", + "relativeRootDir": "./", + "reportedFilePath": "relative" + } + ] + ], + "collectCoverageFrom": [ + "**/server/**", + "!**/node_modules/**", + "!**/dist/**", + "!**/server/database/models/**", + "!**/server/utils/testUtils/**", + "!**/server/utils/configureEnv.js", + "!**server/middleware/logger/index.js" + ], + "coverageReporters": ["json-summary", "text", "lcov"], + "testPathIgnorePatterns": ["/dist/"], + "moduleNameMapper": { + "@server(.*)$": "/server/$1", + "@(database|services|gql|middleware|daos|utils)(.*)$": "/server/$1/$2", + "@config(.*)$": "/config/$1", + "slack-notify": "/node_modules/slack-notify/src/cjs/index.js" + }, + "coverageThreshold": { + "global": { + "statements": 82, + "branches": 82, + "functions": 82, + "lines": 82 + } + }, + "globalSetup": "./test_setup/global-setup.js", + "globalTeardown": "./test_setup/global-teardown.js" +} diff --git a/jest.config.json b/jest.config.json index 011b2805..b48102bc 100644 --- a/jest.config.json +++ b/jest.config.json @@ -23,7 +23,7 @@ "!**server/middleware/logger/index.js" ], "coverageReporters": ["json-summary", "text", "lcov"], - "testPathIgnorePatterns": ["/dist/"], + "testPathIgnorePatterns": ["/dist/", "/__tests__/"], "moduleNameMapper": { "@server(.*)$": "/server/$1", "@(database|services|gql|middleware|daos|utils)(.*)$": "/server/$1/$2", diff --git a/jest.setup.integration.js b/jest.setup.integration.js new file mode 100644 index 00000000..c31f5b12 --- /dev/null +++ b/jest.setup.integration.js @@ -0,0 +1,17 @@ +import { DB_ENV } from '@utils/testUtils/mockData'; + +require('dotenv').config({ + path: '.env.test' +}); + +process.env.ENVIRONMENT_NAME = 'test'; + +beforeEach(() => { + process.env = { ...process.env, ...DB_ENV, ENVIRONMENT_NAME: 'test' }; +}); + +afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.resetModules(); +}); diff --git a/jsconfig.json b/jsconfig.json index c5cdb7b0..cde22271 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -10,7 +10,8 @@ "@daos/*": ["server/daos/*"], "@database/*": ["server/database/*"], "@gql/*": ["server/gql/*"], - "@config/*": ["config/*"] + "@config/*": ["config/*"], + "@test_setup/*": ["test_setup/*"] }, "moduleResolution": "Node" }, diff --git a/package.json b/package.json index 95f0eb72..84b23289 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "prettify": "prettier --write", "precommit": "lint:staged", "test": "export ENVIRONMENT_NAME=test && npx jest --silent --forceExit --detectOpenHandles --ci --coverage --testLocationInResults --json --outputFile=\"report.json\"", + "test:integration": "export ENVIRONMENT_NAME=test && npx jest --silent --forceExit --ci --coverage --testLocationInResults --json --outputFile=\"report.json\" --config ./jest.config.integration.json", "test:badges": "npm run test && jest-coverage-badges --output './badges'", "postinstall": "link-module-alias", "preinstall": "command -v link-module-alias && link-module-alias clean || true" @@ -54,6 +55,7 @@ "cors": "^2.8.5", "crypto": "^1.0.1", "deep-map-keys": "^2.0.1", + "docker-compose": "^0.24.3", "dotenv": "^10.0.0", "eslint-loader": "^4.0.2", "express": "^4.17.1", @@ -68,6 +70,7 @@ "html-webpack-plugin": "^4.5.0", "http": "^0.0.1-security", "ioredis": "^5.2.2", + "is-port-reachable": "3.0.0", "jsonwebtoken": "^8.5.1", "loadash": "^1.0.0", "lodash": "^4.17.21", diff --git a/seeders/01_products.js b/seeders/01_products.js index 4361d5a5..169206ed 100644 --- a/seeders/01_products.js +++ b/seeders/01_products.js @@ -1,13 +1,36 @@ +const products = [ + { + id: 1, + name: 'test1', + category: 'category1', + amount: 10 + }, + { + id: 2, + name: 'test2', + category: 'category2', + amount: 20 + }, + { + id: 3, + name: 'test3', + category: 'category3', + amount: 30 + } +]; + module.exports = { up: queryInterface => { const faker = require('faker'); const range = require('lodash/range'); - const arr = range(1, 2000).map((value, index) => ({ + const arr = range(4, 2000).map((value, index) => ({ + id: value, name: faker.commerce.productName(), category: faker.commerce.department(), amount: parseFloat(faker.commerce.price()) * 100 })); - return queryInterface.bulkInsert('products', arr, {}); + const newArr = products.concat(arr); + return queryInterface.bulkInsert('products', newArr, {}); }, down: queryInterface => queryInterface.bulkDelete('products', null, {}) }; diff --git a/server/services/redis.js b/server/services/redis.js index 01b01e0c..8272d9dd 100644 --- a/server/services/redis.js +++ b/server/services/redis.js @@ -1,3 +1,18 @@ import Redis from 'ioredis'; export const redis = new Redis(process.env.REDIS_PORT, process.env.REDIS_HOST); + +export const setRedisData = async (key, value) => { + const data = await redis.set(key, value); + return data; +}; + +export const getRedisData = async key => { + const data = await redis.get(key); + return data; +}; + +// Add a function to close the Redis connection +export const closeRedisConnection = async () => { + await redis.quit(); +}; diff --git a/server/utils/testUtils/index.js b/server/utils/testUtils/index.js index 45b24a7a..d2638a51 100644 --- a/server/utils/testUtils/index.js +++ b/server/utils/testUtils/index.js @@ -131,6 +131,15 @@ export function mockDBClient(config = { total: 10 }) { }; } +export const getMockDBEnv = () => ({ + DB_URI: 'postgres://test_mock:test123@localhost:54320/test_mock?sslmode=disable', + DB_PORT: 54320, + DB_HOST: 'localhost', + DB_NAME: 'test_mock', + DB_USER: 'test_mock', + DB_PASSWORD: 'test123' +}); + export async function connectToMockDB() { const client = mockDBClient(); try { diff --git a/test_setup/docker-compose.yml b/test_setup/docker-compose.yml new file mode 100644 index 00000000..238092b1 --- /dev/null +++ b/test_setup/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3' + +services: + postgres: #1 + image: postgres:12-alpine #1.1 + command: postgres -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c random_page_cost=1.0 #1.2 + environment: #1.3 + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - '${POSTGRES_PORT}:5432' #1.4 + tmpfs: /var/lib/postgresql/data #.15 + + redis: #2 + image: redis:6.2-alpine + ports: + - '${REDIS_PORT}:6379' + +networks: #3 + default: + name: 'test-server-network' diff --git a/test_setup/global-setup.js b/test_setup/global-setup.js new file mode 100644 index 00000000..f8d1b440 --- /dev/null +++ b/test_setup/global-setup.js @@ -0,0 +1,37 @@ +// Replace the require statement with dynamic import +const { execSync } = require('child_process'); +const { join } = require('path'); +const { upAll, exec } = require('docker-compose'); +const dotenv = require('dotenv'); +const isPortReachable = require('is-port-reachable'); + +module.exports = async () => { + console.time('global-setup'); + dotenv.config({ path: '.env.test' }); + + // Use the imported module + const isDBReachable = await isPortReachable(54320); + + if (isDBReachable) { + console.log('DB already started'); + } else { + console.log('\nStarting up dependencies please wait...\n'); + + await upAll({ + cwd: join(__dirname), + log: true + }); + + await exec('postgres', ['sh', '-c', 'until pg_isready ; do sleep 1; done'], { + cwd: join(__dirname) + }); + + console.log('Running migrations...'); + execSync('npx sequelize db:migrate'); + + console.log('Seeding the db...'); + execSync('npx sequelize db:seed:all'); + } + + console.timeEnd('global-setup'); +}; diff --git a/test_setup/global-teardown.js b/test_setup/global-teardown.js new file mode 100644 index 00000000..beecec94 --- /dev/null +++ b/test_setup/global-teardown.js @@ -0,0 +1,12 @@ +const { closeRedisConnection } = require('@server/services/redis'); +const { down } = require('docker-compose'); +const { join } = require('path'); + +module.exports = async () => { + // Close the Redis connection + await closeRedisConnection(); + await down({ + commandOptions: ['--remove-orphans'], + cwd: join(__dirname) + }); +}; diff --git a/yarn.lock b/yarn.lock index 45536472..fe138c71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4468,20 +4468,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001219: - version "1.0.30001245" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4" - integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA== - -caniuse-lite@^1.0.30001312: - version "1.0.30001313" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001313.tgz#a380b079db91621e1b7120895874e2fd62ed2e2f" - integrity sha512-rI1UN0koZUiKINjysQDuRi2VeSCce3bYJNmDcj3PIKREiAmjakugBul1QSkg/fPrlULYl6oWfGg3PbgOSY9X4Q== - -caniuse-lite@^1.0.30001370: - version "1.0.30001374" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz#3dab138e3f5485ba2e74bd13eca7fe1037ce6f57" - integrity sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw== +caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001312, caniuse-lite@^1.0.30001370: + version "1.0.30001576" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz" + integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== capture-exit@^2.0.0: version "2.0.0" @@ -5261,6 +5251,13 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +docker-compose@^0.24.3: + version "0.24.3" + resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.24.3.tgz#298d7bb4aaf37b3b45d0e4ef55c7f58ccc39cca9" + integrity sha512-x3/QN3AIOMe7j2c8f/jcycizMft7dl8MluoB9OGPAYCyKHHiPUFqI9GjCcsU0kYy24vYKMCcfR6+5ZaEyQlrxg== + dependencies: + yaml "^2.2.2" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -7328,6 +7325,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-port-reachable@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-port-reachable/-/is-port-reachable-3.0.0.tgz#edf721e7d354e6e00cbeb0fc174ad89bdf6056b3" + integrity sha512-056IzLiWHdgVd6Eq1F9HtJl+cIkvi5X2MJ/A1fjQtByHkzQE1wGardnPhqrarOGDF88BOW+297X7PDvZ2vcyVg== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -12544,6 +12546,11 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"