Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#425): E2E test actions #620

Merged
merged 47 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c29a4f8
feat(#425): Create e2e test setup
sugat009 Jan 30, 2024
208fd85
Setup mocha hooks and add to npm run command
sugat009 Jun 20, 2024
7f98495
orchestrate with mocha hooks
m5r Jun 27, 2024
86a0e97
test run in the ci
m5r Jul 1, 2024
4eda2bf
test run in the ci
m5r Jul 1, 2024
8bca220
move session-token test to test/integration folder
m5r Jul 1, 2024
1402640
add trace logs
m5r Jul 1, 2024
0900742
add trace logs
m5r Jul 1, 2024
8143ac8
remove traces
m5r Jul 1, 2024
25140ba
split logic if we have an existing project configuration file
m5r Jul 1, 2024
a896345
refactor hooks and extract utils functions to deal with cht-docker-he…
m5r Jul 3, 2024
d456a26
refactor `spinUpCht` to fix eslint warning about promise executor can…
m5r Jul 3, 2024
639b888
finally get the first e2e test out, working as expected
m5r Jul 3, 2024
6c3d8f1
trace errors in CI
m5r Jul 4, 2024
cf64ad3
check for existence of project configuration before running teardown
m5r Jul 4, 2024
fb12834
add more traces
m5r Jul 4, 2024
64887d4
do we have the latest version of the script?
m5r Jul 4, 2024
8009130
what's wrong with `docker exec`?
m5r Jul 4, 2024
1b26d68
create make parent directories as needed same as `mkdir -p`
m5r Jul 4, 2024
d85caa1
organize todos, remove debugging logs
m5r Jul 4, 2024
2707755
replace 4 spaces => 2 spaces to follow coding style in the repo
m5r Jul 4, 2024
d24846b
oops forgot these
m5r Jul 4, 2024
e2757eb
extract utils functions to reuse in other tests
m5r Jul 4, 2024
1867912
logs
m5r Jul 4, 2024
2d71043
pass project name to `runChtConf` as expected
m5r Jul 4, 2024
4fcac95
await getProjectUrl()
m5r Jul 4, 2024
626bcc5
sonar :)
m5r Jul 4, 2024
c065b78
clean up
m5r Jul 9, 2024
c22e98e
more clean up
m5r Jul 9, 2024
e36c2e2
clearer test title
m5r Jul 9, 2024
5c78268
- switch back to cht-docker-compose.sh from cht-core master
m5r Jul 10, 2024
9b1cfcc
add trace
m5r Jul 10, 2024
9ec0f86
clean up trace
m5r Jul 10, 2024
25155e7
replace hardcoded package.json name
m5r Jul 10, 2024
b0d8f46
add comments to explain the rationale behind the `stdio` option when …
m5r Jul 10, 2024
9688f80
remove linting before running e2e tests
m5r Jul 11, 2024
6899214
increase timeout to prevent frequent failures due to CHT instance tak…
m5r Jul 11, 2024
52d59e0
remove `.cht-docker-helper` in teardown
m5r Jul 11, 2024
3f57497
fix import of `DEFAULT_PROJECT_NAME`
m5r Jul 11, 2024
dcafa3e
dedup code in `initProject`
m5r Jul 11, 2024
8cab016
remove unnecessary `structuredClone`
m5r Jul 11, 2024
0b2379a
add assertions about `baseSettings.language`
m5r Jul 11, 2024
e29816f
throw error early when config file doesn't exist
m5r Jul 11, 2024
116d5b2
extract `readCompiledAppSettings` & `writeBaseAppSettings` cht conf u…
m5r Jul 11, 2024
ebbd7e9
touch a word about e2e tests in readme
m5r Jul 11, 2024
fcdd4cf
it's better with the right npm script...
m5r Jul 11, 2024
97fc47a
format
m5r Jul 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,19 @@ jobs:
coverage
.nyc_output
if: ${{ failure() }}

