Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
clr-li committed Dec 14, 2023
2 parents 0dc8e8e + 32a87c7 commit 199c1ad
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 36 deletions.
8 changes: 4 additions & 4 deletions .badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions .badges/lines-of-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ node_modules
.DS_Store
.env
.firebase
test.log
test.*.log
tap.info
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ We use [Selenium](https://www.npmjs.com/package/selenium-webdriver) for cross br
2. Enable the 'Allow Remote Automation' option in Safari's Develop menu

#### Setup Tests of the OAuth Flow
By default tests mock the Firebase Authentication layer (to run faster and not require storing Google account credentials). To test with a real Google account, run tests with `REAL_LOGIN=true npm test` and the tests will pause and open a browser window for you to login with a google account in. You can provide an account email (`[email protected]`) and password (`TEST_PASSWORD=xxxx`) in the `.env` file to attempt automatic login for these test, but they may still require manual input during the login phase if your account has MFA enabled or other security settings that interfere.
By default tests mock the Firebase Authentication layer (to run faster and not require storing Google account credentials). To test with a real Google account, run tests with an account email (`[email protected]`) and password (`TEST_PASSWORD=xxxx`) in the `.env` file. The tests will attempt to automatically login for these test, but they may still require manual input during the login phase if your account has MFA enabled or other security settings that interfere. Currently only supported for Google Chrome.

## Glitch Development
Preferably don't edit directly on Glitch except to change the production `.data` or `.env`. If necessary,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "server.js",
"scripts": {
"start": "DB_FILE=./.data/ATT.db node server.js",
"predev": "open http://localhost:3000 || start chrome \"http://localhost:3000\" || google-chrome 'http://localhost:3000' || echo 'Could not open browser automatically. Please open http://localhost:3000 manually'",
"dev": "DB_FILE=./.data/ATT.db DEVELOPMENT=true PORT=3000 nodemon --inspect -r dotenv/config server.js",
"fire": "node -r dotenv/config deploy.js",
"glitch": "git checkout master && git pull origin main --no-rebase --no-edit && git push glitch --force && git push origin master --force",
Expand Down
161 changes: 148 additions & 13 deletions test/client.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,150 @@
/* node:coverage disable */
const {Builder, Browser, By, Key, until} = require('selenium-webdriver');

(async function example() {
let driver = await new Builder()
.forBrowser(Browser.CHROME)
.build();
try {
await driver.get('https://www.google.com/ncr');
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN);
await driver.wait(until.titleIs('webdriver - Google Search'), 1000);
} finally {
await driver.quit();
// import test utils
const { describe, it, after, afterEach, before, beforeEach } = require('node:test'); // read about the builtin Node.js test framework here: https://nodejs.org/docs/latest-v18.x/api/test.html
const assert = require('node:assert');
const { Builder, Browser, By, Key, until, WebDriver } = require('selenium-webdriver'); // read about selenium here: https://www.selenium.dev/documentation/en/webdriver/
const chrome = require('selenium-webdriver/chrome'); // read about chrome options here: https://chromedriver.chromium.org/capabilities
const { captureConsole } = require('./utils.js');
captureConsole('./test.client.log');

describe('Client', () => {
/** @type {number} */
let port;

/** @type {import('node:http2').Http2Server} */
let listener;

/** @type {WebDriver} */
let driver;

/** @type {number} How many ms to wait for automated browser actions before failing */
const TIMEOUT = 2000;

function supportsGoogleSignIn() {
return process.env.TEST_EMAIL && process.env.TEST_PASSWORD && [undefined, "chrome"].includes(process.env.SELENIUM_BROWSER);
}

async function findDeep(path, element = null) {
if (!element) {
element = driver;
}
const getShadow = "return arguments[0].shadowRoot;";
const parts = path.split('->');
for (let i = 0; i < parts.length; i++) {
try {
element = await element.findElement(By.css(parts[i]));
} catch (e) {
const shadowRoot = await driver.executeScript(getShadow, element);
element = await shadowRoot.findElement(By.css(parts[i]));
}
}
return element;
}
})();

before(async () => {
// spin up the server
process.env.DEVELOPMENT = supportsGoogleSignIn() ? 'false' : 'true';
({ listener } = require('../server.js'));
port = listener.address().port;

// spin up the browser
const chromeOptions = new chrome.Options();
if (supportsGoogleSignIn()) {
chromeOptions.addArguments('--disable-blink-features=AutomationControlled');
chromeOptions.addArguments('--excludeSwitches=enable-automation');
chromeOptions.addArguments('--useAutomationExtension=false');
}

driver = await new Builder()
.forBrowser(Browser.CHROME)
.setChromeOptions(chromeOptions)
.build();
});

describe('Authentication', () => {
it('Should signin and redirect correctly via dev login', { skip: supportsGoogleSignIn() }, async () => {
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`);
await driver.findElement(By.id('signInAsAlex')).click();
await driver.wait(until.titleIs('Home'), TIMEOUT);

// if login worked, trying to login again should redirect directly
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`);
await driver.wait(until.titleIs('Home'), TIMEOUT);
});

it('Should not show dev login in prod', { todo: true, skip: true }, async () => {
// does not currently work because the client uses the hostname to determine if it's in development
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`);
const signInAsAlex = await driver.findElement(By.id('signInAsAlex'));
assert.strictEqual(await signInAsAlex.isDisplayed(), false);
});

it('Should signin and redirect correctly via prod login', { skip: !supportsGoogleSignIn() }, async () => {
// navigate to the login page
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`);
// sign in with google
await driver.findElement(By.id('signInWithGoogle')).click();
// wait for the popup to open
await driver.wait(async () => (await driver.getAllWindowHandles()).length === 2, TIMEOUT);
// figure out which window is the popup
const handles = await driver.getAllWindowHandles();
const main_handle = await driver.getWindowHandle();
const popup_handle = handles.filter(handle => handle !== main_handle)[0];
// switch to the popup
await driver.switchTo().window(popup_handle);
// wait for the popup to load
await driver.wait(until.titleIs('Sign in - Google Accounts'), TIMEOUT);
await driver.sleep(100);
// enter the email
await driver.actions()
.sendKeys(process.env.TEST_EMAIL)
.sendKeys(Key.RETURN)
.perform();
// wait for the password page to load
await driver.sleep(3000);
// enter the password
await driver.actions()
.sendKeys(process.env.TEST_PASSWORD)
.sendKeys(Key.RETURN)
.perform();
// wait for the popup to close
await driver.wait(async () => (await driver.getAllWindowHandles()).length === 1, 60_000);
// switch back to the main window
await driver.switchTo().window(main_handle);
await driver.wait(until.titleIs('Home'), TIMEOUT);
// if login worked, trying to login again should redirect directly
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`);
await driver.wait(until.titleIs('Home'), TIMEOUT);
});

it('Signout should work', async () => {
await driver.get(`http://localhost:${port}`);
await driver.sleep(1000);
const user_icon = await findDeep("navigation-manager->navigation-bar->user-icon");
await user_icon.click();
const profile = await findDeep("#profile", user_icon);
await driver.wait(until.elementIsVisible(profile), TIMEOUT);
assert.strictEqual(await profile.isDisplayed(), true);
const logout = await findDeep("button:nth-of-type(3)", profile);
await logout.click();

// if logout worked, trying to login again should stay on the login page
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`);
await driver.wait(until.titleIs('Login'), TIMEOUT);
try {
await driver.wait(until.titleIs('Home'), TIMEOUT);
assert.fail('Should not redirect to home page after logout');
} catch (e) {
assert.strictEqual(JSON.stringify(e), "{\"name\":\"TimeoutError\",\"remoteStacktrace\":\"\"}");
}
});
});

after(async () => {
// close the browser
await driver.quit();

// close the server
await new Promise((resolve, reject) => listener.close(resolve));
});
});
17 changes: 4 additions & 13 deletions test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,19 @@ const { describe, it, after, afterEach, before, beforeEach } = require('node:tes
const assert = require('node:assert');
const request = require('supertest'); // we use supertest to test HTTP requests/responses. Read more here: https://github.com/ladjs/supertest
const { v4 } = require("uuid");
const { captureConsole } = require('./utils.js');
captureConsole('./test.server.log');

// import code to test
const { app, listener } = require('../server.js');
const { asyncGet, asyncRun, asyncAll, asyncRunWithChanges, asyncRunWithID, reinitializeIfNotExists } = require('../Database.js');
const { createBusiness, deleteBusiness } = require('../Business.js');
const auth = require('../Auth.js');

// ============================ SETUP ============================
/** Capture console log output in a separate file so it doesn't conflict with test output */
const { createWriteStream } = require('fs');
createWriteStream('./test.log', {flags: 'w'}).write('')
console.log = async (message) => {
const tty = createWriteStream('./test.log', {flags: 'a'});
const msg = typeof message === 'string' ? message : JSON.stringify(message, null, 2);
return tty.write(msg + '\n');
}
console.error = console.log;
console.log('# Test logs created on ' + new Date().toISOString());

const TEST_DB_FILE = process.env.DB_FILE || ':memory:';
// ============================ TESTS ============================
describe('Server', () => {
const TEST_DB_FILE = process.env.DB_FILE || ':memory:';

const EXPIRED_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImQwNWI0MDljNmYyMmM0MDNlMWY5MWY5ODY3YWM0OTJhOTA2MTk1NTgiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiQ2xhaXJlIENsaXV3QFVXLkVkdSIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BRWRGVHA0d1R5UVJFNU13dVhNa1B1MGpkZV9ma1FHRllxTDlyTTE3cHBLZT1zOTYtYyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9hdHRlbmRhbmNlc2Nhbm5lcnFyIiwiYXVkIjoiYXR0ZW5kYW5jZXNjYW5uZXJxciIsImF1dGhfdGltZSI6MTY3NTIwNjM5MCwidXNlcl9pZCI6IkEySVN4WktRVU9nSlRhQkpmM2pHMEVjNUNMdzIiLCJzdWIiOiJBMklTeFpLUVVPZ0pUYUJKZjNqRzBFYzVDTHcyIiwiaWF0IjoxNjc1MjA2MzkwLCJleHAiOjE2NzUyMDk5OTAsImVtYWlsIjoiY2xpdXdAdXcuZWR1IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMDIzNDg1MDIyODIwMzg4OTQ5MzUiXSwiZW1haWwiOlsiY2xpdXdAdXcuZWR1Il19LCJzaWduX2luX3Byb3ZpZGVyIjoiZ29vZ2xlLmNvbSJ9fQ.RA4rqYq1fGfU58OthW1zdb76zfSbvmYTf2al-gwQei8d0sZ5YgUKvXt-wHRAsYCzah1mUebmvfG8U2n_wFcIIZG5W48EN2G4idvHtKJNV149SA5H-QZ9MxaYK3FdY68wtKRcl9IExX0tNth7-4gKHfMWF15Yz8ja2MxH8Xp_RgXmEd1gxKD-86-hT0VADM7ccMbIrURK2d9GCpUoCjCgdzLJVuJ62CotCUjF5QoMwL2IeK-pIBwp2eyh-Hsy1BB3bwcgtxf926bD3MLuWjSNJNjntvcqTbtpD-38xt2TzyWIA6t9xkGHTRCMhFlm8dmv_CPXzN12nLqg6xjp-CYCnQ";
const INVALID_TOKEN = v4();
const VALID_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk1MWMwOGM1MTZhZTM1MmI4OWU0ZDJlMGUxNDA5NmY3MzQ5NDJhODciLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiQWxleGFuZGVyIE1ldHpnZXIiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUxtNXd1MEs1SW5aZElPYmhWTW95UDVtaWFzQkxMeFlPRV9KalI4aXg4Y1o9czk2LWMiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vYXR0ZW5kYW5jZXNjYW5uZXJxciIsImF1ZCI6ImF0dGVuZGFuY2VzY2FubmVycXIiLCJhdXRoX3RpbWUiOjE2Njk5NjEzMTUsInVzZXJfaWQiOiJmRlN1dkVuSFpiaGtwYUU0Y1F2eWJDUElPUlYyIiwic3ViIjoiZkZTdXZFbkhaYmhrcGFFNGNRdnliQ1BJT1JWMiIsImlhdCI6MTY2OTk2MTMxNSwiZXhwIjoxNjY5OTY0OTE1LCJlbWFpbCI6ImFsZXhhbmRlci5sZUBvdXRsb29rLmRrIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMDc5NzQzODUyNDExMjU1ODQwODUiXSwiZW1haWwiOlsiYWxleGFuZGVyLmxlQG91dGxvb2suZGsiXX0sInNpZ25faW5fcHJvdmlkZXIiOiJnb29nbGUuY29tIn19.r50SDswArj53NJbwO8vWAYjWVq7uvo_56RBRyt2ZLKyLrHAOWDsj8Muxg1N2OuAOX5ZOZscXttqPb9wwvnh79tYlciZru5GuBcDXYHuMM18HsOBTkqsdWQlnsneDLawMZYP4u5U9dx2NZSCQIpDmfv8CckPfav7izCcdUxAZaKs6ngzBjpz9O7dpKW8pFscaWtncqyH9PXGtChlDd4kOdYO-YJWkA3-ZZ7_S_AviCHbAG-veyTzoacyCPdDJrNzNq9tiWGvILFtmClpMLqf9v9GdvlRt0dPTHx7p-Q6uTlhXvFGIG8ggqbIxbVxVr_sonbV4Nl47lsoDp0icLLjEuQ";
Expand Down
18 changes: 18 additions & 0 deletions test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* node:coverage disable */
// Capture console log output in a separate file so it doesn't conflict with test output.
const { createWriteStream } = require('fs');

module.exports.captureConsole = (filePath = './test.log') => {
const clear = createWriteStream(filePath, {flags: 'w'})
clear.write('')
clear.close();
const tty = createWriteStream(filePath, {flags: 'a'});
console.log = async (...messages) => {
for (const message of messages) {
const msg = typeof message === 'string' ? message : JSON.stringify(message, null, 2);
tty.write(msg + '\n');
}
}
console.error = console.log;
console.log('# Test logs created on ' + new Date().toISOString());
};

0 comments on commit 199c1ad

Please sign in to comment.