const {test} = require('tap');
const imaps = require('imap-simple');
const qp = require('quoted-printable');
require('dotenv').config();
const ize = Number(process.env.ize);
function create_deferred() {
let resolve, reject;
let p = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject});
p.resolve = resolve;
p.reject = reject;
return p;
}
async function cursor(p) {
if(ize) {
await p.addInitScript(function() {
if(window === window.parent) {
window.addEventListener('DOMContentLoaded', function() {
const box = document.createElement('puppeteer-mouse-pointer');
const styleElement = document.createElement('style');
styleElement.innerHTML = `
puppeteer-mouse-pointer {
pointer-events: none;
position: absolute;
top: 0;
z-index: 10000;
left: 0;
width: 20px;
height: 20px;
background-color: white;
opacity: .5;
border: 1px solid black;
border-radius: 10px;
margin: -10px 0 0 -10px;
padding: 0;
}
`;
document.head.appendChild(styleElement);
document.body.appendChild(box);
document.addEventListener('mousemove', event => {
styleElement.sheet.cssRules[0].style.left = event.pageX + 'px';
styleElement.sheet.cssRules[0].style.top = event.pageY + 'px';
}, true);
document.addEventListener(
'mousedown',
({buttons}) => styleElement.sheet.cssRules[0].style.backgroundColor = buttons === 7 ? 'black' : `rgb(${buttons & 1 ? 255 : 0}, ${buttons & 2 ? 255 : 0}, ${buttons & 4 ? 255 : 0})`,
true
);
document.addEventListener(
'mouseup',
({buttons}) => styleElement.sheet.cssRules[0].style.backgroundColor = buttons ? `rgb(${buttons & 1 ? 255 : 0}, ${buttons & 2 ? 255 : 0}, ${buttons & 4 ? 255 : 0})` : 'white',
true
);
}, false);
}
});
}
return p;
}
(async function() {
const browser = await require('playwright').chromium.launch(ize ? {headless: false, slowMo: ize / 10} : undefined);
const c1 = await browser.newContext();
const page = await c1.newPage().then(cursor);
let page2 = browser.newContext()
.then(c => c.newPage())
.then(cursor)
.then(async p => {await p.goto('http://localhost:3000'); return p;})
.then(async p => {await p.click('#age-gate > button'); return p;});
let otp = create_deferred();
let afterfirst = false;
let inbox = imaps.connect({
imap: {
user: process.env.email,
password: process.env.password,
host: process.env.imaphost,
port: process.env.imapport,
tls: true,
tlsOptions: { servername: process.env.imaphost }
},
onmail: async function() {
if(afterfirst) {
let shit = await inbox.search([['TO', nowEmail]], {bodies: ['TEXT'], markSeen: false})
if(shit.length) {
let out = [];
for(let i = 0; i < shit.length; ++i) {
await inbox.deleteMessage(shit[i].attributes.uid);
out.push(qp.decode(shit[i].parts[0].body));
}
otp.resolve(out);
}
} else {
afterfirst = true; }
}
}).then(async c => {await c.openBox('INBOX'); return c;});
let nowEmail = process.env.email.split('@');
nowEmail = nowEmail[0] + process.env.subaddressing + Date.now() + '@' + nowEmail[1];
await test("there is an age gate on navigation and not logged in", {bail: true}, async function(t) {
t.plan(1); await page.goto('http://localhost:3000');
t.resolves(page.click('#age-gate > button'), "it's passable"); });
await test("can't login / request otp of nonexistent user", {bail: true}, async function(t) {
t.plan(4);
const body = await page.innerHTML('body');
await page.fill("input[type='email']", nowEmail);
async function dialogListener(dialog) {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'user does not exist', '"user does not exist" alert');
t.equal(await page.innerHTML('body'), body, 'site contents same as before button click');
_.resolve();
}
page.on('dialog', dialogListener);
let _ = create_deferred();
await page.click('text=login');
await _;
_ = create_deferred();
await page.click('text=send login link to email');
await _;
page.off('dialog', dialogListener);
});
await test("registering nonexistent user logs in", {bail: true}, async function(t) {
t.plan(2);
await page.click('text=register');
await t.resolves(page.waitForSelector('text=logout'), 'logged in since logout button is present');
t.notEqual(await page.$('text=' + nowEmail), null, 'and email address is present');
});
await test("navigating away / refreshing after login without remember me logs out", {bail: true}, async function(t) {
t.plan(3);
await page.goto('http://localhost:3000/user'); await t.resolves(page.click('#age-gate > button'), "need to pass through age gate again since its state is not persistent across page loads and");
t.notEqual(await page.$('text=login'), null, 'logged out since (login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present)');
});
{
const body = await page.innerHTML('body');
await test("can't register existing user", {bail: true}, async function(t) {
t.plan(2);
await page.fill("input[type='email']", nowEmail);
let _ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'user already exists', '"user already exists" alert');
_.resolve();
});
await page.click('text=register');
await _;
t.equal(await page.innerHTML('body'), body, 'site contents same as before button click');
});
await test("can't login previously registered user with incorrect credentials", {bail: true}, async function(t) {
t.plan(2);
await page.fill("input[type='password']", 'X');
let _ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'wrong passphrase', '"wrong passphrase" alert');
_.resolve();
});
await page.click('text=login');
await _;
await page.fill("input[type='password']", '');
t.equal(await page.innerHTML('body'), body, "site contents same as before button click (after clearing input[type='password'] since if not empty, site assumes user wants to login via otp from email instead of clicking link and changes the button with text 'send login link to email' to 'login via code from email')");
});
await test("login previously registered user with correct credentials", {bail: true}, async function(t) {
t.plan(2);
await page.click('text=login');
await t.resolves(page.waitForSelector('text=logout'), 'logged in since logout button is present');
t.notEqual(await page.$('text=' + nowEmail), null, 'and email address is present');
});
await test("logging out shows same content as before login", {bail: true}, async function(t) {
t.plan(3);
await page.click('text=logout');
await t.resolves(page.waitForSelector('text=login'), 'logged out since login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present');
t.equal(await page.innerHTML('body'), body, 'site contents same as before login');
});
}
await test("navigating away / refreshing after login with remember me stays logged in", {bail: true}, async function(t) {
t.plan(5);
await page.fill("input[type='email']", nowEmail);
await page.check('text=remember me');
await page.click('text=login');
await t.resolves(page.waitForSelector('text=logout'), 'logged in since logout button is present');
t.notEqual(await page.$('text=' + nowEmail), null, 'and email address is present');
await page.reload();
await t.resolves(page.waitForSelector('text=logout'), 'logged in since logout button is present');
t.notEqual(await page.$('text=' + nowEmail), null, 'and email address is present');
t.equal(await page.$('#age-gate'), null, "even though age gate state is not persistent across page loads, it doesn't even show since we're logged in");
});
await test("login on different session will logout original session and remove its remember me", {bail: true}, async function(t) {
t.plan(9);
let _ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'logged in elsewhere', 'session 1 detected login from session 2');
_.resolve();
});
page2 = await page2;
await page2.fill("input[type='email']", nowEmail);
await page2.check('text=remember me');
await page2.click('text=login');
await t.resolves(page2.waitForSelector('text=logout'), 'logged in at session 2 since logout button is present');
t.notEqual(await page2.$('text=' + nowEmail), null, 'and email address is present');
await _;
t.notEqual(await page.$('text=login'), null, 'session 1 logged out since login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present');
t.notEqual(await page.$('#age-gate > button'), null, "show age gate since it was never passed this navigation since we were logged in via remember me");
await page.reload();
await t.resolves(page.click('#age-gate > button'), 'age gate is passable after reload since not blocked by "invalid token" alert (token should be deleted from persistent storage on user replace null event)');
t.notEqual(await page.$('text=login'), null, 'still logged out since login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present');
});
await test("login on different session will invalidate remember me on navigation if sharedworker was dead", {bail: true}, async function(t) {
t.plan(6);
await page2.goto('about:blank');
await page.fill("input[type='email']", nowEmail);
await page.check('text=remember me'); await page.click('text=login');
await t.resolves(page.waitForSelector('text=logout'), 'logged in at session 1 since logout button is present');
t.notEqual(await page.$('text=' + nowEmail), null, 'and email address is present');
let _ = create_deferred();
page2.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'invalid token', 'after navigation in session 2, alert "invalid token" since sharedworker could not delete the token from persistent storage since it was dead'); _.resolve();
});
await page2.goto('http://localhost:3000');
await _;
t.resolves(page2.click('#age-gate > button'), 'there is an age gate due to auto login failure and is passable after dismissing alert');
t.notEqual(await page2.$('text=login'), null, 'logged out since login button is present');
t.equal(await page2.$('text=' + nowEmail), null, 'and email address is present');
});
{
let damail;
await test("otp should not be consumed if remember me can auto login", {bail: true}, async function(t) {
t.plan(5);
await page2.fill("input[type='email']", nowEmail);
let _ = create_deferred();
page2.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'check your email', '"check your email" alert after clicking "send login link to email" in session 2');
_.resolve();
});
await page2.click('text=send login link to email');
inbox = await inbox;
await _;
damail = await otp;
t.equal(damail.length, 1, 'only 1 email sent to user');
await page.setContent(damail[0]);
page.click('a');
await t.resolves(page.waitForSelector('text=logout'), "in session 1, navigate to the URL from email generated from session 2. Now, session 1 is logged in via remember me since no prompt to remember me and logout button is present");
t.equal(await page.$('#age-gate'), null, "and there's no age gate");
t.notEqual(await page.$('text=' + nowEmail), null, "and email address is present");
});
await test("otp will be consumed if remember me cannot auto login", {bail: true}, async function(t) {
t.plan(15);
const page22 = await page2.context().newPage();
await cursor(page22);
await page22.setContent(damail[0]);
let _ = create_deferred();
page22.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
t.equal(dialog.message(), 'remember me?', 'in session 2, open a new tab and navigate to the URL from email generated from session 2 while keeping the tab that generated the email open. Now, session 2 is logged in via otp since there is a prompt to remember me');
await dialog.accept();
_.resolve();
});
let __ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'logged in elsewhere', 'session 1 detected login from session 2');
__.resolve();
});
page22.click('a');
await _;
t.equal(await page22.$('#age-gate'), null, "in new tab since there's no age gate");
t.notEqual(await page22.$('text=logout'), null, "and logout button is present");
t.notEqual(await page22.$('text=' + nowEmail), null, 'and email address is present');
await t.resolves(page2.waitForSelector('text=logout'), "and in original tab since there's no age gate");
t.equal(await page2.$('#age-gate'), null, "and logout button is present");
t.notEqual(await page2.$('text=' + nowEmail), null, 'and email address is present');
await __;
t.notEqual(await page.$('text=login'), null, 'session 1 logged out since login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present');
t.notEqual(await page.$('#age-gate > button'), null, "show age gate since it was never passed this navigation since we were logged in via remember me");
__ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'invalid otp', '"invalid otp" alert since it has been consumed by session 2');
__.resolve();
});
await page.reload();
await __;
await t.resolves(page.click('#age-gate > button'), 'there is an age gate due to auto login failure and is passable after dismissing alert');
t.notEqual(await page.$('text=login'), null, 'still logged out since login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present');
await page22.close(); });
}
await test("remember me via otp works", {bail: true}, async function(t) {
t.plan(3);
await page2.reload();
await t.resolves(page2.waitForSelector('text=logout'), "logged in in session 2 original tab after closing new tab and reloading since logout button is present");
t.equal(await page2.$('#age-gate'), null, "and there's no age gate");
t.notEqual(await page2.$('text=' + nowEmail), null, 'and email address is present');
});
{
let damail;
await test("otp should not be consumed if already logged in", {bail: true}, async function(t) {
t.plan(8);
await page.fill("input[type='email']", nowEmail);
let _ = create_deferred();
otp = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'check your email', '"check your email" alert after clicking "send login link to email" in session 1');
_.resolve();
});
await page.click('text=send login link to email');
await _;
damail = await otp;
t.equal(damail.length, 1, 'only 1 email sent to user');
const page22 = await page2.context().newPage();
await cursor(page22);
await page22.setContent(damail[0]);
page22.click('a');
await t.resolves(page22.waitForSelector('text=logout'), "in session 2 new tab, navigate to the URL from email generated from session 1. Now, session 2 new tab logged in via original tab since no prompt to remember me and logout button is present");
t.equal(await page22.$('#age-gate'), null, "and there's no age gate");
t.notEqual(await page22.$('text=' + nowEmail), null, 'and email address is present');
t.notEqual(await page2.$('text=logout'), "still logged in in session 2 original tab since logout button is present");
t.equal(await page2.$('#age-gate'), null, "and there's no age gate");
t.notEqual(await page2.$('text=' + nowEmail), null, 'and email address is present');
await page22.close();
});
await test("no remember me via otp will login to only one sharedworker", {bail: true}, async function(t) {
t.plan(9);
await page.setContent(damail[0]);
let _ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
t.equal(dialog.message(), 'remember me?', 'in session 1 using the tab that generated the email, navigate to the URL from email. Now, session 1 is logged in via otp since there is a prompt to remember me');
await dialog.dismiss();
_.resolve();
});
let __ = create_deferred();
page2.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
t.equal(dialog.message(), 'logged in elsewhere', 'session 2 detected login from session 1');
await dialog.dismiss();
__.resolve();
})
page.click('a');
await _;
await __;
t.equal(await page.$('#age-gate'), null, "and there's no age gate");
t.notEqual(await page.$('text=logout'), null, "and logout button is present");
t.notEqual(await page.$('text=' + nowEmail), null, 'and email address is present');
_ = create_deferred();
page.once('dialog', async dialog => {
if(ize) {
await new Promise(resolve => setTimeout(resolve, ize));
}
await dialog.dismiss();
t.equal(dialog.message(), 'invalid otp', '"invalid otp" alert since it has already been consumed before reload');
_.resolve();
});
await page.reload();
await _;
await t.resolves(page.click('#age-gate > button'), 'there is an age gate due to auto login failure and is passable after dismissing alert');
t.notEqual(await page.$('text=login'), null, 'logged out since login button is present');
t.equal(await page.$('text=' + nowEmail), null, 'and email address is not present');
});
}
browser.close();
inbox.end();
})();