fix: guard openSession against empty name from unreachable federation tiles
Clicking an unreachable remote device tile passed empty session name to openSession(), producing /api/federation/2/connect/ (no session name) → 405. Fix: bail early if name is empty or whitespace. Also added guard in grid click handlers to skip error/status tiles entirely, preventing clicks on unreachable or auth_failed tiles.
This commit is contained in:
@@ -839,10 +839,14 @@ function renderGrid(sessions) {
|
|||||||
on(tile, 'click', (e) => {
|
on(tile, 'click', (e) => {
|
||||||
// Don't navigate when clicking the delete button inside the tile
|
// Don't navigate when clicking the delete button inside the tile
|
||||||
if (e.target.closest && e.target.closest('.tile-delete')) return;
|
if (e.target.closest && e.target.closest('.tile-delete')) return;
|
||||||
|
// Don't open error/status tiles (unreachable, auth_failed)
|
||||||
|
if (tile.classList.contains('source-tile--error') || !tile.dataset.session) return;
|
||||||
openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' });
|
openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' });
|
||||||
});
|
});
|
||||||
on(tile, 'keydown', (e) => {
|
on(tile, 'keydown', (e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
// Don't open error/status tiles (unreachable, auth_failed)
|
||||||
|
if (tile.classList.contains('source-tile--error') || !tile.dataset.session) return;
|
||||||
openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' });
|
openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1154,6 +1158,7 @@ function updatePageTitle() {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function openSession(name, opts = {}) {
|
async function openSession(name, opts = {}) {
|
||||||
|
if (!name || !name.trim()) return;
|
||||||
hidePreview();
|
hidePreview();
|
||||||
_viewingSession = name;
|
_viewingSession = name;
|
||||||
_viewingRemoteId = opts.remoteId != null ? opts.remoteId : '';
|
_viewingRemoteId = opts.remoteId != null ? opts.remoteId : '';
|
||||||
|
|||||||
@@ -1240,6 +1240,52 @@ test('openSession for local session still POSTs to local /api/sessions/{name}/co
|
|||||||
globalThis.setTimeout = origSetTimeout;
|
globalThis.setTimeout = origSetTimeout;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('openSession bails early when name is empty string', async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
const origFetch = globalThis.fetch;
|
||||||
|
const origGetById = globalThis.document.getElementById;
|
||||||
|
const origQS = globalThis.document.querySelector;
|
||||||
|
const origSetTimeout = globalThis.setTimeout;
|
||||||
|
globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; };
|
||||||
|
globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } });
|
||||||
|
globalThis.document.querySelector = () => null;
|
||||||
|
globalThis.setTimeout = () => {};
|
||||||
|
globalThis.window._openTerminal = () => {};
|
||||||
|
|
||||||
|
await app.openSession('', {});
|
||||||
|
|
||||||
|
// Should NOT make any fetch calls to /connect
|
||||||
|
const connectCall = fetchCalls.find((c) => c.url && c.url.includes('/connect'));
|
||||||
|
assert.ok(!connectCall, 'should NOT call connect when name is empty string');
|
||||||
|
globalThis.fetch = origFetch;
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelector = origQS;
|
||||||
|
globalThis.setTimeout = origSetTimeout;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openSession bails early when name is whitespace only', async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
const origFetch = globalThis.fetch;
|
||||||
|
const origGetById = globalThis.document.getElementById;
|
||||||
|
const origQS = globalThis.document.querySelector;
|
||||||
|
const origSetTimeout = globalThis.setTimeout;
|
||||||
|
globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; };
|
||||||
|
globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } });
|
||||||
|
globalThis.document.querySelector = () => null;
|
||||||
|
globalThis.setTimeout = () => {};
|
||||||
|
globalThis.window._openTerminal = () => {};
|
||||||
|
|
||||||
|
await app.openSession(' \t\n ', {});
|
||||||
|
|
||||||
|
// Should NOT make any fetch calls to /connect
|
||||||
|
const connectCall = fetchCalls.find((c) => c.url && c.url.includes('/connect'));
|
||||||
|
assert.ok(!connectCall, 'should NOT call connect when name is whitespace only');
|
||||||
|
globalThis.fetch = origFetch;
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelector = origQS;
|
||||||
|
globalThis.setTimeout = origSetTimeout;
|
||||||
|
});
|
||||||
|
|
||||||
// --- closeSession ---
|
// --- closeSession ---
|
||||||
|
|
||||||
test('closeSession is exported', () => {
|
test('closeSession is exported', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user