From c29a4f847940005da0bd0a6ea730f117385abfca Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 30 Jan 2024 11:42:26 +0545 Subject: [PATCH 01/47] feat(#425): Create e2e test setup --- package.json | 1 + test/e2e/test_runner.sh | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100755 test/e2e/test_runner.sh diff --git a/package.json b/package.json index f28dcbe8..293a69c5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB", "docker-stop-couchdb": "docker stop cht-conf-couchdb || true", "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb", + "test-e2e": "npm run eslint && ./test/e2e/test_runner.sh", "semantic-release": "semantic-release" }, "bin": { diff --git a/test/e2e/test_runner.sh b/test/e2e/test_runner.sh new file mode 100755 index 00000000..859457d4 --- /dev/null +++ b/test/e2e/test_runner.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +set -eu + +chtCoreDockerComposeFile='cht-docker-compose.sh' +chtCoreDockerComposeUrl='https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh' +chtCoreProjectName='cht_conf_e2e_tests' +chtConfe2eFolderName='e2e_tests' +currentDir=$(pwd) + +log_info() { + echo "[INFO] $1" +} + +log_error() { + echo "[ERROR] $1" +} + +setup() { + log_info "Starting cht-conf e2e tests" + + mkdir -p "$chtConfe2eFolderName" + cd "$chtConfe2eFolderName" + + log_info "Setting up cht-core" + + curl -s -o "$chtCoreDockerComposeFile" "$chtCoreDockerComposeUrl" + chmod +x "$chtCoreDockerComposeFile" + + if { + echo "y" + echo "y" + echo "$chtCoreProjectName" + } | ./"$chtCoreDockerComposeFile"; then + log_info "cht-core setup complete." + else + log_error "Failed to set up cht-core. Manual cleanup may be required. Exiting." + exit 1 + fi + + # since the setup is completed if the execution flow + # reaches here, this makes sure that the destruction of + # the setup always occurs + trap destroy EXIT + trap destroy ERR +} + +run_tests() { + log_info "Running e2e tests" + + nyc --reporter=html mocha --forbid-only "../test/e2e/**/*.spec.js" + + log_info "e2e tests complete" +} + +destroy() { + log_info "Destroying cht-core." + + if ./"$chtCoreDockerComposeFile" "$chtCoreProjectName.env" destroy; then + log_info "cht-core destroy complete. Exiting." + else + log_error "Failed to destroy cht-core. Manual cleanup may be required." + fi + + cd "$currentDir" + rm -rf "$chtConfe2eFolderName" +} + +setup + +run_tests From 208fd8591d737f8b7755f05b47a2d674210c4f50 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 20 Jun 2024 16:30:17 +0545 Subject: [PATCH 02/47] Setup mocha hooks and add to npm run command --- package.json | 2 +- test/e2e/.mocharc.js | 20 ++++++++++++++++++++ test/e2e/hooks.js | 15 +++++++++++++++ test/e2e/placeholder.spec.js | 7 +++++++ test/e2e/specs.js | 5 +++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/e2e/.mocharc.js create mode 100644 test/e2e/hooks.js create mode 100644 test/e2e/placeholder.spec.js create mode 100644 test/e2e/specs.js diff --git a/package.json b/package.json index 293a69c5..95c5eecf 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB", "docker-stop-couchdb": "docker stop cht-conf-couchdb || true", "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb", - "test-e2e": "npm run eslint && ./test/e2e/test_runner.sh", + "test-e2e": "npm run eslint && mocha --config test/e2e/.mocharc.js", "semantic-release": "semantic-release" }, "bin": { diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js new file mode 100644 index 00000000..6ad8a1f9 --- /dev/null +++ b/test/e2e/.mocharc.js @@ -0,0 +1,20 @@ +const chaiExclude = require('chai-exclude'); +const chaiAsPromised = require('chai-as-promised'); +const chai = require('chai'); +chai.use(chaiAsPromised); +chai.use(chaiExclude); + +module.exports = { + allowUncaught: false, + color: true, + checkLeaks: true, + fullTrace: true, + asyncOnly: false, + spec: require('./specs').all, + timeout: 200 * 1000, //API takes a little long to start up + reporter: 'spec', + file: [ 'test/e2e/hooks.js' ], + captureFile: 'test/e2e/results.txt', + exit: true, + recursive: true, +}; diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js new file mode 100644 index 00000000..a751b6e3 --- /dev/null +++ b/test/e2e/hooks.js @@ -0,0 +1,15 @@ +before(() => { + console.log('before'); +}); + +after(() => { + console.log('after'); +}); + +beforeEach(() => { + console.log('beforeEach'); +}); + +afterEach(() => { + console.log('afterEach'); +}); \ No newline at end of file diff --git a/test/e2e/placeholder.spec.js b/test/e2e/placeholder.spec.js new file mode 100644 index 00000000..f5154a76 --- /dev/null +++ b/test/e2e/placeholder.spec.js @@ -0,0 +1,7 @@ +const assert = require('assert'); + +describe('placeholder', () => { + it('checks if the mocha test setup works', () => { + assert.strictEqual(1, 1); + }); +}); \ No newline at end of file diff --git a/test/e2e/specs.js b/test/e2e/specs.js new file mode 100644 index 00000000..8deec4aa --- /dev/null +++ b/test/e2e/specs.js @@ -0,0 +1,5 @@ +module.exports = { + all: [ + 'test/e2e/**/*.spec.js' + ] +}; \ No newline at end of file From 7f984955b5b217e2a684c64504ea4689be0a63da Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 27 Jun 2024 16:35:23 +0200 Subject: [PATCH 03/47] orchestrate with mocha hooks --- .gitignore | 1 + test/e2e/.mocharc.js | 2 +- test/e2e/hooks.js | 63 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2c86ae6e..39c909fa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage .nyc_output .DS_Store test/.DS_Store +test/e2e/.cht-docker-helper diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js index 6ad8a1f9..dd3a5a76 100644 --- a/test/e2e/.mocharc.js +++ b/test/e2e/.mocharc.js @@ -13,7 +13,7 @@ module.exports = { spec: require('./specs').all, timeout: 200 * 1000, //API takes a little long to start up reporter: 'spec', - file: [ 'test/e2e/hooks.js' ], + file: ['test/e2e/hooks.js'], captureFile: 'test/e2e/results.txt', exit: true, recursive: true, diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index a751b6e3..bebc677f 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -1,9 +1,66 @@ -before(() => { +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const { spawn } = require('child_process'); + +const projectName = 'cht_conf_e2e_tests'; +const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); +const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-compose.sh'); + +const downloadDockerHelperScript = () => new Promise((resolve, reject) => { + const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); + https + .get('https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { + response.pipe(file); + file.on('finish', () => file.close(resolve)); + file.on('error', () => file.close(reject)); + }) + .on('error', () => { + fs.unlinkSync(file.path); + file.close(() => reject('Failed to download CHT Docker Helper script "cht-docker-compose.sh"')); + }); +}); + +const spinUpCHT = () => new Promise((resolve, reject) => { + const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); + + const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); + if (fs.existsSync(configFile)) { + childProcess.stdin.write('n\n'); + childProcess.stdin.write('1\n'); + } else { + childProcess.stdin.write('y\n'); + childProcess.stdin.write('y\n'); + childProcess.stdin.write(`${projectName}\n`); + } +}); + +const takeDownCHT = () => new Promise((resolve, reject) => { + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'stop'], { cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); +}); + +before(async () => { console.log('before'); + + if (!fs.existsSync(dockerHelperDirectory)) { + fs.mkdirSync(dockerHelperDirectory); + } + + if (!fs.existsSync(dockerHelperScript)) { + await downloadDockerHelperScript(); + } + + await spinUpCHT(); }); -after(() => { +after(async () => { console.log('after'); + + await takeDownCHT(); }); beforeEach(() => { @@ -12,4 +69,4 @@ beforeEach(() => { afterEach(() => { console.log('afterEach'); -}); \ No newline at end of file +}); From 86a0e97be6bc33451481773e550d52f7b2dccd0c Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 13:42:41 +0200 Subject: [PATCH 04/47] test run in the ci --- .github/workflows/build.yml | 24 ++++++++++++++++++++++++ test/e2e/hooks.js | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d439f55f..907e1b2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,3 +30,27 @@ jobs: coverage .nyc_output if: ${{ failure() }} + + e2e: + name: E2E tests + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.x + - name: Install dependencies + run: | + pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic + npm ci + - name: Run E2E tests + run: npm test-e2e +# - name: Archive Results +# uses: actions/upload-artifact@v2 +# with: +# name: Coverage Report +# path: | +# coverage +# .nyc_output +# if: ${{ failure() }} diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index bebc677f..47c014a9 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -38,7 +38,7 @@ const spinUpCHT = () => new Promise((resolve, reject) => { }); const takeDownCHT = () => new Promise((resolve, reject) => { - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'stop'], { cwd: dockerHelperDirectory }); + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { cwd: dockerHelperDirectory }); childProcess.on('error', reject); childProcess.on('close', resolve); }); From 4eda2bfdf6ac823e0e5a66eeb366c13f001c4db0 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 14:35:52 +0200 Subject: [PATCH 05/47] test run in the ci --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 907e1b2d..adeb6395 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic npm ci - name: Run E2E tests - run: npm test-e2e + run: npm run test-e2e # - name: Archive Results # uses: actions/upload-artifact@v2 # with: From 8bca220ff0bbf486dec26b45d1f85306d1dc72ad Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 15:17:00 +0200 Subject: [PATCH 06/47] move session-token test to test/integration folder --- package.json | 2 +- test/e2e/.mocharc.js | 8 +------- test/e2e/placeholder.spec.js | 2 +- test/e2e/specs.js | 5 ----- test/{e2e => integration}/session-token.spec.js | 4 ++-- 5 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 test/e2e/specs.js rename test/{e2e => integration}/session-token.spec.js (98%) diff --git a/package.json b/package.json index 95c5eecf..9a73fc9e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "eslint": "eslint 'src/**/*.js' test/*.js 'test/**/*.js'", "docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB", "docker-stop-couchdb": "docker stop cht-conf-couchdb || true", - "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb", + "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" --exclude \"../../test/e2e/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb", "test-e2e": "npm run eslint && mocha --config test/e2e/.mocharc.js", "semantic-release": "semantic-release" }, diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js index dd3a5a76..71ba560f 100644 --- a/test/e2e/.mocharc.js +++ b/test/e2e/.mocharc.js @@ -1,16 +1,10 @@ -const chaiExclude = require('chai-exclude'); -const chaiAsPromised = require('chai-as-promised'); -const chai = require('chai'); -chai.use(chaiAsPromised); -chai.use(chaiExclude); - module.exports = { allowUncaught: false, color: true, checkLeaks: true, fullTrace: true, asyncOnly: false, - spec: require('./specs').all, + spec: ['test/e2e/**/*.spec.js'], timeout: 200 * 1000, //API takes a little long to start up reporter: 'spec', file: ['test/e2e/hooks.js'], diff --git a/test/e2e/placeholder.spec.js b/test/e2e/placeholder.spec.js index f5154a76..cb892267 100644 --- a/test/e2e/placeholder.spec.js +++ b/test/e2e/placeholder.spec.js @@ -4,4 +4,4 @@ describe('placeholder', () => { it('checks if the mocha test setup works', () => { assert.strictEqual(1, 1); }); -}); \ No newline at end of file +}); diff --git a/test/e2e/specs.js b/test/e2e/specs.js deleted file mode 100644 index 8deec4aa..00000000 --- a/test/e2e/specs.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - all: [ - 'test/e2e/**/*.spec.js' - ] -}; \ No newline at end of file diff --git a/test/e2e/session-token.spec.js b/test/integration/session-token.spec.js similarity index 98% rename from test/e2e/session-token.spec.js rename to test/integration/session-token.spec.js index b0e0c0ca..52eafb66 100644 --- a/test/e2e/session-token.spec.js +++ b/test/integration/session-token.spec.js @@ -56,7 +56,7 @@ const runCliCommand = (command) => { }); }; -describe('e2e/session-token', function() { +describe('integration/session-token', function() { this.timeout(15000); let sessionToken; @@ -149,4 +149,4 @@ describe('e2e/session-token', function() { // Bad Request: Malformed AuthSession cookie .that.contains('INFO Error: Received error code 400'); }); -}); \ No newline at end of file +}); From 1402640b0f7edd7a3a708ed0affe6f6cc1d40089 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 15:19:47 +0200 Subject: [PATCH 07/47] add trace logs --- test/e2e/hooks.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 47c014a9..48c66baa 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -55,12 +55,16 @@ before(async () => { } await spinUpCHT(); + + console.log('cht up'); }); after(async () => { console.log('after'); await takeDownCHT(); + + console.log('cht down'); }); beforeEach(() => { From 09007420a4b5742a1e4503a0bc6da0903cbccadd Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 15:24:33 +0200 Subject: [PATCH 08/47] add trace logs --- test/e2e/hooks.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 48c66baa..76e27cb0 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -43,6 +43,14 @@ const takeDownCHT = () => new Promise((resolve, reject) => { childProcess.on('close', resolve); }); +const time = async (fn, label) => { + const before = Date.now(); + const res = await fn(); + const took = Date.now() - before; + console.log(`${label} took ${took}ms`); + return res; +}; + before(async () => { console.log('before'); @@ -51,10 +59,10 @@ before(async () => { } if (!fs.existsSync(dockerHelperScript)) { - await downloadDockerHelperScript(); + await time(downloadDockerHelperScript, 'download docker helper script'); } - await spinUpCHT(); + await time(spinUpCHT, 'spin up cht'); console.log('cht up'); }); @@ -62,7 +70,7 @@ before(async () => { after(async () => { console.log('after'); - await takeDownCHT(); + await time(takeDownCHT, 'take down cht'); console.log('cht down'); }); From 8143ac8c236588f109cd2a065da1033982187da3 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 15:33:45 +0200 Subject: [PATCH 09/47] remove traces --- .github/workflows/build.yml | 8 ----- test/e2e/hooks.js | 26 +++----------- test/e2e/test_runner.sh | 71 ------------------------------------- 3 files changed, 5 insertions(+), 100 deletions(-) delete mode 100755 test/e2e/test_runner.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index adeb6395..e1299af7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,11 +46,3 @@ jobs: npm ci - name: Run E2E tests run: npm run test-e2e -# - name: Archive Results -# uses: actions/upload-artifact@v2 -# with: -# name: Coverage Report -# path: | -# coverage -# .nyc_output -# if: ${{ failure() }} diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 76e27cb0..0321f79a 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -43,42 +43,26 @@ const takeDownCHT = () => new Promise((resolve, reject) => { childProcess.on('close', resolve); }); -const time = async (fn, label) => { - const before = Date.now(); - const res = await fn(); - const took = Date.now() - before; - console.log(`${label} took ${took}ms`); - return res; -}; - before(async () => { - console.log('before'); - if (!fs.existsSync(dockerHelperDirectory)) { fs.mkdirSync(dockerHelperDirectory); } if (!fs.existsSync(dockerHelperScript)) { - await time(downloadDockerHelperScript, 'download docker helper script'); + await downloadDockerHelperScript(); } - await time(spinUpCHT, 'spin up cht'); - - console.log('cht up'); + await spinUpCHT(); }); after(async () => { - console.log('after'); - - await time(takeDownCHT, 'take down cht'); - - console.log('cht down'); + await takeDownCHT(); }); beforeEach(() => { - console.log('beforeEach'); + // console.log('beforeEach'); }); afterEach(() => { - console.log('afterEach'); + // console.log('afterEach'); }); diff --git a/test/e2e/test_runner.sh b/test/e2e/test_runner.sh deleted file mode 100755 index 859457d4..00000000 --- a/test/e2e/test_runner.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -set -eu - -chtCoreDockerComposeFile='cht-docker-compose.sh' -chtCoreDockerComposeUrl='https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh' -chtCoreProjectName='cht_conf_e2e_tests' -chtConfe2eFolderName='e2e_tests' -currentDir=$(pwd) - -log_info() { - echo "[INFO] $1" -} - -log_error() { - echo "[ERROR] $1" -} - -setup() { - log_info "Starting cht-conf e2e tests" - - mkdir -p "$chtConfe2eFolderName" - cd "$chtConfe2eFolderName" - - log_info "Setting up cht-core" - - curl -s -o "$chtCoreDockerComposeFile" "$chtCoreDockerComposeUrl" - chmod +x "$chtCoreDockerComposeFile" - - if { - echo "y" - echo "y" - echo "$chtCoreProjectName" - } | ./"$chtCoreDockerComposeFile"; then - log_info "cht-core setup complete." - else - log_error "Failed to set up cht-core. Manual cleanup may be required. Exiting." - exit 1 - fi - - # since the setup is completed if the execution flow - # reaches here, this makes sure that the destruction of - # the setup always occurs - trap destroy EXIT - trap destroy ERR -} - -run_tests() { - log_info "Running e2e tests" - - nyc --reporter=html mocha --forbid-only "../test/e2e/**/*.spec.js" - - log_info "e2e tests complete" -} - -destroy() { - log_info "Destroying cht-core." - - if ./"$chtCoreDockerComposeFile" "$chtCoreProjectName.env" destroy; then - log_info "cht-core destroy complete. Exiting." - else - log_error "Failed to destroy cht-core. Manual cleanup may be required." - fi - - cd "$currentDir" - rm -rf "$chtConfe2eFolderName" -} - -setup - -run_tests From 25140bafea12a0978b1d551f2fee451404697592 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 1 Jul 2024 15:46:49 +0200 Subject: [PATCH 10/47] split logic if we have an existing project configuration file --- test/e2e/hooks.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 0321f79a..08e5e201 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -22,15 +22,15 @@ const downloadDockerHelperScript = () => new Promise((resolve, reject) => { }); const spinUpCHT = () => new Promise((resolve, reject) => { - const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); - const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); if (fs.existsSync(configFile)) { - childProcess.stdin.write('n\n'); - childProcess.stdin.write('1\n'); + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); } else { + const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); childProcess.stdin.write('y\n'); childProcess.stdin.write('y\n'); childProcess.stdin.write(`${projectName}\n`); From a8963452ecffca9f686ceb6062cb31e2f1be5d22 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 3 Jul 2024 12:18:39 +0200 Subject: [PATCH 11/47] refactor hooks and extract utils functions to deal with cht-docker-helper --- test/e2e/cht-docker-utils.js | 57 ++++++++++++++++++++++++++++++++ test/e2e/hooks.js | 63 ++---------------------------------- 2 files changed, 59 insertions(+), 61 deletions(-) create mode 100644 test/e2e/cht-docker-utils.js diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js new file mode 100644 index 00000000..7ac5edd8 --- /dev/null +++ b/test/e2e/cht-docker-utils.js @@ -0,0 +1,57 @@ +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const { spawn } = require('child_process'); + +const projectName = 'cht_conf_e2e_tests'; +const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); +const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-compose.sh'); + +const downloadDockerHelperScript = () => new Promise((resolve, reject) => { + const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); + https + .get('https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { + response.pipe(file); + file.on('finish', () => file.close(resolve)); + file.on('error', () => file.close(reject)); + }) + .on('error', () => { + fs.unlinkSync(file.path); + file.close(() => reject('Failed to download CHT Docker Helper script "cht-docker-compose.sh"')); + }); +}); + +const spinUpCHT = () => new Promise(async (resolve, reject) => { + if (!fs.existsSync(dockerHelperDirectory)) { + await fs.promises.mkdir(dockerHelperDirectory); + } + + if (!fs.existsSync(dockerHelperScript)) { + await downloadDockerHelperScript(); + } + + const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); + if (fs.existsSync(configFile)) { + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); + } else { + const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); + childProcess.stdin.write('y\n'); + childProcess.stdin.write('y\n'); + childProcess.stdin.write(`${projectName}\n`); + } +}); + +const tearDownCHT = () => new Promise((resolve, reject) => { + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); +}); + +module.exports = { + spinUpCHT, + tearDownCHT, +}; diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 08e5e201..b444f12f 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -1,68 +1,9 @@ -const path = require('path'); -const fs = require('fs'); -const https = require('https'); -const { spawn } = require('child_process'); - -const projectName = 'cht_conf_e2e_tests'; -const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); -const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-compose.sh'); - -const downloadDockerHelperScript = () => new Promise((resolve, reject) => { - const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); - https - .get('https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { - response.pipe(file); - file.on('finish', () => file.close(resolve)); - file.on('error', () => file.close(reject)); - }) - .on('error', () => { - fs.unlinkSync(file.path); - file.close(() => reject('Failed to download CHT Docker Helper script "cht-docker-compose.sh"')); - }); -}); - -const spinUpCHT = () => new Promise((resolve, reject) => { - const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); - if (fs.existsSync(configFile)) { - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); - } else { - const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); - childProcess.stdin.write('y\n'); - childProcess.stdin.write('y\n'); - childProcess.stdin.write(`${projectName}\n`); - } -}); - -const takeDownCHT = () => new Promise((resolve, reject) => { - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); -}); +const { spinUpCHT, tearDownCHT } = require('./cht-docker-utils'); before(async () => { - if (!fs.existsSync(dockerHelperDirectory)) { - fs.mkdirSync(dockerHelperDirectory); - } - - if (!fs.existsSync(dockerHelperScript)) { - await downloadDockerHelperScript(); - } - await spinUpCHT(); }); after(async () => { - await takeDownCHT(); -}); - -beforeEach(() => { - // console.log('beforeEach'); -}); - -afterEach(() => { - // console.log('afterEach'); + await tearDownCHT(); }); From d456a260c23196fa433fb215934f50b75c42cb18 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 3 Jul 2024 12:36:17 +0200 Subject: [PATCH 12/47] refactor `spinUpCht` to fix eslint warning about promise executor cannot be async --- test/e2e/cht-docker-utils.js | 30 ++++++++++++++++++------------ test/e2e/hooks.js | 6 +++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 7ac5edd8..8d4bd9c8 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -21,21 +21,15 @@ const downloadDockerHelperScript = () => new Promise((resolve, reject) => { }); }); -const spinUpCHT = () => new Promise(async (resolve, reject) => { - if (!fs.existsSync(dockerHelperDirectory)) { - await fs.promises.mkdir(dockerHelperDirectory); - } - - if (!fs.existsSync(dockerHelperScript)) { - await downloadDockerHelperScript(); - } - +const startProject = () => new Promise((resolve, reject) => { const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); if (fs.existsSync(configFile)) { + // project config already exists, reuse it const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); childProcess.on('error', reject); childProcess.on('close', resolve); } else { + // initialize a new project, config will be saved to `${projectName}.env` const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); childProcess.on('error', reject); childProcess.on('close', resolve); @@ -45,13 +39,25 @@ const spinUpCHT = () => new Promise(async (resolve, reject) => { } }); -const tearDownCHT = () => new Promise((resolve, reject) => { +const spinUpCht = async () => { + if (!fs.existsSync(dockerHelperDirectory)) { + await fs.promises.mkdir(dockerHelperDirectory); + } + + if (!fs.existsSync(dockerHelperScript)) { + await downloadDockerHelperScript(); + } + + await startProject(); +}; + +const tearDownCht = () => new Promise((resolve, reject) => { const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { cwd: dockerHelperDirectory }); childProcess.on('error', reject); childProcess.on('close', resolve); }); module.exports = { - spinUpCHT, - tearDownCHT, + spinUpCht, + tearDownCht, }; diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index b444f12f..f57783da 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -1,9 +1,9 @@ -const { spinUpCHT, tearDownCHT } = require('./cht-docker-utils'); +const { spinUpCht, tearDownCht } = require('./cht-docker-utils'); before(async () => { - await spinUpCHT(); + await spinUpCht(); }); after(async () => { - await tearDownCHT(); + await tearDownCht(); }); From 639b888e9f7095319cb9a0460d4d8a299e478909 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 3 Jul 2024 19:01:45 +0200 Subject: [PATCH 13/47] finally get the first e2e test out, working as expected --- test/e2e/cht-docker-utils.js | 66 ++++++++++++++++----- test/e2e/edit-app-settings.spec.js | 92 ++++++++++++++++++++++++++++++ test/e2e/hooks.js | 2 + test/e2e/placeholder.spec.js | 7 --- 4 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 test/e2e/edit-app-settings.spec.js delete mode 100644 test/e2e/placeholder.spec.js diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 8d4bd9c8..7ffc604d 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -2,6 +2,7 @@ const path = require('path'); const fs = require('fs'); const https = require('https'); const { spawn } = require('child_process'); +const request = require('request-promise-native'); const projectName = 'cht_conf_e2e_tests'; const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); @@ -21,6 +22,39 @@ const downloadDockerHelperScript = () => new Promise((resolve, reject) => { }); }); +const ensureScriptExists = async () => { + if (!fs.existsSync(dockerHelperDirectory)) { + await fs.promises.mkdir(dockerHelperDirectory); + } + + if (!fs.existsSync(dockerHelperScript)) { + await downloadDockerHelperScript(); + } +}; + +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +const isProjectReady = async (attempt = 1) => { + console.log(`Checking if CHT is ready, attempt ${attempt}.`); + const COUCHDB_USER = 'medic'; + const COUCHDB_PASSWORD = 'password'; + const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; + await request({ uri: `${url}/api/v2/monitoring`, json: true }) + .catch(async (error) => { + if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { + await sleep(1000); + return isProjectReady(attempt + 1); + } + + if ([502, 503].includes(error.statusCode)) { + await sleep(1000); + return isProjectReady(attempt + 1); + } + + throw error; + }); +}; + const startProject = () => new Promise((resolve, reject) => { const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); if (fs.existsSync(configFile)) { @@ -32,30 +66,34 @@ const startProject = () => new Promise((resolve, reject) => { // initialize a new project, config will be saved to `${projectName}.env` const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); childProcess.on('error', reject); - childProcess.on('close', resolve); + childProcess.on('close', async () => { + await isProjectReady(); + resolve(); + }); childProcess.stdin.write('y\n'); childProcess.stdin.write('y\n'); childProcess.stdin.write(`${projectName}\n`); } }); -const spinUpCht = async () => { - if (!fs.existsSync(dockerHelperDirectory)) { - await fs.promises.mkdir(dockerHelperDirectory); - } - - if (!fs.existsSync(dockerHelperScript)) { - await downloadDockerHelperScript(); - } +const destroyProject = () => new Promise((resolve, reject) => { + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { + stdio: 'inherit', + cwd: dockerHelperDirectory + }); + childProcess.on('error', reject); + childProcess.on('close', resolve); +}); +const spinUpCht = async () => { + await ensureScriptExists(); await startProject(); }; -const tearDownCht = () => new Promise((resolve, reject) => { - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); -}); +const tearDownCht = async () => { + await ensureScriptExists(); + await destroyProject(); +}; module.exports = { spinUpCht, diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js new file mode 100644 index 00000000..e7d43674 --- /dev/null +++ b/test/e2e/edit-app-settings.spec.js @@ -0,0 +1,92 @@ +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { expect } = require('chai'); +const fse = require('fs-extra'); +const request = require('request-promise-native'); + +const COUCHDB_USER = 'medic'; +const COUCHDB_PASSWORD = 'password'; +const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; +const projectDirectory = path.resolve(__dirname, '../../build/e2e-edit-app-settings'); + +const runChtConf = (command) => new Promise((resolve, reject) => { + const cliPath = path.join(__dirname, '../../src/bin/index.js'); + exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout, stderr) => { + if (!error) { + return resolve(stdout); + } + + console.error('error', error); + console.error('stdout', stdout); + console.error('stderr', stderr); + reject(new Error(stdout.toString())); + }); +}); + +describe('edit-app-settings', () => { + before(async () => { + if (fs.existsSync(projectDirectory)) { + fse.removeSync(projectDirectory); + } + + fs.mkdirSync(projectDirectory); + fs.writeFileSync( + path.join(projectDirectory, 'package.json'), + JSON.stringify({ + name: 'e2e-edit-app-settings', + version: '1.0.0', + dependencies: { + 'cht-conf': 'file:../..', + }, + }, null, 4), + ); + + await runChtConf('initialise-project-layout'); + }); + + after(async () => { + // fse.removeSync(projectDirectory); + }); + + it('checks if the mocha test setup works', async () => { + const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); + + // eslint-disable-next-line no-undef + const baseSettings = structuredClone(initialSettings); // TODO: upgrade eslint to accept syntax supported by node 18+ + baseSettings.languages = baseSettings.languages.map(language => { + if (language.locale === 'en') { + language.enabled = false; + } + + return language; + }); + baseSettings.locale = 'fr'; + baseSettings.locale_outgoing = 'fr'; + await fs.promises.writeFile( + path.join(projectDirectory, 'app_settings/base_settings.json'), + JSON.stringify(baseSettings, null, 2), + ); + + await runChtConf('compile-app-settings'); + const compiledSettings = JSON.parse( + await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json')) + ); + expect(compiledSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ + locale: 'en', + enabled: false, + }); + expect(compiledSettings.locale).to.equal('fr'); + expect(compiledSettings.locale_outgoing).to.equal('fr'); + + await runChtConf('upload-app-settings'); + const newSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); + expect(newSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ + locale: 'en', + enabled: false, + }); + expect(newSettings.locale).to.equal('fr'); + expect(newSettings.locale_outgoing).to.equal('fr'); + + }); +}); diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index f57783da..017d374b 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -1,6 +1,8 @@ const { spinUpCht, tearDownCht } = require('./cht-docker-utils'); before(async () => { + // cleanup eventual leftovers before starting + await tearDownCht(); await spinUpCht(); }); diff --git a/test/e2e/placeholder.spec.js b/test/e2e/placeholder.spec.js deleted file mode 100644 index cb892267..00000000 --- a/test/e2e/placeholder.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -const assert = require('assert'); - -describe('placeholder', () => { - it('checks if the mocha test setup works', () => { - assert.strictEqual(1, 1); - }); -}); From 6c3d8f1d84363655327e47401fcd5bec2a431a49 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 12:52:46 +0200 Subject: [PATCH 14/47] trace errors in CI --- test/e2e/cht-docker-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 7ffc604d..ac75f7c4 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -41,6 +41,7 @@ const isProjectReady = async (attempt = 1) => { const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; await request({ uri: `${url}/api/v2/monitoring`, json: true }) .catch(async (error) => { + console.error(error); if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { await sleep(1000); return isProjectReady(attempt + 1); From cf64ad30d086d1291c7bbafdc06da98f31d37dae Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 14:23:43 +0200 Subject: [PATCH 15/47] check for existence of project configuration before running teardown --- test/e2e/cht-docker-utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index ac75f7c4..a9431cad 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -92,6 +92,10 @@ const spinUpCht = async () => { }; const tearDownCht = async () => { + if (!fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) { + return; + } + await ensureScriptExists(); await destroyProject(); }; From fb12834484f4543b8ede485676413c997d593fe1 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 14:24:00 +0200 Subject: [PATCH 16/47] add more traces --- test/e2e/cht-docker-utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index a9431cad..b4bede94 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -71,6 +71,10 @@ const startProject = () => new Promise((resolve, reject) => { await isProjectReady(); resolve(); }); + + childProcess.stdout.on('data', data => console.log('out', data.toString())); + childProcess.stderr.on('data', data => console.log('err', data.toString())); + childProcess.stdin.write('y\n'); childProcess.stdin.write('y\n'); childProcess.stdin.write(`${projectName}\n`); From 64887d40f95f3b5cfebf8075d045325eed322a6a Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 14:29:32 +0200 Subject: [PATCH 17/47] do we have the latest version of the script? --- test/e2e/cht-docker-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index b4bede94..06ac9346 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -28,8 +28,10 @@ const ensureScriptExists = async () => { } if (!fs.existsSync(dockerHelperScript)) { + console.log('downloading script'); await downloadDockerHelperScript(); } + console.log(fs.readFileSync(dockerHelperScript).toString()); }; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); From 800913032287b681140ba6ada366ec54e54325a7 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 15:01:38 +0200 Subject: [PATCH 18/47] what's wrong with `docker exec`? --- test/e2e/cht-docker-utils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 06ac9346..5bc002d6 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -11,7 +11,7 @@ const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-com const downloadDockerHelperScript = () => new Promise((resolve, reject) => { const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); https - .get('https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { + .get('https://raw.githubusercontent.com/medic/cht-core/dnm-docker-helper-experiments/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { response.pipe(file); file.on('finish', () => file.close(resolve)); file.on('error', () => file.close(reject)); @@ -28,10 +28,9 @@ const ensureScriptExists = async () => { } if (!fs.existsSync(dockerHelperScript)) { - console.log('downloading script'); await downloadDockerHelperScript(); + console.log(fs.readFileSync(dockerHelperScript).toString()); } - console.log(fs.readFileSync(dockerHelperScript).toString()); }; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); From 1b26d681d7e0b7e76cca6369739c524052436f8f Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 16:29:37 +0200 Subject: [PATCH 19/47] create make parent directories as needed same as `mkdir -p` --- test/e2e/edit-app-settings.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index e7d43674..2e0908f6 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -30,7 +30,7 @@ describe('edit-app-settings', () => { fse.removeSync(projectDirectory); } - fs.mkdirSync(projectDirectory); + fse.mkdirpSync(projectDirectory); fs.writeFileSync( path.join(projectDirectory, 'package.json'), JSON.stringify({ From d85caa17fe2bcbf1ed061230366d6118bc741be6 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 16:43:11 +0200 Subject: [PATCH 20/47] organize todos, remove debugging logs --- test/e2e/cht-docker-utils.js | 6 +----- test/e2e/edit-app-settings.spec.js | 11 +++++++---- test/e2e/hooks.js | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 5bc002d6..cd2192a5 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -29,7 +29,6 @@ const ensureScriptExists = async () => { if (!fs.existsSync(dockerHelperScript)) { await downloadDockerHelperScript(); - console.log(fs.readFileSync(dockerHelperScript).toString()); } }; @@ -37,12 +36,12 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const isProjectReady = async (attempt = 1) => { console.log(`Checking if CHT is ready, attempt ${attempt}.`); + // TODO: read these 3 settings from the project.env file instead of hardcoding them const COUCHDB_USER = 'medic'; const COUCHDB_PASSWORD = 'password'; const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; await request({ uri: `${url}/api/v2/monitoring`, json: true }) .catch(async (error) => { - console.error(error); if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { await sleep(1000); return isProjectReady(attempt + 1); @@ -73,9 +72,6 @@ const startProject = () => new Promise((resolve, reject) => { resolve(); }); - childProcess.stdout.on('data', data => console.log('out', data.toString())); - childProcess.stderr.on('data', data => console.log('err', data.toString())); - childProcess.stdin.write('y\n'); childProcess.stdin.write('y\n'); childProcess.stdin.write(`${projectName}\n`); diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 2e0908f6..5b53c5e0 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -5,11 +5,13 @@ const { expect } = require('chai'); const fse = require('fs-extra'); const request = require('request-promise-native'); +// TODO: read these 3 settings from the project.env file instead of hardcoding them const COUCHDB_USER = 'medic'; const COUCHDB_PASSWORD = 'password'; const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; const projectDirectory = path.resolve(__dirname, '../../build/e2e-edit-app-settings'); +// TODO: extract this to utils const runChtConf = (command) => new Promise((resolve, reject) => { const cliPath = path.join(__dirname, '../../src/bin/index.js'); exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout, stderr) => { @@ -17,9 +19,10 @@ const runChtConf = (command) => new Promise((resolve, reject) => { return resolve(stdout); } - console.error('error', error); - console.error('stdout', stdout); - console.error('stderr', stderr); + // TODO: these should use the logger, should be trace/error logs + // console.error('error', error); + // console.error('stdout', stdout); + // console.error('stderr', stderr); reject(new Error(stdout.toString())); }); }); @@ -46,7 +49,7 @@ describe('edit-app-settings', () => { }); after(async () => { - // fse.removeSync(projectDirectory); + fse.removeSync(projectDirectory); }); it('checks if the mocha test setup works', async () => { diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 017d374b..93134bcb 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -2,10 +2,10 @@ const { spinUpCht, tearDownCht } = require('./cht-docker-utils'); before(async () => { // cleanup eventual leftovers before starting - await tearDownCht(); + // await tearDownCht(); await spinUpCht(); }); after(async () => { - await tearDownCht(); + // await tearDownCht(); }); From 2707755fc769df5308cb4f8d28d5c7696a4f3d12 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 16:44:17 +0200 Subject: [PATCH 21/47] replace 4 spaces => 2 spaces to follow coding style in the repo --- test/e2e/.mocharc.js | 24 +++--- test/e2e/cht-docker-utils.js | 122 +++++++++++++------------- test/e2e/edit-app-settings.spec.js | 132 ++++++++++++++--------------- test/e2e/hooks.js | 8 +- 4 files changed, 143 insertions(+), 143 deletions(-) diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js index 71ba560f..8e82b4e0 100644 --- a/test/e2e/.mocharc.js +++ b/test/e2e/.mocharc.js @@ -1,14 +1,14 @@ module.exports = { - allowUncaught: false, - color: true, - checkLeaks: true, - fullTrace: true, - asyncOnly: false, - spec: ['test/e2e/**/*.spec.js'], - timeout: 200 * 1000, //API takes a little long to start up - reporter: 'spec', - file: ['test/e2e/hooks.js'], - captureFile: 'test/e2e/results.txt', - exit: true, - recursive: true, + allowUncaught: false, + color: true, + checkLeaks: true, + fullTrace: true, + asyncOnly: false, + spec: ['test/e2e/**/*.spec.js'], + timeout: 200 * 1000, //API takes a little long to start up + reporter: 'spec', + file: ['test/e2e/hooks.js'], + captureFile: 'test/e2e/results.txt', + exit: true, + recursive: true, }; diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index cd2192a5..1abe3d56 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -9,17 +9,17 @@ const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-compose.sh'); const downloadDockerHelperScript = () => new Promise((resolve, reject) => { - const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); - https - .get('https://raw.githubusercontent.com/medic/cht-core/dnm-docker-helper-experiments/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { - response.pipe(file); - file.on('finish', () => file.close(resolve)); - file.on('error', () => file.close(reject)); - }) - .on('error', () => { - fs.unlinkSync(file.path); - file.close(() => reject('Failed to download CHT Docker Helper script "cht-docker-compose.sh"')); - }); + const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); + https + .get('https://raw.githubusercontent.com/medic/cht-core/dnm-docker-helper-experiments/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { + response.pipe(file); + file.on('finish', () => file.close(resolve)); + file.on('error', () => file.close(reject)); + }) + .on('error', () => { + fs.unlinkSync(file.path); + file.close(() => reject('Failed to download CHT Docker Helper script "cht-docker-compose.sh"')); + }); }); const ensureScriptExists = async () => { @@ -35,73 +35,73 @@ const ensureScriptExists = async () => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const isProjectReady = async (attempt = 1) => { - console.log(`Checking if CHT is ready, attempt ${attempt}.`); - // TODO: read these 3 settings from the project.env file instead of hardcoding them - const COUCHDB_USER = 'medic'; - const COUCHDB_PASSWORD = 'password'; - const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; - await request({ uri: `${url}/api/v2/monitoring`, json: true }) - .catch(async (error) => { - if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { - await sleep(1000); - return isProjectReady(attempt + 1); - } + console.log(`Checking if CHT is ready, attempt ${attempt}.`); + // TODO: read these 3 settings from the project.env file instead of hardcoding them + const COUCHDB_USER = 'medic'; + const COUCHDB_PASSWORD = 'password'; + const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; + await request({ uri: `${url}/api/v2/monitoring`, json: true }) + .catch(async (error) => { + if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { + await sleep(1000); + return isProjectReady(attempt + 1); + } - if ([502, 503].includes(error.statusCode)) { - await sleep(1000); - return isProjectReady(attempt + 1); - } + if ([502, 503].includes(error.statusCode)) { + await sleep(1000); + return isProjectReady(attempt + 1); + } - throw error; - }); + throw error; + }); }; const startProject = () => new Promise((resolve, reject) => { - const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); - if (fs.existsSync(configFile)) { - // project config already exists, reuse it - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); - } else { - // initialize a new project, config will be saved to `${projectName}.env` - const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', async () => { - await isProjectReady(); - resolve(); - }); + const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); + if (fs.existsSync(configFile)) { + // project config already exists, reuse it + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', resolve); + } else { + // initialize a new project, config will be saved to `${projectName}.env` + const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', async () => { + await isProjectReady(); + resolve(); + }); - childProcess.stdin.write('y\n'); - childProcess.stdin.write('y\n'); - childProcess.stdin.write(`${projectName}\n`); - } + childProcess.stdin.write('y\n'); + childProcess.stdin.write('y\n'); + childProcess.stdin.write(`${projectName}\n`); + } }); const destroyProject = () => new Promise((resolve, reject) => { - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { - stdio: 'inherit', - cwd: dockerHelperDirectory - }); - childProcess.on('error', reject); - childProcess.on('close', resolve); + const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { + stdio: 'inherit', + cwd: dockerHelperDirectory + }); + childProcess.on('error', reject); + childProcess.on('close', resolve); }); const spinUpCht = async () => { - await ensureScriptExists(); - await startProject(); + await ensureScriptExists(); + await startProject(); }; const tearDownCht = async () => { - if (!fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) { - return; - } + if (!fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) { + return; + } - await ensureScriptExists(); - await destroyProject(); + await ensureScriptExists(); + await destroyProject(); }; module.exports = { - spinUpCht, - tearDownCht, + spinUpCht, + tearDownCht, }; diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 5b53c5e0..149b81a4 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -13,83 +13,83 @@ const projectDirectory = path.resolve(__dirname, '../../build/e2e-edit-app-setti // TODO: extract this to utils const runChtConf = (command) => new Promise((resolve, reject) => { - const cliPath = path.join(__dirname, '../../src/bin/index.js'); - exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout, stderr) => { - if (!error) { - return resolve(stdout); - } + const cliPath = path.join(__dirname, '../../src/bin/index.js'); + exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout, stderr) => { + if (!error) { + return resolve(stdout); + } - // TODO: these should use the logger, should be trace/error logs - // console.error('error', error); - // console.error('stdout', stdout); - // console.error('stderr', stderr); - reject(new Error(stdout.toString())); - }); + // TODO: these should use the logger, should be trace/error logs + // console.error('error', error); + // console.error('stdout', stdout); + // console.error('stderr', stderr); + reject(new Error(stdout.toString())); + }); }); describe('edit-app-settings', () => { - before(async () => { - if (fs.existsSync(projectDirectory)) { - fse.removeSync(projectDirectory); - } - - fse.mkdirpSync(projectDirectory); - fs.writeFileSync( - path.join(projectDirectory, 'package.json'), - JSON.stringify({ - name: 'e2e-edit-app-settings', - version: '1.0.0', - dependencies: { - 'cht-conf': 'file:../..', - }, - }, null, 4), - ); + before(async () => { + if (fs.existsSync(projectDirectory)) { + fse.removeSync(projectDirectory); + } - await runChtConf('initialise-project-layout'); - }); + fse.mkdirpSync(projectDirectory); + fs.writeFileSync( + path.join(projectDirectory, 'package.json'), + JSON.stringify({ + name: 'e2e-edit-app-settings', + version: '1.0.0', + dependencies: { + 'cht-conf': 'file:../..', + }, + }, null, 4), + ); - after(async () => { - fse.removeSync(projectDirectory); - }); + await runChtConf('initialise-project-layout'); + }); - it('checks if the mocha test setup works', async () => { - const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); + after(async () => { + fse.removeSync(projectDirectory); + }); - // eslint-disable-next-line no-undef - const baseSettings = structuredClone(initialSettings); // TODO: upgrade eslint to accept syntax supported by node 18+ - baseSettings.languages = baseSettings.languages.map(language => { - if (language.locale === 'en') { - language.enabled = false; - } + it('checks if the mocha test setup works', async () => { + const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); - return language; - }); - baseSettings.locale = 'fr'; - baseSettings.locale_outgoing = 'fr'; - await fs.promises.writeFile( - path.join(projectDirectory, 'app_settings/base_settings.json'), - JSON.stringify(baseSettings, null, 2), - ); + // eslint-disable-next-line no-undef + const baseSettings = structuredClone(initialSettings); // TODO: upgrade eslint to accept syntax supported by node 18+ + baseSettings.languages = baseSettings.languages.map(language => { + if (language.locale === 'en') { + language.enabled = false; + } - await runChtConf('compile-app-settings'); - const compiledSettings = JSON.parse( - await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json')) - ); - expect(compiledSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ - locale: 'en', - enabled: false, - }); - expect(compiledSettings.locale).to.equal('fr'); - expect(compiledSettings.locale_outgoing).to.equal('fr'); + return language; + }); + baseSettings.locale = 'fr'; + baseSettings.locale_outgoing = 'fr'; + await fs.promises.writeFile( + path.join(projectDirectory, 'app_settings/base_settings.json'), + JSON.stringify(baseSettings, null, 2), + ); - await runChtConf('upload-app-settings'); - const newSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); - expect(newSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ - locale: 'en', - enabled: false, - }); - expect(newSettings.locale).to.equal('fr'); - expect(newSettings.locale_outgoing).to.equal('fr'); + await runChtConf('compile-app-settings'); + const compiledSettings = JSON.parse( + await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json')) + ); + expect(compiledSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ + locale: 'en', + enabled: false, + }); + expect(compiledSettings.locale).to.equal('fr'); + expect(compiledSettings.locale_outgoing).to.equal('fr'); + await runChtConf('upload-app-settings'); + const newSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); + expect(newSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ + locale: 'en', + enabled: false, }); + expect(newSettings.locale).to.equal('fr'); + expect(newSettings.locale_outgoing).to.equal('fr'); + + }); }); diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 93134bcb..9baa89f5 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -1,11 +1,11 @@ const { spinUpCht, tearDownCht } = require('./cht-docker-utils'); before(async () => { - // cleanup eventual leftovers before starting - // await tearDownCht(); - await spinUpCht(); + // cleanup eventual leftovers before starting + // await tearDownCht(); + await spinUpCht(); }); after(async () => { - // await tearDownCht(); + // await tearDownCht(); }); From d24846b38f1a4b7dbd13839ce7b5c05c11d975ab Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 16:46:30 +0200 Subject: [PATCH 22/47] oops forgot these --- test/e2e/edit-app-settings.spec.js | 2 +- test/e2e/hooks.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 149b81a4..400dcadf 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -14,7 +14,7 @@ const projectDirectory = path.resolve(__dirname, '../../build/e2e-edit-app-setti // TODO: extract this to utils const runChtConf = (command) => new Promise((resolve, reject) => { const cliPath = path.join(__dirname, '../../src/bin/index.js'); - exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout, stderr) => { + exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout) => { if (!error) { return resolve(stdout); } diff --git a/test/e2e/hooks.js b/test/e2e/hooks.js index 9baa89f5..55486ce7 100644 --- a/test/e2e/hooks.js +++ b/test/e2e/hooks.js @@ -2,10 +2,10 @@ const { spinUpCht, tearDownCht } = require('./cht-docker-utils'); before(async () => { // cleanup eventual leftovers before starting - // await tearDownCht(); + await tearDownCht(); await spinUpCht(); }); after(async () => { - // await tearDownCht(); + await tearDownCht(); }); From e2757eb556f3b944afff437992dcf71fcb9c7a36 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 19:08:56 +0200 Subject: [PATCH 23/47] extract utils functions to reuse in other tests --- test/e2e/cht-conf-utils.js | 59 ++++++++++++++++++++++++++++++ test/e2e/cht-docker-utils.js | 53 +++++++++++++++++++-------- test/e2e/edit-app-settings.spec.js | 49 ++++--------------------- 3 files changed, 105 insertions(+), 56 deletions(-) create mode 100644 test/e2e/cht-conf-utils.js diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js new file mode 100644 index 00000000..7c45bc2e --- /dev/null +++ b/test/e2e/cht-conf-utils.js @@ -0,0 +1,59 @@ +const path = require('path'); +const { exec } = require('child_process'); +const fs = require('fs'); +const fse = require('fs-extra'); + +const { getProjectUrl } = require('./cht-docker-utils'); + +const getProjectDirectory = (projectName) => path.resolve(__dirname, `../../build/${projectName}`); + +const runChtConf = (projectName, command) => new Promise((resolve, reject) => { + getProjectUrl(projectName).then(url => { + const projectDirectory = getProjectDirectory(projectName); + const cliPath = path.join(__dirname, '../../src/bin/index.js'); + exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout) => { + if (!error) { + return resolve(stdout); + } + + // TODO: these should use the logger, should be trace/error logs + // console.error('error', error); + // console.error('stdout', stdout); + // console.error('stderr', stderr); + reject(new Error(stdout.toString())); + }); + }); +}); + +const cleanupProject = (projectName) => { + const projectDirectory = getProjectDirectory(projectName); + fse.removeSync(projectDirectory); +}; + +const initProject = async (projectName) => { + const projectDirectory = getProjectDirectory(projectName); + if (fs.existsSync(projectDirectory)) { + fse.removeSync(projectDirectory); + } + + fse.mkdirpSync(projectDirectory); + fs.writeFileSync( + path.join(projectDirectory, 'package.json'), + JSON.stringify({ + name: 'e2e-edit-app-settings', + version: '1.0.0', + dependencies: { + 'cht-conf': 'file:../..', + }, + }, null, 4), + ); + + await runChtConf('initialise-project-layout'); +}; + +module.exports = { + cleanupProject, + getProjectDirectory, + initProject, + runChtConf, +}; diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 1abe3d56..e9854590 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -4,13 +4,14 @@ const https = require('https'); const { spawn } = require('child_process'); const request = require('request-promise-native'); -const projectName = 'cht_conf_e2e_tests'; +const DEFAULT_PROJECT_NAME = 'cht_conf_e2e_tests'; const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-compose.sh'); const downloadDockerHelperScript = () => new Promise((resolve, reject) => { const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); https + // TODO: switch back to using `master` branch of cht-core once DNS issue is resolved - https://github.com/medic/medic-infrastructure/issues/571#issuecomment-2209120441 .get('https://raw.githubusercontent.com/medic/cht-core/dnm-docker-helper-experiments/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { response.pipe(file); file.on('finish', () => file.close(resolve)); @@ -34,29 +35,50 @@ const ensureScriptExists = async () => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); -const isProjectReady = async (attempt = 1) => { +const getProjectConfig = async (projectName) => { + try { + const configFile = await fs.promises.readFile(path.resolve(dockerHelperDirectory, `${projectName}.env`)); + return Object.fromEntries( + configFile.toString() + .split('\n') + .map(line => line.split('=')) + .filter(entry => entry.length === 2), + ); + } catch (error) { + return { + COUCHDB_USER: 'medic', + COUCHDB_PASSWORD: 'password', + NGINX_HTTPS_PORT: '10443', + }; + } +}; + +const getProjectUrl = async (projectName = DEFAULT_PROJECT_NAME) => { + const config = await getProjectConfig(projectName); + const { COUCHDB_USER, COUCHDB_PASSWORD, NGINX_HTTPS_PORT } = config; + return `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:${NGINX_HTTPS_PORT}`; +}; + +const isProjectReady = async (projectName, attempt = 1) => { console.log(`Checking if CHT is ready, attempt ${attempt}.`); - // TODO: read these 3 settings from the project.env file instead of hardcoding them - const COUCHDB_USER = 'medic'; - const COUCHDB_PASSWORD = 'password'; - const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; + const url = await getProjectUrl(projectName); await request({ uri: `${url}/api/v2/monitoring`, json: true }) .catch(async (error) => { if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { await sleep(1000); - return isProjectReady(attempt + 1); + return isProjectReady(projectName, attempt + 1); } if ([502, 503].includes(error.statusCode)) { await sleep(1000); - return isProjectReady(attempt + 1); + return isProjectReady(projectName, attempt + 1); } throw error; }); }; -const startProject = () => new Promise((resolve, reject) => { +const startProject = (projectName) => new Promise((resolve, reject) => { const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); if (fs.existsSync(configFile)) { // project config already exists, reuse it @@ -68,7 +90,7 @@ const startProject = () => new Promise((resolve, reject) => { const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); childProcess.on('error', reject); childProcess.on('close', async () => { - await isProjectReady(); + await isProjectReady(projectName); resolve(); }); @@ -78,7 +100,7 @@ const startProject = () => new Promise((resolve, reject) => { } }); -const destroyProject = () => new Promise((resolve, reject) => { +const destroyProject = (projectName) => new Promise((resolve, reject) => { const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { stdio: 'inherit', cwd: dockerHelperDirectory @@ -87,21 +109,22 @@ const destroyProject = () => new Promise((resolve, reject) => { childProcess.on('close', resolve); }); -const spinUpCht = async () => { +const spinUpCht = async (projectName = DEFAULT_PROJECT_NAME) => { await ensureScriptExists(); - await startProject(); + await startProject(projectName); }; -const tearDownCht = async () => { +const tearDownCht = async (projectName = DEFAULT_PROJECT_NAME) => { if (!fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) { return; } await ensureScriptExists(); - await destroyProject(); + await destroyProject(projectName); }; module.exports = { + getProjectUrl, spinUpCht, tearDownCht, }; diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 400dcadf..5df7d18e 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -1,58 +1,25 @@ const fs = require('fs'); const path = require('path'); -const { exec } = require('child_process'); const { expect } = require('chai'); -const fse = require('fs-extra'); const request = require('request-promise-native'); -// TODO: read these 3 settings from the project.env file instead of hardcoding them -const COUCHDB_USER = 'medic'; -const COUCHDB_PASSWORD = 'password'; -const url = `https://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127-0-0-1.local-ip.medicmobile.org:10443`; -const projectDirectory = path.resolve(__dirname, '../../build/e2e-edit-app-settings'); - -// TODO: extract this to utils -const runChtConf = (command) => new Promise((resolve, reject) => { - const cliPath = path.join(__dirname, '../../src/bin/index.js'); - exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout) => { - if (!error) { - return resolve(stdout); - } - - // TODO: these should use the logger, should be trace/error logs - // console.error('error', error); - // console.error('stdout', stdout); - // console.error('stderr', stderr); - reject(new Error(stdout.toString())); - }); -}); +const { cleanupProject, getProjectDirectory, initProject, runChtConf } = require('./cht-conf-utils'); +const { getProjectUrl } = require('./cht-docker-utils'); describe('edit-app-settings', () => { - before(async () => { - if (fs.existsSync(projectDirectory)) { - fse.removeSync(projectDirectory); - } - - fse.mkdirpSync(projectDirectory); - fs.writeFileSync( - path.join(projectDirectory, 'package.json'), - JSON.stringify({ - name: 'e2e-edit-app-settings', - version: '1.0.0', - dependencies: { - 'cht-conf': 'file:../..', - }, - }, null, 4), - ); + const projectName = 'e2e-edit-app-settings'; + const projectDirectory = getProjectDirectory(projectName); - await runChtConf('initialise-project-layout'); + before(async () => { + await initProject(projectName); }); after(async () => { - fse.removeSync(projectDirectory); + await cleanupProject(projectName); }); it('checks if the mocha test setup works', async () => { + const url = getProjectUrl(); const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); // eslint-disable-next-line no-undef From 186791253ce706ab26bd602a19a7d704651c460d Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 19:14:17 +0200 Subject: [PATCH 24/47] logs --- test/e2e/cht-conf-utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js index 7c45bc2e..99aa219e 100644 --- a/test/e2e/cht-conf-utils.js +++ b/test/e2e/cht-conf-utils.js @@ -11,15 +11,15 @@ const runChtConf = (projectName, command) => new Promise((resolve, reject) => { getProjectUrl(projectName).then(url => { const projectDirectory = getProjectDirectory(projectName); const cliPath = path.join(__dirname, '../../src/bin/index.js'); - exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout) => { + exec(`node ${cliPath} --url=${url} ${command}`, { cwd: projectDirectory }, (error, stdout, stderr) => { if (!error) { return resolve(stdout); } // TODO: these should use the logger, should be trace/error logs - // console.error('error', error); - // console.error('stdout', stdout); - // console.error('stderr', stderr); + console.error('error', error); + console.error('stdout', stdout); + console.error('stderr', stderr); reject(new Error(stdout.toString())); }); }); From 2d7104331ba80bcec60f111e0ecb8e2b7412b353 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 19:18:20 +0200 Subject: [PATCH 25/47] pass project name to `runChtConf` as expected --- test/e2e/cht-conf-utils.js | 2 +- test/e2e/edit-app-settings.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js index 99aa219e..3637f776 100644 --- a/test/e2e/cht-conf-utils.js +++ b/test/e2e/cht-conf-utils.js @@ -48,7 +48,7 @@ const initProject = async (projectName) => { }, null, 4), ); - await runChtConf('initialise-project-layout'); + await runChtConf(projectName, 'initialise-project-layout'); }; module.exports = { diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 5df7d18e..fc632f4b 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -38,7 +38,7 @@ describe('edit-app-settings', () => { JSON.stringify(baseSettings, null, 2), ); - await runChtConf('compile-app-settings'); + await runChtConf(projectName, 'compile-app-settings'); const compiledSettings = JSON.parse( await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json')) ); @@ -49,7 +49,7 @@ describe('edit-app-settings', () => { expect(compiledSettings.locale).to.equal('fr'); expect(compiledSettings.locale_outgoing).to.equal('fr'); - await runChtConf('upload-app-settings'); + await runChtConf(projectName, 'upload-app-settings'); const newSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); expect(newSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ locale: 'en', From 4fcac9546cf348a39750aade8615d25f553a26e8 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 19:25:48 +0200 Subject: [PATCH 26/47] await getProjectUrl() --- test/e2e/edit-app-settings.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index fc632f4b..00d9b9f1 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -19,7 +19,7 @@ describe('edit-app-settings', () => { }); it('checks if the mocha test setup works', async () => { - const url = getProjectUrl(); + const url = await getProjectUrl(); const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); // eslint-disable-next-line no-undef From 626bcc53e64ecead13222a8a17b1c4f9435d943d Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 4 Jul 2024 19:30:05 +0200 Subject: [PATCH 27/47] sonar :) --- test/e2e/cht-docker-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index e9854590..4e2b53e6 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -45,6 +45,7 @@ const getProjectConfig = async (projectName) => { .filter(entry => entry.length === 2), ); } catch (error) { + console.error(error); return { COUCHDB_USER: 'medic', COUCHDB_PASSWORD: 'password', From c065b78edd1f29ccf9344d5daf5774b53d652dde Mon Sep 17 00:00:00 2001 From: m5r Date: Tue, 9 Jul 2024 11:46:44 +0200 Subject: [PATCH 28/47] clean up --- test/e2e/cht-conf-utils.js | 8 +++----- test/e2e/cht-docker-utils.js | 2 +- test/e2e/edit-app-settings.spec.js | 5 +++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js index 3637f776..302fcf5c 100644 --- a/test/e2e/cht-conf-utils.js +++ b/test/e2e/cht-conf-utils.js @@ -3,6 +3,7 @@ const { exec } = require('child_process'); const fs = require('fs'); const fse = require('fs-extra'); +const log = require('../../src/lib/log'); const { getProjectUrl } = require('./cht-docker-utils'); const getProjectDirectory = (projectName) => path.resolve(__dirname, `../../build/${projectName}`); @@ -16,11 +17,8 @@ const runChtConf = (projectName, command) => new Promise((resolve, reject) => { return resolve(stdout); } - // TODO: these should use the logger, should be trace/error logs - console.error('error', error); - console.error('stdout', stdout); - console.error('stderr', stderr); - reject(new Error(stdout.toString())); + log.error(stderr); + reject(new Error(stdout.toString('utf8'))); }); }); }); diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 4e2b53e6..a7ba8f7a 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -37,7 +37,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const getProjectConfig = async (projectName) => { try { - const configFile = await fs.promises.readFile(path.resolve(dockerHelperDirectory, `${projectName}.env`)); + const configFile = await fs.promises.readFile(path.resolve(dockerHelperDirectory, `${projectName}.env`), 'utf8'); return Object.fromEntries( configFile.toString() .split('\n') diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 00d9b9f1..b34b3834 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -22,8 +22,9 @@ describe('edit-app-settings', () => { const url = await getProjectUrl(); const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); + // TODO: remove next line when we upgrade eslint and its `parserOptions.ecmaVersion` setting to parse syntax supported by node 18+ // eslint-disable-next-line no-undef - const baseSettings = structuredClone(initialSettings); // TODO: upgrade eslint to accept syntax supported by node 18+ + const baseSettings = structuredClone(initialSettings); baseSettings.languages = baseSettings.languages.map(language => { if (language.locale === 'en') { language.enabled = false; @@ -40,7 +41,7 @@ describe('edit-app-settings', () => { await runChtConf(projectName, 'compile-app-settings'); const compiledSettings = JSON.parse( - await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json')) + await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json'), 'utf8') ); expect(compiledSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ locale: 'en', From c22e98e630200e6bffb363eccb24bf31c64e18fb Mon Sep 17 00:00:00 2001 From: m5r Date: Tue, 9 Jul 2024 11:53:44 +0200 Subject: [PATCH 29/47] more clean up --- test/e2e/.mocharc.js | 2 +- test/e2e/cht-docker-utils.js | 7 +++++-- test/e2e/edit-app-settings.spec.js | 10 ++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js index 8e82b4e0..9d509352 100644 --- a/test/e2e/.mocharc.js +++ b/test/e2e/.mocharc.js @@ -5,7 +5,7 @@ module.exports = { fullTrace: true, asyncOnly: false, spec: ['test/e2e/**/*.spec.js'], - timeout: 200 * 1000, //API takes a little long to start up + timeout: 60_000, // API takes a little long to start up reporter: 'spec', file: ['test/e2e/hooks.js'], captureFile: 'test/e2e/results.txt', diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index a7ba8f7a..c2c2dac3 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -4,6 +4,8 @@ const https = require('https'); const { spawn } = require('child_process'); const request = require('request-promise-native'); +const log = require('../../src/lib/log'); + const DEFAULT_PROJECT_NAME = 'cht_conf_e2e_tests'; const dockerHelperDirectory = path.resolve(__dirname, '.cht-docker-helper'); const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-compose.sh'); @@ -45,7 +47,7 @@ const getProjectConfig = async (projectName) => { .filter(entry => entry.length === 2), ); } catch (error) { - console.error(error); + log.error(error); return { COUCHDB_USER: 'medic', COUCHDB_PASSWORD: 'password', @@ -61,7 +63,7 @@ const getProjectUrl = async (projectName = DEFAULT_PROJECT_NAME) => { }; const isProjectReady = async (projectName, attempt = 1) => { - console.log(`Checking if CHT is ready, attempt ${attempt}.`); + log.info(`Checking if CHT is ready, attempt ${attempt}.`); const url = await getProjectUrl(projectName); await request({ uri: `${url}/api/v2/monitoring`, json: true }) .catch(async (error) => { @@ -125,6 +127,7 @@ const tearDownCht = async (projectName = DEFAULT_PROJECT_NAME) => { }; module.exports = { + DEFAULT_PROJECT_NAME, getProjectUrl, spinUpCht, tearDownCht, diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index b34b3834..94bf7c28 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -3,11 +3,17 @@ const path = require('path'); const { expect } = require('chai'); const request = require('request-promise-native'); -const { cleanupProject, getProjectDirectory, initProject, runChtConf } = require('./cht-conf-utils'); const { getProjectUrl } = require('./cht-docker-utils'); +const { + DEFAULT_PROJECT_NAME, + cleanupProject, + getProjectDirectory, + initProject, + runChtConf, +} = require('./cht-conf-utils'); describe('edit-app-settings', () => { - const projectName = 'e2e-edit-app-settings'; + const projectName = DEFAULT_PROJECT_NAME; const projectDirectory = getProjectDirectory(projectName); before(async () => { From e36c2e2a1a9baf1b8dec12cd614cbc309c6d4215 Mon Sep 17 00:00:00 2001 From: m5r Date: Tue, 9 Jul 2024 11:58:01 +0200 Subject: [PATCH 30/47] clearer test title --- test/e2e/edit-app-settings.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 94bf7c28..a58bb871 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -24,8 +24,8 @@ describe('edit-app-settings', () => { await cleanupProject(projectName); }); - it('checks if the mocha test setup works', async () => { - const url = await getProjectUrl(); + it('disables a language, recompile, and push app settings', async () => { + const url = await getProjectUrl(projectName); const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); // TODO: remove next line when we upgrade eslint and its `parserOptions.ecmaVersion` setting to parse syntax supported by node 18+ From 5c78268c6d9cb93d9f60d206d7daef42515e6ef2 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 10 Jul 2024 11:49:13 +0200 Subject: [PATCH 31/47] - switch back to cht-docker-compose.sh from cht-core master - hardcode local-ip IP address in the CI job --- .github/workflows/build.yml | 3 +++ test/e2e/cht-docker-utils.js | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1299af7..984ab61d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,5 +44,8 @@ jobs: run: | pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic npm ci + - name: Hard code local-ip IP in /etc/hosts per https://github.com/medic/medic-infrastructure/issues/571#issuecomment-2209120441 + run: | + echo "15.188.129.97 local-ip.medicmobile.org" | sudo tee -a /etc/hosts - name: Run E2E tests run: npm run test-e2e diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index c2c2dac3..bd804191 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -13,8 +13,7 @@ const dockerHelperScript = path.resolve(dockerHelperDirectory, './cht-docker-com const downloadDockerHelperScript = () => new Promise((resolve, reject) => { const file = fs.createWriteStream(dockerHelperScript, { mode: 0o755 }); https - // TODO: switch back to using `master` branch of cht-core once DNS issue is resolved - https://github.com/medic/medic-infrastructure/issues/571#issuecomment-2209120441 - .get('https://raw.githubusercontent.com/medic/cht-core/dnm-docker-helper-experiments/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { + .get('https://raw.githubusercontent.com/medic/cht-core/master/scripts/docker-helper-4.x/cht-docker-compose.sh', (response) => { response.pipe(file); file.on('finish', () => file.close(resolve)); file.on('error', () => file.close(reject)); From 9b1cfcc0105d4bfbd3c8bd31f7d050d8a78a1107 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 10 Jul 2024 11:52:33 +0200 Subject: [PATCH 32/47] add trace --- test/e2e/cht-docker-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index bd804191..c39d2af3 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -66,6 +66,7 @@ const isProjectReady = async (projectName, attempt = 1) => { const url = await getProjectUrl(projectName); await request({ uri: `${url}/api/v2/monitoring`, json: true }) .catch(async (error) => { + log.error(error); if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { await sleep(1000); return isProjectReady(projectName, attempt + 1); From 9ec0f8602429f95c84883dc510c84b6fdea4346f Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 10 Jul 2024 12:02:15 +0200 Subject: [PATCH 33/47] clean up trace --- test/e2e/cht-docker-utils.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index c39d2af3..0ed98adb 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -66,18 +66,17 @@ const isProjectReady = async (projectName, attempt = 1) => { const url = await getProjectUrl(projectName); await request({ uri: `${url}/api/v2/monitoring`, json: true }) .catch(async (error) => { - log.error(error); - if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { - await sleep(1000); - return isProjectReady(projectName, attempt + 1); + if ( + error.error.code !== 'DEPTH_ZERO_SELF_SIGNED_CERT' || + ![502, 503].includes(error.statusCode) + ) { + // unexpected error, log it to keep a trace, + // but we'll keep retrying until the instance is up, or we hit the timeout limit + log.trace(error); } - if ([502, 503].includes(error.statusCode)) { - await sleep(1000); - return isProjectReady(projectName, attempt + 1); - } - - throw error; + await sleep(1000); + return isProjectReady(projectName, attempt + 1); }); }; From 25155e735c4835a49736d58b86e3dfd8b5a23d58 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 10 Jul 2024 13:31:00 +0200 Subject: [PATCH 34/47] replace hardcoded package.json name --- test/e2e/cht-conf-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js index 302fcf5c..dc9a3a88 100644 --- a/test/e2e/cht-conf-utils.js +++ b/test/e2e/cht-conf-utils.js @@ -38,7 +38,7 @@ const initProject = async (projectName) => { fs.writeFileSync( path.join(projectDirectory, 'package.json'), JSON.stringify({ - name: 'e2e-edit-app-settings', + name: projectName, version: '1.0.0', dependencies: { 'cht-conf': 'file:../..', From b0d8f463707c658aaf146fe28e8a42c9561ff243 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 10 Jul 2024 13:31:54 +0200 Subject: [PATCH 35/47] add comments to explain the rationale behind the `stdio` option when running the docker helper script --- test/e2e/cht-docker-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 0ed98adb..8286fb7e 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -89,6 +89,7 @@ const startProject = (projectName) => new Promise((resolve, reject) => { childProcess.on('close', resolve); } else { // initialize a new project, config will be saved to `${projectName}.env` + // stdio: 'pipe' to answer the prompts to initialize a project by writing to stdin const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); childProcess.on('error', reject); childProcess.on('close', async () => { @@ -103,6 +104,7 @@ const startProject = (projectName) => new Promise((resolve, reject) => { }); const destroyProject = (projectName) => new Promise((resolve, reject) => { + // stdio: 'inherit' to see the script's logs and understand why it requests elevated permissions when cleaning up project files const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { stdio: 'inherit', cwd: dockerHelperDirectory From 9688f80c2e4039526e2648ee85b4a609deec3335 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 17:08:20 +0200 Subject: [PATCH 36/47] remove linting before running e2e tests --- package.json | 2 +- test/e2e/.mocharc.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9a73fc9e..12eea8b6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB", "docker-stop-couchdb": "docker stop cht-conf-couchdb || true", "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" --exclude \"../../test/e2e/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb", - "test-e2e": "npm run eslint && mocha --config test/e2e/.mocharc.js", + "test-e2e": "mocha --config test/e2e/.mocharc.js", "semantic-release": "semantic-release" }, "bin": { diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js index 9d509352..496d8e43 100644 --- a/test/e2e/.mocharc.js +++ b/test/e2e/.mocharc.js @@ -5,7 +5,7 @@ module.exports = { fullTrace: true, asyncOnly: false, spec: ['test/e2e/**/*.spec.js'], - timeout: 60_000, // API takes a little long to start up + timeout: 60_000, // spinning up a CHT instance takes a little long reporter: 'spec', file: ['test/e2e/hooks.js'], captureFile: 'test/e2e/results.txt', From 6899214f57ed8ea88735b0edefc6c824c269b461 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 17:11:29 +0200 Subject: [PATCH 37/47] increase timeout to prevent frequent failures due to CHT instance taking too long to be ready --- test/e2e/.mocharc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/.mocharc.js b/test/e2e/.mocharc.js index 496d8e43..c20016ad 100644 --- a/test/e2e/.mocharc.js +++ b/test/e2e/.mocharc.js @@ -5,7 +5,7 @@ module.exports = { fullTrace: true, asyncOnly: false, spec: ['test/e2e/**/*.spec.js'], - timeout: 60_000, // spinning up a CHT instance takes a little long + timeout: 120_000, // spinning up a CHT instance takes a little long reporter: 'spec', file: ['test/e2e/hooks.js'], captureFile: 'test/e2e/results.txt', From 52d59e07de939ed104f5013b4a290a1072331980 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 17:29:38 +0200 Subject: [PATCH 38/47] remove `.cht-docker-helper` in teardown --- test/e2e/cht-docker-utils.js | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 8286fb7e..9ecbb72e 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -2,6 +2,7 @@ const path = require('path'); const fs = require('fs'); const https = require('https'); const { spawn } = require('child_process'); +const fse = require('fs-extra'); const request = require('request-promise-native'); const log = require('../../src/lib/log'); @@ -81,33 +82,26 @@ const isProjectReady = async (projectName, attempt = 1) => { }; const startProject = (projectName) => new Promise((resolve, reject) => { - const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`); - if (fs.existsSync(configFile)) { - // project config already exists, reuse it - const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'up'], { cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', resolve); - } else { - // initialize a new project, config will be saved to `${projectName}.env` - // stdio: 'pipe' to answer the prompts to initialize a project by writing to stdin - const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); - childProcess.on('error', reject); - childProcess.on('close', async () => { - await isProjectReady(projectName); - resolve(); - }); + log.info(`Starting CHT instance "${projectName}"`); - childProcess.stdin.write('y\n'); - childProcess.stdin.write('y\n'); - childProcess.stdin.write(`${projectName}\n`); - } + // stdio: 'pipe' to answer the prompts to initialize a project by writing to stdin + const childProcess = spawn(dockerHelperScript, { stdio: 'pipe', cwd: dockerHelperDirectory }); + childProcess.on('error', reject); + childProcess.on('close', async () => { + await isProjectReady(projectName); + resolve(); + }); + + childProcess.stdin.write('y\n'); + childProcess.stdin.write('y\n'); + childProcess.stdin.write(`${projectName}\n`); }); const destroyProject = (projectName) => new Promise((resolve, reject) => { // stdio: 'inherit' to see the script's logs and understand why it requests elevated permissions when cleaning up project files const childProcess = spawn(dockerHelperScript, [`${projectName}.env`, 'destroy'], { stdio: 'inherit', - cwd: dockerHelperDirectory + cwd: dockerHelperDirectory, }); childProcess.on('error', reject); childProcess.on('close', resolve); @@ -119,12 +113,16 @@ const spinUpCht = async (projectName = DEFAULT_PROJECT_NAME) => { }; const tearDownCht = async (projectName = DEFAULT_PROJECT_NAME) => { - if (!fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) { + if (!fs.existsSync(dockerHelperDirectory)) { return; } - await ensureScriptExists(); - await destroyProject(projectName); + if (fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) { + await ensureScriptExists(); + await destroyProject(projectName); + } + + fse.removeSync(dockerHelperDirectory); }; module.exports = { From 3f57497d4a52468b66145d872695769a6f48d755 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 17:31:01 +0200 Subject: [PATCH 39/47] fix import of `DEFAULT_PROJECT_NAME` --- test/e2e/edit-app-settings.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index a58bb871..1c60d6ba 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -3,9 +3,8 @@ const path = require('path'); const { expect } = require('chai'); const request = require('request-promise-native'); -const { getProjectUrl } = require('./cht-docker-utils'); +const { DEFAULT_PROJECT_NAME, getProjectUrl } = require('./cht-docker-utils'); const { - DEFAULT_PROJECT_NAME, cleanupProject, getProjectDirectory, initProject, From dcafa3e9552648cdd1ac1c44df4f462618aa6d6c Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 17:33:14 +0200 Subject: [PATCH 40/47] dedup code in `initProject` --- test/e2e/cht-conf-utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js index dc9a3a88..4dc85933 100644 --- a/test/e2e/cht-conf-utils.js +++ b/test/e2e/cht-conf-utils.js @@ -25,14 +25,14 @@ const runChtConf = (projectName, command) => new Promise((resolve, reject) => { const cleanupProject = (projectName) => { const projectDirectory = getProjectDirectory(projectName); - fse.removeSync(projectDirectory); + if (fs.existsSync(projectDirectory)) { + fse.removeSync(projectDirectory); + } }; const initProject = async (projectName) => { const projectDirectory = getProjectDirectory(projectName); - if (fs.existsSync(projectDirectory)) { - fse.removeSync(projectDirectory); - } + cleanupProject(projectName); fse.mkdirpSync(projectDirectory); fs.writeFileSync( From 8cab01661e1c6b195ac97d953835514dfbfa16ba Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 17:44:04 +0200 Subject: [PATCH 41/47] remove unnecessary `structuredClone` --- test/e2e/edit-app-settings.spec.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index 1c60d6ba..d0175f12 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -25,11 +25,7 @@ describe('edit-app-settings', () => { it('disables a language, recompile, and push app settings', async () => { const url = await getProjectUrl(projectName); - const initialSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); - - // TODO: remove next line when we upgrade eslint and its `parserOptions.ecmaVersion` setting to parse syntax supported by node 18+ - // eslint-disable-next-line no-undef - const baseSettings = structuredClone(initialSettings); + const baseSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); baseSettings.languages = baseSettings.languages.map(language => { if (language.locale === 'en') { language.enabled = false; @@ -63,6 +59,5 @@ describe('edit-app-settings', () => { }); expect(newSettings.locale).to.equal('fr'); expect(newSettings.locale_outgoing).to.equal('fr'); - }); }); From 0b2379a875bb7736f5fab5e60ed50df599751b42 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 21:35:42 +0200 Subject: [PATCH 42/47] add assertions about `baseSettings.language` --- test/e2e/edit-app-settings.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index d0175f12..d96ce2b8 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -26,6 +26,10 @@ describe('edit-app-settings', () => { it('disables a language, recompile, and push app settings', async () => { const url = await getProjectUrl(projectName); const baseSettings = await request.get({ url: `${url}/api/v1/settings`, json: true }); + baseSettings.languages.forEach(language => expect(language.enabled).to.be.true); + expect(baseSettings.locale).to.equal('en'); + expect(baseSettings.locale_outgoing).to.equal('en'); + baseSettings.languages = baseSettings.languages.map(language => { if (language.locale === 'en') { language.enabled = false; From e29816fcfce6af9250b9bfdc3ddde9ff2f5e19ba Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 21:38:37 +0200 Subject: [PATCH 43/47] throw error early when config file doesn't exist --- test/e2e/cht-docker-utils.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/test/e2e/cht-docker-utils.js b/test/e2e/cht-docker-utils.js index 9ecbb72e..ea400ae0 100644 --- a/test/e2e/cht-docker-utils.js +++ b/test/e2e/cht-docker-utils.js @@ -38,22 +38,18 @@ const ensureScriptExists = async () => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const getProjectConfig = async (projectName) => { - try { - const configFile = await fs.promises.readFile(path.resolve(dockerHelperDirectory, `${projectName}.env`), 'utf8'); - return Object.fromEntries( - configFile.toString() - .split('\n') - .map(line => line.split('=')) - .filter(entry => entry.length === 2), - ); - } catch (error) { - log.error(error); - return { - COUCHDB_USER: 'medic', - COUCHDB_PASSWORD: 'password', - NGINX_HTTPS_PORT: '10443', - }; + const configFilePath = path.resolve(dockerHelperDirectory, `${projectName}.env`); + if (!fs.existsSync(configFilePath)) { + throw new Error(`Unexpected error: config file not found at ${configFilePath}`); } + + const configFile = await fs.promises.readFile(configFilePath, 'utf8'); + return Object.fromEntries( + configFile.toString() + .split('\n') + .map(line => line.split('=')) + .filter(entry => entry.length === 2), + ); }; const getProjectUrl = async (projectName = DEFAULT_PROJECT_NAME) => { From 116d5b281a8fa7670f23089fc068f31804e3b64e Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 21:44:36 +0200 Subject: [PATCH 44/47] extract `readCompiledAppSettings` & `writeBaseAppSettings` cht conf utils --- test/e2e/cht-conf-utils.js | 19 +++++++++++++++++++ test/e2e/edit-app-settings.spec.js | 15 ++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/test/e2e/cht-conf-utils.js b/test/e2e/cht-conf-utils.js index 4dc85933..82802e70 100644 --- a/test/e2e/cht-conf-utils.js +++ b/test/e2e/cht-conf-utils.js @@ -49,9 +49,28 @@ const initProject = async (projectName) => { await runChtConf(projectName, 'initialise-project-layout'); }; +const writeBaseAppSettings = async (projectName, baseSettings) => { + const projectDirectory = getProjectDirectory(projectName); + + return await fs.promises.writeFile( + path.join(projectDirectory, 'app_settings/base_settings.json'), + JSON.stringify(baseSettings, null, 2), + ); +}; + +const readCompiledAppSettings = async (projectName) => { + const projectDirectory = getProjectDirectory(projectName); + + return JSON.parse( + await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json'), 'utf8') + ); +}; + module.exports = { cleanupProject, getProjectDirectory, initProject, runChtConf, + readCompiledAppSettings, + writeBaseAppSettings, }; diff --git a/test/e2e/edit-app-settings.spec.js b/test/e2e/edit-app-settings.spec.js index d96ce2b8..f44be9e6 100644 --- a/test/e2e/edit-app-settings.spec.js +++ b/test/e2e/edit-app-settings.spec.js @@ -1,19 +1,17 @@ -const fs = require('fs'); -const path = require('path'); const { expect } = require('chai'); const request = require('request-promise-native'); const { DEFAULT_PROJECT_NAME, getProjectUrl } = require('./cht-docker-utils'); const { cleanupProject, - getProjectDirectory, initProject, runChtConf, + readCompiledAppSettings, + writeBaseAppSettings, } = require('./cht-conf-utils'); describe('edit-app-settings', () => { const projectName = DEFAULT_PROJECT_NAME; - const projectDirectory = getProjectDirectory(projectName); before(async () => { await initProject(projectName); @@ -39,15 +37,10 @@ describe('edit-app-settings', () => { }); baseSettings.locale = 'fr'; baseSettings.locale_outgoing = 'fr'; - await fs.promises.writeFile( - path.join(projectDirectory, 'app_settings/base_settings.json'), - JSON.stringify(baseSettings, null, 2), - ); + await writeBaseAppSettings(projectName, baseSettings); await runChtConf(projectName, 'compile-app-settings'); - const compiledSettings = JSON.parse( - await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json'), 'utf8') - ); + const compiledSettings = await readCompiledAppSettings(projectName); expect(compiledSettings.languages.find(language => language.locale === 'en')).to.deep.equal({ locale: 'en', enabled: false, From ebbd7e9e6e73c28966b8fa99fe756180cbdda7d8 Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 22:28:11 +0200 Subject: [PATCH 45/47] touch a word about e2e tests in readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 7c9b017f..dc01b897 100644 --- a/README.md +++ b/README.md @@ -303,8 +303,20 @@ To develop a new action or improve an existing one, check the ["Actions" doc](sr ## Testing +### Unit tests + Execute `npm test` to run static analysis checks and the test suite. Requires Docker to run integration tests against a CouchDB instance. +### End-to-end tests + +Run `npm run e2e-test` to run the end-to-end test suite against an actual CHT instance locally. +These tests rely on [CHT Docker Helper](https://docs.communityhealthtoolkit.org/hosting/4.x/app-developer/#cht-docker-helper-for-4x) +to spin up and tear down an instance locally. + +The code interfacing with CHT Docker Helper lives in [`test/e2e/cht-docker-utils.js`](./test/e2e/cht-docker-utils.js). +You should rely on the API exposed by this file to orchestrate CHT instances for testing purposes. +It is preferable to keep the number of CHT instances orchestrated in E2E tests low as it takes a non-negligible amount of time to spin up an instance and can quickly lead to timeouts. + ## Executing your local branch 1. Clone the project locally From fcdd4cfddcb0a8f6a82430569753a1c77806de8a Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 22:37:53 +0200 Subject: [PATCH 46/47] it's better with the right npm script... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc01b897..d5a12b46 100644 --- a/README.md +++ b/README.md @@ -309,7 +309,7 @@ Execute `npm test` to run static analysis checks and the test suite. Requires Do ### End-to-end tests -Run `npm run e2e-test` to run the end-to-end test suite against an actual CHT instance locally. +Run `npm run test-e2e` to run the end-to-end test suite against an actual CHT instance locally. These tests rely on [CHT Docker Helper](https://docs.communityhealthtoolkit.org/hosting/4.x/app-developer/#cht-docker-helper-for-4x) to spin up and tear down an instance locally. From 97fc47a6d1707214276ba328039c465004b4323c Mon Sep 17 00:00:00 2001 From: m5r Date: Thu, 11 Jul 2024 22:38:44 +0200 Subject: [PATCH 47/47] format --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d5a12b46..f29e579c 100644 --- a/README.md +++ b/README.md @@ -309,13 +309,9 @@ Execute `npm test` to run static analysis checks and the test suite. Requires Do ### End-to-end tests -Run `npm run test-e2e` to run the end-to-end test suite against an actual CHT instance locally. -These tests rely on [CHT Docker Helper](https://docs.communityhealthtoolkit.org/hosting/4.x/app-developer/#cht-docker-helper-for-4x) -to spin up and tear down an instance locally. +Run `npm run test-e2e` to run the end-to-end test suite against an actual CHT instance locally. These tests rely on [CHT Docker Helper](https://docs.communityhealthtoolkit.org/hosting/4.x/app-developer/#cht-docker-helper-for-4x) to spin up and tear down an instance locally. -The code interfacing with CHT Docker Helper lives in [`test/e2e/cht-docker-utils.js`](./test/e2e/cht-docker-utils.js). -You should rely on the API exposed by this file to orchestrate CHT instances for testing purposes. -It is preferable to keep the number of CHT instances orchestrated in E2E tests low as it takes a non-negligible amount of time to spin up an instance and can quickly lead to timeouts. +The code interfacing with CHT Docker Helper lives in [`test/e2e/cht-docker-utils.js`](./test/e2e/cht-docker-utils.js). You should rely on the API exposed by this file to orchestrate CHT instances for testing purposes. It is preferable to keep the number of CHT instances orchestrated in E2E tests low as it takes a non-negligible amount of time to spin up an instance and can quickly lead to timeouts. ## Executing your local branch