Skip to content
Merged
Changes from all commits
Commits
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
74 changes: 72 additions & 2 deletions src/tests/backend/specs/admin/anonymizeAuthorSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,95 @@ const ask = (socket: any, evt: string, payload: any, replyEvt: string) =>
socket.emit(evt, payload);
});

// adminSocket() depends on Etherpad's default plain-text password check for
// settings.users[name].password. Plugins like ep_hash_auth replace the
// authenticate hook to expect hashed credentials, so the basic-auth probe
// returns no admin session, /settings's connection handler returns without
// registering listeners (see src/node/hooks/express/adminsettings.ts:25),
// and every socket.emit() afterwards waits forever for a reply that
// nothing will ever send. The socket itself still connects when admin
// session is missing, so the probe has to run at the application layer:
// emit a known `/settings` event (`load`) and wait for the matching reply
// (`settings`). If it doesn't arrive within the budget, skip — much
// cheaper than letting mocha's 120s per-test timeout absorb 7 stalled
// tests. Tracked in #7795.
const PROBE_BUDGET_MS = 15000;
const adminSocketWithProbe = async (budgetMs: number): Promise<{
ok: true; socket: any;
} | {ok: false; reason: string;}> => {
const deadline = Date.now() + budgetMs;
let socket: any;
try {
socket = await Promise.race([
adminSocket(),
new Promise<never>((_, rej) =>
setTimeout(() => rej(new Error('adminSocket connect timed out')),
Math.max(0, deadline - Date.now()))),
]);
} catch (err: any) {
return {ok: false, reason: String(err && err.message || err)};
}
const remaining = Math.max(0, deadline - Date.now());
// authorLoad is gated on the admin session being present (see
// adminsettings.ts:25 — non-admin connections never register it) but
// doesn't depend on any disk-resident settings file the way `load`
// does, so it's a stable application-level liveness probe.
const replied = new Promise<true>((res) => socket.once('results:authorLoad', () => res(true)));
socket.emit('authorLoad', {
pattern: '__anonymizeAuthorSocket-probe__', offset: 0, limit: 1,
sortBy: 'name', ascending: true, includeErased: false,
});
const probed = await Promise.race([
replied,
new Promise<false>((res) => setTimeout(() => res(false), remaining)),
]);
if (!probed) {
socket.disconnect();
return {ok: false, reason: `no \`results:authorLoad\` reply within ${budgetMs}ms (no admin handlers registered)`};
}
return {ok: true, socket};
};

describe(__filename, function () {
let socket: any;
let originalFlag: boolean;
let savedUsers: any;
let savedRequireAuthentication: boolean;
let setupCompleted = false;

before(async function () {
this.timeout(60000);
await common.init();

// Capture backups BEFORE any mutation so after() can restore cleanly
// even if the probe times out (adminSocket mutates settings.users
// and settings.requireAuthentication on its way in).
settings.gdprAuthorErasure = settings.gdprAuthorErasure || {enabled: false};
originalFlag = settings.gdprAuthorErasure.enabled;
settings.gdprAuthorErasure.enabled = true;
savedUsers = settings.users;
savedRequireAuthentication = settings.requireAuthentication;
socket = await adminSocket();
settings.gdprAuthorErasure.enabled = true;
setupCompleted = true;

const probe = await adminSocketWithProbe(PROBE_BUDGET_MS);
if (!probe.ok) {
console.warn(
`[anonymizeAuthorSocket] admin socket probe failed (${probe.reason}); ` +
'skipping suite — likely an authenticate-hook plugin (e.g. ep_hash_auth) ' +
'rejecting the test\'s plain-text admin credentials. Tracked in #7795.');
this.skip();
return;
}
socket = probe.socket;
});

after(function () {
if (socket) socket.disconnect();
// before() may have called this.skip() before capturing backups (e.g.
// a common.init() failure), so guard against writing undefined into
// settings. Once setupCompleted flips true the backup variables are
// safe to read.
if (!setupCompleted) return;
settings.gdprAuthorErasure.enabled = originalFlag;
// savedUsers and settings.users point at the same object — restoring
// the reference is a no-op against the in-place mutation. Delete the
Expand Down
Loading