135 lines
8.0 KiB
JavaScript
135 lines
8.0 KiB
JavaScript
// Q175 phase 1 (#1690, design test 24, sage-1340 F1, sage-1347 F7): the page under --sso. A read
|
|
// the Console answers 401 `sign-in required` signs the page out the way a proxy redirect does, and
|
|
// the line links to /auth/login with the current path and query; any other 401 stays an error. Both
|
|
// fetch paths in app.js and both in bus.js (fetchRead and fetchApi) do it. The Sign out form shows
|
|
// only when /api/config says sso. The board-held.test.mjs stub, with two lists of paths: those in
|
|
// window.__signIn answer 401 `sign-in required`, those in window.__other 401 `unauthenticated`. A
|
|
// page URL holding `sso` gets `sso: true` added to /api/config. Poll timers fire after 150 ms.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { Store } from '../../bus/src/store.mjs';
|
|
import { Broker } from '../../bus/src/broker.mjs';
|
|
import { views } from '../../bus/src/views.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { browser } from './browser.mjs';
|
|
import { fixture, close } from './fixture.mjs';
|
|
|
|
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles: { pm: { authority: { withinRole: ['message.send'], crossRole: [] } }, cto: { authority: { withinRole: ['message.send'], crossRole: [] } } } } };
|
|
|
|
const stub = `(() => {
|
|
const real = window.fetch.bind(window);
|
|
window.__reqs = []; window.__signIn = []; window.__other = [];
|
|
const timeout = window.setTimeout.bind(window);
|
|
window.setTimeout = (f, ms, ...rest) => timeout(f, ms === 10000 ? 150 : ms, ...rest);
|
|
const answer = (status, body) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
window.fetch = async (url, init = {}) => {
|
|
const path = String(url).split('?')[0];
|
|
window.__reqs.push(path);
|
|
if (window.__signIn.includes(path)) return answer(401, { error: 'sign-in required' });
|
|
if (window.__other.includes(path)) return answer(401, { error: 'unauthenticated' });
|
|
if (path === '/api/config' && /sso/.test(location.search)) return answer(200, { ...(await (await real(url, init)).json()), sso: true });
|
|
return real(url, init);
|
|
};
|
|
})();`;
|
|
|
|
async function setup() {
|
|
const root = mkdtempSync(join(tmpdir(), 'webui-sso-browser-'));
|
|
const store = new Store(root), b0 = new Broker({ store, businesses });
|
|
b0.request(b0.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi', address: 'pm-run' }), { verb: 'role.claim' });
|
|
const reader = b0.bindReader({ business: 'demo' });
|
|
const bus = views({ call: async (verb, args = {}) => structuredClone(b0.request(reader, { verb, args })) });
|
|
const f = await fixture(), b = await browser({ machine: true });
|
|
const web = await startServer({ port: 0, board: f.boardURL, bus }), base = `http://127.0.0.1:${web.address().port}/`;
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub });
|
|
await b.viewport(1280);
|
|
const wait = expr => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,50))}throw new Error('Condition timed out: '+${JSON.stringify(expr)})})()`);
|
|
const text = sel => b.evaluate(`document.querySelector(${JSON.stringify(sel)})?.textContent ?? null`);
|
|
const shown = sel => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(sel)});return !!e && !e.hidden && getComputedStyle(e).display !== 'none' && !!e.getClientRects().length})()`);
|
|
const reqs = () => b.evaluate('window.__reqs.length');
|
|
const settle = () => new Promise(r => setTimeout(r, 600));
|
|
const done = async () => { await b.close(); await close(web); await f.close(); store.close(); rmSync(root, { recursive: true, force: true }); };
|
|
return { b, base, wait, text, shown, reqs, settle, done };
|
|
}
|
|
|
|
const LOADED = 'document.querySelector("table.sessions [data-open=\'proj/agent1\']")';
|
|
const LINE = 'Signed out. Sign in again.';
|
|
|
|
test('a 401 sign-in required on each fetch path signs the page out with a sign-in link and stops the refresh; another 401 stays an error', { timeout: 120000 }, async () => {
|
|
const { b, base, wait, text, shown, reqs, settle, done } = await setup();
|
|
// The line, its link back to this page, the Sign out form gone, and no further fetch.
|
|
const out = async ret => {
|
|
await wait('!document.querySelector("#signed-out").hidden');
|
|
assert.equal(await text('#signed-out'), LINE);
|
|
assert.equal(await b.evaluate('document.querySelector("#signed-out a").getAttribute("href")'), `/auth/login?return=${encodeURIComponent(ret)}`);
|
|
assert.equal(await shown('#sign-out'), false);
|
|
assert.equal(await b.evaluate('document.querySelector("#refresh").disabled'), true);
|
|
const n = await reqs();
|
|
await b.evaluate('document.querySelector("#refresh").click(), true');
|
|
await settle();
|
|
assert.equal(await reqs(), n, 'the page fetched after signing out');
|
|
};
|
|
try {
|
|
// app.js api(): the Board read, with sso on from /api/config.
|
|
await b.navigate(`${base}?sso&x=1`);
|
|
await wait(LOADED);
|
|
await b.evaluate(`window.__signIn.push('/api/board'), document.querySelector('#refresh').click(), true`);
|
|
await out('/?sso&x=1');
|
|
|
|
// app.js getJSON: the conversation catalogue behind History.
|
|
await b.navigate(`${base}?sso&conv`);
|
|
await wait(LOADED);
|
|
await b.evaluate(`window.__signIn.push('/api/conversations'), document.querySelector('table.sessions [data-history="proj/agent1"]').click(), true`);
|
|
await out('/?sso&conv');
|
|
|
|
// app.js api(): the board POST behind Seen.
|
|
await b.navigate(`${base}?sso&seen`);
|
|
await wait(LOADED);
|
|
await b.evaluate(`document.querySelector('table.sessions [data-open="proj/agent1"]').click(), true`);
|
|
await wait('document.querySelector("#inspection [data-seen]")');
|
|
await b.evaluate(`window.__signIn.push('/api/seen'), document.querySelector('#inspection [data-seen]').click(), true`);
|
|
await out('/?sso&seen');
|
|
|
|
// bus.js fetchRead and fetchApi, on a page whose /api/config has no sso: the link comes from
|
|
// the 401 itself (F1).
|
|
await b.navigate(`${base}?read#/inbox`);
|
|
await wait(LOADED);
|
|
await b.evaluate(`window.__signIn.push('/api/bus/inbox'), document.querySelector('#refresh').click(), true`);
|
|
await out('/?read');
|
|
await b.navigate(`${base}?module`);
|
|
await wait(LOADED);
|
|
await b.evaluate(`window.__signIn.push('/api/queue'), location.hash = '#/queue', true`);
|
|
await out('/?module');
|
|
|
|
// Any other 401 is an error: no sign-out, and the polls go on.
|
|
await b.navigate(`${base}?sso&other`);
|
|
await wait(LOADED);
|
|
await b.evaluate(`window.__other.push('/api/board', '/api/bus/inbox', '/api/queue'), document.querySelector('#refresh').click(), true`);
|
|
await settle();
|
|
assert.equal(await b.evaluate('document.querySelector("#signed-out").hidden'), true);
|
|
assert.equal(await b.evaluate('document.body.classList.contains("signed-out")'), false);
|
|
const n = await reqs();
|
|
await settle();
|
|
assert.ok(await reqs() > n, 'the refresh stopped on another 401');
|
|
await b.evaluate('document.querySelector("#avatar-button").click()');
|
|
assert.equal(await shown('#sign-out'), true);
|
|
} finally { await done(); }
|
|
});
|
|
|
|
test('the Sign out form shows only when /api/config says sso, and posts to /auth/logout', { timeout: 120000 }, async () => {
|
|
const { b, base, wait, shown, done } = await setup();
|
|
try {
|
|
await b.navigate(base);
|
|
await wait(LOADED);
|
|
await new Promise(r => setTimeout(r, 300));
|
|
assert.equal(await shown('#sign-out'), false);
|
|
await b.navigate(`${base}?sso`);
|
|
await wait(`${LOADED} && !document.querySelector("#sign-out").hidden`);
|
|
await b.evaluate('document.querySelector("#avatar-button").click()');
|
|
assert.equal(await shown('#sign-out'), true);
|
|
assert.deepEqual(await b.evaluate('(() => { const f = document.querySelector("#sign-out"); return [f.method, f.getAttribute("action"), f.querySelector("button").textContent, f.querySelector("button").type]; })()'), ['post', '/auth/logout', 'Sign out', 'submit']);
|
|
} finally { await done(); }
|
|
});
|