diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 202e33f..3d3020b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,16 @@ jobs: - name: Build run: npm run build - - name: Run Tests + - name: Run Tests (local) + run: | + export TEST_DEPLOY_LOCAL="true" + npm install -g @metacall/faas + metacall-faas & + sleep 10 + npm run coverage + + - name: Run Tests (production) + if: github.event_name != 'pull_request' run: | touch .env echo 'METACALL_AUTH_EMAIL="${{ secrets.METACALL_AUTH_EMAIL }}"' >> .env diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0306705 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Run mocha (local)", + "runtimeExecutable": "mocha", + "cwd": "${workspaceFolder}", + "args": ["dist/test"], + "preLaunchTask": "npm: buildDebug", + "autoAttachChildProcesses": true, + "env": { + "TEST_DEPLOY_LOCAL": "true" + } + }, + { + "type": "node", + "request": "launch", + "name": "Run mocha (production)", + "runtimeExecutable": "mocha", + "cwd": "${workspaceFolder}", + "args": ["dist/test"], + "preLaunchTask": "npm: buildDebug", + "autoAttachChildProcesses": true + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/index.js", + "preLaunchTask": "npm: buildDebug", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + } + ] +} diff --git a/package-lock.json b/package-lock.json index 539b30e..851a601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1804,9 +1804,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -3407,12 +3407,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -6460,9 +6460,9 @@ } }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -7659,12 +7659,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, diff --git a/package.json b/package.json index 21775d2..06d5c92 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,13 @@ "metacall-deploy": "dist/index.js" }, "scripts": { - "test": "npm run --silent build && mocha dist/test", + "test": "npm run buildDebug && mocha dist/test", "coverage": "nyc npm run test", - "unit-integration": "npm run --silent test -- --ignore **/*.integration.spec.js", + "unit": "npm run --silent test -- --ignore **/*.integration.spec.js", "prepublishOnly": "npm run --silent build", "postinstall": "node -e \"require('fs').existsSync('githooks') && require('./githooks/configure.js').configure()\"", "build": "npm run --silent lint && tsc", + "buildDebug": "npm run --silent lint && tsc --sourceMap true", "lint": "eslint . --ignore-pattern dist", "fix": "eslint . --ignore-pattern dist --fix", "start": "node dist/index.js" @@ -27,9 +28,9 @@ "deploy", "tool" ], - "author": "Thomas Rory Gummerson (https://trgwii.no/)", + "author": "Vicente Eduardo Ferrer Garcia (https://metacall.io/)", "contributors": [ - "Vicente Eduardo Ferrer Garcia (https://metacall.io/)" + "Thomas Rory Gummerson (https://trgwii.no/)" ], "license": "Apache-2.0", "bugs": { diff --git a/src/startup.ts b/src/startup.ts index b2b17df..593a5f8 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -11,9 +11,11 @@ import { auth } from './auth'; import args from './cli/args'; import { Config, defaultPath, load } from './config'; +const devToken = 'local'; // Use some random token in order to proceed + export const startup = async (confDir: string | undefined): Promise => { const config = await load(confDir || defaultPath); - const token = args['dev'] ? '' : await auth(config); + const token = args['dev'] ? devToken : await auth(config); return Object.assign(config, { token }); }; diff --git a/src/test/cli.integration.spec.ts b/src/test/cli.integration.spec.ts index 92afede..725d9ed 100644 --- a/src/test/cli.integration.spec.ts +++ b/src/test/cli.integration.spec.ts @@ -1,38 +1,16 @@ import { fail, notStrictEqual, ok, strictEqual } from 'assert'; -import * as dotenv from 'dotenv'; -import { promises } from 'fs'; -import os from 'os'; import { join } from 'path'; -import { configFilePath } from '../config'; -import { exists, loadFile } from '../utils'; import { + checkEnvVars, + clearCache, + createTmpDirectory, deleted, deployed, - generateRandomString, keys, - runWithInput -} from './cmd'; + runCLI +} from './cli'; -dotenv.config(); - -// Define tty as interactive in order to test properly the CLI -process.env.NODE_ENV = 'testing'; -process.env.METACALL_DEPLOY_INTERACTIVE = 'true'; - -const runCLI = (args: string[], inputs: string[]) => { - return runWithInput('dist/index.js', args, inputs); -}; - -const clearCache = async (): Promise => { - if (await exists(configFilePath())) - await runCLI(['-l'], [keys.enter]).promise; -}; - -const createTmpDirectory = async (): Promise => { - return await promises.mkdtemp(join(os.tmpdir(), `dep-`)); -}; - -describe('Integration CLI', function () { +describe('Integration CLI (Deploy)', function () { this.timeout(2000000); const url = 'https://github.com/metacall/examples'; @@ -48,96 +26,6 @@ describe('Integration CLI', function () { 'time-app-web' ); - const checkEnvVars = (): { email: string; password: string } | never => { - const email = process.env.METACALL_AUTH_EMAIL; - const password = process.env.METACALL_AUTH_PASSWORD; - - if (typeof email === 'undefined' || typeof password === 'undefined') { - fail( - 'No environment files present to test the below flags, please set up METACALL_AUTH_EMAIL and METACALL_AUTH_PASSWORD' - ); - } - - return { email, password }; - }; - - // Test for env variables before running tests - before(async function () { - await clearCache(); - checkEnvVars(); - }); - - // Invalid token login - it('Should fail with malformed jwt', async () => { - await clearCache(); - - const workdir = await createTmpDirectory(); - - try { - const result = await runCLI( - ['--token=yeet', `--workdir=${workdir}`], - [keys.enter] - ).promise; - - fail( - `The CLI passed without errors and it should have failed. Result: ${String( - result - )}` - ); - } catch (err) { - ok(String(err) === 'X Token invalid: jwt malformed\n'); - } - }); - - // No credentials provided - it('Should fail with no credentials with --token', async () => { - await clearCache(); - - try { - const result = await runCLI( - ['--token='], - [keys.enter, keys.enter, keys.kill] - ).promise; - - fail( - `The CLI passed without errors and it should have failed. Result: ${String( - result - )}` - ); - } catch (err) { - ok( - String(err) === - 'X Token invalid: Invalid authorization header, no credentials provided.\n' - ); - } - }); - - // Invalid login credentials - it('Should fail with invalid login credentials', async () => { - await clearCache(); - - const workdir = await createTmpDirectory(); - - try { - const result = await runCLI( - [ - '--email=yeet@yeet.com', - '--password=yeetyeet', - `--workdir=${workdir}` - ], - [keys.enter] - ).promise; - - fail( - `The CLI passed without errors and it should have failed. Result: ${String( - result - )}` - ); - } catch (err) { - ok(String(err) === 'X Invalid account email or password.\n'); - } - }); - // --email & --password it('Should be able to login using --email & --password flag', async function () { await clearCache(); @@ -161,30 +49,6 @@ describe('Integration CLI', function () { } }); - // --token - it('Should be able to login using --token flag', async function () { - const file = await loadFile(configFilePath()); - const token = file.split('=')[1]; - - await clearCache(); - - notStrictEqual(token, undefined); - - const workdir = await createTmpDirectory(); - - try { - await runCLI( - [`--token=${token}`, `--workdir=${workdir}`], - [keys.enter, keys.enter] - ).promise; - } catch (err) { - strictEqual( - err, - `X The directory you specified (${workdir}) is empty.\n` - ); - } - }); - // --help it('Should be able to print help guide using --help flag', async () => { const result = await runCLI(['--help'], [keys.enter]).promise; @@ -334,6 +198,7 @@ describe('Integration CLI', function () { return result; }); + // TODO: // --force // it('Should be able to deploy forcefully using --force flag', async () => { // const resultDel = await runCLI( @@ -389,134 +254,7 @@ describe('Integration CLI', function () { await runCLI(['--listPlans'], [keys.enter]).promise, 'i Essential: 2\n' )); - - // signup already taken email - it('Should fail with taken email', async () => { - await clearCache(); - try { - const result = await runCLI( - [], - [ - keys.down, - keys.down, - keys.enter, - 'noot@noot.com', - keys.enter, - 'diaa', - keys.enter, - 'diaa', - keys.enter, - 'diaa', - keys.enter - ] - ).promise; - fail( - `The CLI passed without errors and it should fail. Result: ${String( - result - )}` - ); - } catch (error) { - ok(String(error).includes('Account already exists')); - } - }); - - // signup with invalid email - it('Should fail with invalid email', async () => { - await clearCache(); - - try { - const result = await runCLI( - [], - [ - keys.up, - keys.enter, - 'diaabadr82gmail.com', - keys.enter, - '1234', - keys.enter, - '1234', - keys.enter, - 'diaa', - keys.enter - ] - ).promise; - fail( - `The CLI passed without errors and it should fail. Result: ${String( - result - )}` - ); - } catch (error) { - ok(String(error).includes('Invalid email')); - } - }); - - // signup with taken alias - it('Should fail with taken alias', async () => { - await clearCache(); - const str = generateRandomString(Math.floor(Math.random() * 10) + 1); - - try { - const result = await runCLI( - [], - [ - keys.up, - keys.enter, - `${str}@yeet.com`, - keys.enter, - '1234', - keys.enter, - '1234', - keys.enter, - 'creatoon', - keys.enter - ] - ).promise; - fail( - `The CLI passed without errors and it should fail. Result: ${String( - result - )}` - ); - } catch (error) { - ok(String(error).includes('alias is already taken')); - } - }); - - // Note: Disable this test for now, I do not want to spam the FaaS - // success signup - /* - it('Should be able to signup successfully', async () => { - await clearCache(); - const str = generateRandomString(Math.floor(Math.random() * 10) + 1); - - try { - const result = await runCLI( - [], - [ - keys.up, - keys.enter, - `${str}@yeet.com`, - keys.enter, - str, - keys.enter, - str, - keys.enter, - str, - keys.enter - ] - ).promise; - ok(String(result).includes('A verification email has been sent')); - } catch (error) { - fail( - `The CLI failed with error: ${String( - error - )} and it should pass.` - ); - } - }); - */ }); // TODO: Tests to add // if there is only one log file -> select it (TODO: This must be reviewed in case we use TUI) - -// test for mangled token, expired diff --git a/src/test/cmd.ts b/src/test/cli.ts similarity index 74% rename from src/test/cmd.ts rename to src/test/cli.ts index 945ed47..7396e56 100644 --- a/src/test/cmd.ts +++ b/src/test/cli.ts @@ -1,14 +1,23 @@ import API from '@metacall/protocol/protocol'; +import { fail } from 'assert'; import concat from 'concat-stream'; import spawn from 'cross-spawn'; import * as dotenv from 'dotenv'; import { existsSync } from 'fs'; -import { constants } from 'os'; +import fs from 'fs/promises'; +import os from 'os'; +import { join } from 'path'; import args from '../cli/args'; +import { configFilePath } from '../config'; import { startup } from '../startup'; +import { exists } from '../utils'; dotenv.config(); +// Define tty as interactive in order to test properly the CLI +process.env.NODE_ENV = 'testing'; +process.env.METACALL_DEPLOY_INTERACTIVE = 'true'; + const PATH = process.env.PATH; const HOME = process.env.HOME; @@ -59,7 +68,7 @@ export const runWithInput = ( child.stdin?.end(); killTimeout = setTimeout(() => { - child.kill(constants.signals.SIGTERM); + child.kill(os.constants.signals.SIGTERM); }, 3000); return; @@ -181,3 +190,32 @@ export const generateRandomString = (length: number): string => { return result; }; + +export const runCLI = (args: string[], inputs: string[]) => { + if (process.env.TEST_DEPLOY_LOCAL === 'true') { + args.push('--dev'); + } + return runWithInput('dist/index.js', args, inputs); +}; + +export const clearCache = async (): Promise => { + if (await exists(configFilePath())) + await runCLI(['-l'], [keys.enter]).promise; +}; + +export const checkEnvVars = (): { email: string; password: string } | never => { + const email = process.env.METACALL_AUTH_EMAIL; + const password = process.env.METACALL_AUTH_PASSWORD; + + if (typeof email === 'undefined' || typeof password === 'undefined') { + fail( + 'No environment files present to test the below flags, please set up METACALL_AUTH_EMAIL and METACALL_AUTH_PASSWORD' + ); + } + + return { email, password }; +}; + +export const createTmpDirectory = async (): Promise => { + return await fs.mkdtemp(join(os.tmpdir(), `dep-`)); +}; diff --git a/src/test/login.cli.integration.spec.ts b/src/test/login.cli.integration.spec.ts new file mode 100644 index 0000000..f18314b --- /dev/null +++ b/src/test/login.cli.integration.spec.ts @@ -0,0 +1,246 @@ +import { fail, notStrictEqual, ok, strictEqual } from 'assert'; +import { configFilePath } from '../config'; +import { loadFile } from '../utils'; +import { + checkEnvVars, + clearCache, + createTmpDirectory, + generateRandomString, + keys, + runCLI +} from './cli'; + +// Run this test only in production mode, not in local +const describeTest = + process.env.TEST_DEPLOY_LOCAL !== 'true' ? describe : describe.skip; + +describeTest('Integration CLI (Login)', function () { + // Test for env variables before running tests + before(async function () { + await clearCache(); + checkEnvVars(); + }); + + // Invalid token login + it('Should fail with malformed jwt', async () => { + await clearCache(); + + const workdir = await createTmpDirectory(); + + try { + const result = await runCLI( + ['--token=yeet', `--workdir=${workdir}`], + [keys.enter] + ).promise; + + fail( + `The CLI passed without errors and it should have failed. Result: ${String( + result + )}` + ); + } catch (err) { + ok(String(err) === 'X Token invalid: jwt malformed\n'); + } + }); + + // No credentials provided + it('Should fail with no credentials with --token', async () => { + await clearCache(); + + try { + const result = await runCLI( + ['--token='], + [keys.enter, keys.enter, keys.kill] + ).promise; + + fail( + `The CLI passed without errors and it should have failed. Result: ${String( + result + )}` + ); + } catch (err) { + ok( + String(err) === + 'X Token invalid: Invalid authorization header, no credentials provided.\n' + ); + } + }); + + // Invalid login credentials + it('Should fail with invalid login credentials', async () => { + await clearCache(); + + const workdir = await createTmpDirectory(); + + try { + const result = await runCLI( + [ + '--email=yeet@yeet.com', + '--password=yeetyeet', + `--workdir=${workdir}` + ], + [keys.enter] + ).promise; + + fail( + `The CLI passed without errors and it should have failed. Result: ${String( + result + )}` + ); + } catch (err) { + ok(String(err) === 'X Invalid account email or password.\n'); + } + }); + + // --token + it('Should be able to login using --token flag', async function () { + const file = await loadFile(configFilePath()); + const token = file.split('=')[1]; + + await clearCache(); + + notStrictEqual(token, undefined); + + const workdir = await createTmpDirectory(); + + try { + await runCLI( + [`--token=${token}`, `--workdir=${workdir}`], + [keys.enter, keys.enter] + ).promise; + } catch (err) { + strictEqual( + err, + `X The directory you specified (${workdir}) is empty.\n` + ); + } + }); + + // signup already taken email + it('Should fail with taken email', async () => { + await clearCache(); + try { + const result = await runCLI( + [], + [ + keys.down, + keys.down, + keys.enter, + 'noot@noot.com', + keys.enter, + 'diaa', + keys.enter, + 'diaa', + keys.enter, + 'diaa', + keys.enter + ] + ).promise; + fail( + `The CLI passed without errors and it should fail. Result: ${String( + result + )}` + ); + } catch (error) { + ok(String(error).includes('Account already exists')); + } + }); + + // signup with invalid email + it('Should fail with invalid email', async () => { + await clearCache(); + + try { + const result = await runCLI( + [], + [ + keys.up, + keys.enter, + 'diaabadr82gmail.com', + keys.enter, + '1234', + keys.enter, + '1234', + keys.enter, + 'diaa', + keys.enter + ] + ).promise; + fail( + `The CLI passed without errors and it should fail. Result: ${String( + result + )}` + ); + } catch (error) { + ok(String(error).includes('Invalid email')); + } + }); + + // signup with taken alias + it('Should fail with taken alias', async () => { + await clearCache(); + const str = generateRandomString(Math.floor(Math.random() * 10) + 1); + + try { + const result = await runCLI( + [], + [ + keys.up, + keys.enter, + `${str}@yeet.com`, + keys.enter, + '1234', + keys.enter, + '1234', + keys.enter, + 'creatoon', + keys.enter + ] + ).promise; + fail( + `The CLI passed without errors and it should fail. Result: ${String( + result + )}` + ); + } catch (error) { + ok(String(error).includes('alias is already taken')); + } + }); + + // Note: Disable this test for now, I do not want to spam the FaaS + // success signup + /* + it('Should be able to signup successfully', async () => { + await clearCache(); + const str = generateRandomString(Math.floor(Math.random() * 10) + 1); + + try { + const result = await runCLI( + [], + [ + keys.up, + keys.enter, + `${str}@yeet.com`, + keys.enter, + str, + keys.enter, + str, + keys.enter, + str, + keys.enter + ] + ).promise; + ok(String(result).includes('A verification email has been sent')); + } catch (error) { + fail( + `The CLI failed with error: ${String( + error + )} and it should pass.` + ); + } + }); + */ +}); + +// TODO: Tests to add +// test for mangled token, expired