e2e:
jkuester marked this conversation as resolved.
Show resolved Hide resolved
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/[email protected]#egg=pyxform-medic
npm ci
- name: Run E2E tests
run: npm run test-e2e
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ coverage
.nyc_output
.DS_Store
test/.DS_Store
test/e2e/.cht-docker-helper
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"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",
m5r marked this conversation as resolved.
Show resolved Hide resolved
"semantic-release": "semantic-release"
},
"bin": {
Expand Down
14 changes: 14 additions & 0 deletions test/e2e/.mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
allowUncaught: false,
color: true,
checkLeaks: true,
fullTrace: true,
asyncOnly: false,
spec: ['test/e2e/**/*.spec.js'],
timeout: 60_000, // API takes a little long to start up
reporter: 'spec',
file: ['test/e2e/hooks.js'],
captureFile: 'test/e2e/results.txt',
exit: true,
recursive: true,
};
57 changes: 57 additions & 0 deletions test/e2e/cht-conf-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const path = require('path');
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}`);

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, stderr) => {
if (!error) {
return resolve(stdout);
}

log.error(stderr);
reject(new Error(stdout.toString('utf8')));
});
});
});

const cleanupProject = (projectName) => {
const projectDirectory = getProjectDirectory(projectName);
fse.removeSync(projectDirectory);
};

const initProject = async (projectName) => {
const projectDirectory = getProjectDirectory(projectName);
if (fs.existsSync(projectDirectory)) {
m5r marked this conversation as resolved.
Show resolved Hide resolved
fse.removeSync(projectDirectory);
}

fse.mkdirpSync(projectDirectory);
fs.writeFileSync(
jkuester marked this conversation as resolved.
Show resolved Hide resolved
path.join(projectDirectory, 'package.json'),
JSON.stringify({
name: 'e2e-edit-app-settings',
version: '1.0.0',
dependencies: {
'cht-conf': 'file:../..',
},
}, null, 4),
);

await runChtConf(projectName, 'initialise-project-layout');
};

module.exports = {
cleanupProject,
getProjectDirectory,
initProject,
runChtConf,
};
134 changes: 134 additions & 0 deletions test/e2e/cht-docker-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const path = require('path');
const fs = require('fs');
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');

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));
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 () => {
if (!fs.existsSync(dockerHelperDirectory)) {
await fs.promises.mkdir(dockerHelperDirectory);
}

if (!fs.existsSync(dockerHelperScript)) {
jkuester marked this conversation as resolved.
Show resolved Hide resolved
await downloadDockerHelperScript();
}
};

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) {
m5r marked this conversation as resolved.
Show resolved Hide resolved
log.error(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) => {
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) => {
if (error.error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
await sleep(1000);
return isProjectReady(projectName, attempt + 1);
}

if ([502, 503].includes(error.statusCode)) {
await sleep(1000);
return isProjectReady(projectName, attempt + 1);
}

throw error;
});
};

const startProject = (projectName) => new Promise((resolve, reject) => {
const configFile = path.resolve(dockerHelperDirectory, `${projectName}.env`);
if (fs.existsSync(configFile)) {
m5r marked this conversation as resolved.
Show resolved Hide resolved
// 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`
m5r marked this conversation as resolved.
Show resolved Hide resolved
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');
jkuester marked this conversation as resolved.
Show resolved Hide resolved
childProcess.stdin.write('y\n');
childProcess.stdin.write(`${projectName}\n`);
}
});

const destroyProject = (projectName) => 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 (projectName = DEFAULT_PROJECT_NAME) => {
await ensureScriptExists();
await startProject(projectName);
};

const tearDownCht = async (projectName = DEFAULT_PROJECT_NAME) => {
if (!fs.existsSync(path.resolve(dockerHelperDirectory, `${projectName}.env`))) {
return;
}

await ensureScriptExists();
await destroyProject(projectName);
};

module.exports = {
DEFAULT_PROJECT_NAME,
getProjectUrl,
spinUpCht,
tearDownCht,
};
69 changes: 69 additions & 0 deletions test/e2e/edit-app-settings.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const fs = require('fs');
const path = require('path');
const { expect } = require('chai');
const request = require('request-promise-native');

const { getProjectUrl } = require('./cht-docker-utils');
const {
DEFAULT_PROJECT_NAME,
m5r marked this conversation as resolved.
Show resolved Hide resolved
cleanupProject,
getProjectDirectory,
initProject,
runChtConf,
} = require('./cht-conf-utils');

describe('edit-app-settings', () => {
const projectName = DEFAULT_PROJECT_NAME;
const projectDirectory = getProjectDirectory(projectName);

before(async () => {
await initProject(projectName);
});

after(async () => {
await cleanupProject(projectName);
});

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 });
m5r marked this conversation as resolved.
Show resolved Hide resolved

// 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);
m5r marked this conversation as resolved.
Show resolved Hide resolved
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),
);
m5r marked this conversation as resolved.
Show resolved Hide resolved

await runChtConf(projectName, 'compile-app-settings');
const compiledSettings = JSON.parse(
await fs.promises.readFile(path.join(projectDirectory, 'app_settings.json'), 'utf8')
);
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(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',
enabled: false,
});
expect(newSettings.locale).to.equal('fr');
expect(newSettings.locale_outgoing).to.equal('fr');

m5r marked this conversation as resolved.
Show resolved Hide resolved
});
});
11 changes: 11 additions & 0 deletions test/e2e/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { spinUpCht, tearDownCht } = require('./cht-docker-utils');

before(async () => {
// cleanup eventual leftovers before starting
await tearDownCht();
await spinUpCht();
});

after(async () => {
await tearDownCht();
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const runCliCommand = (command) => {
});
};

describe('e2e/session-token', function() {
describe('integration/session-token', function() {
this.timeout(15000);

let sessionToken;
Expand Down Expand Up @@ -149,4 +149,4 @@ describe('e2e/session-token', function() {
// Bad Request: Malformed AuthSession cookie
.that.contains('INFO Error: Received error code 400');
});
});
});