const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const { JSDOM } = require('jsdom'); const test = require('node:test'); const assert = require('node:assert/strict'); const handoffSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'credential-vault-handoff.js'), 'utf8'); const formSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'service-credentials.js'), 'utf8'); const initSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'core', 'init.js'), 'utf8'); function loadVault() { const window = { location: { origin: 'https://status.sami' } }; const context = vm.createContext({ window, SITE: { tld: '.sami' }, URL }); vm.runInContext(handoffSource, context); return window.DCCredentialVault; } async function exerciseFailedModalWrite({ service, fetchJson, setupInputs, expectedEndpoint, writeResponse, expectedError = /vault write rejected/, }) { const dom = new JSDOM('', { url: 'https://status.sami/', runScripts: 'outside-only', }); const { window } = dom; const writeUrls = []; let resumeCalls = 0; window.ErrorHandler = class { logError() {} }; window.SITE = { tld: '.sami' }; window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html); window.fetch = async (url) => ({ ok: true, json: async () => fetchJson(url) }); window.secureFetch = async (url) => { writeUrls.push(url); return writeResponse || { ok: false, status: 500, json: async () => ({ success: false, error: 'vault write rejected' }), }; }; window.DCCredentialVault = { isAllowedReturnUrl: () => true, resume: async () => { resumeCalls++; return true; }, }; window.confirm = () => true; window.eval(formSource); await window.openServiceCredsModal(service, { returnUrl: `https://${service.id}.sami/` }); setupInputs(window.document); window.document.getElementById('svc-creds-save').click(); await new Promise(resolve => setTimeout(resolve, 20)); assert.equal(writeUrls[0], expectedEndpoint); assert.equal(resumeCalls, 0); assert.match(window.document.getElementById('svc-creds-error').textContent, expectedError); } test('existing dashboard session mints a one-time token and resumes on the target host', async () => { const vault = loadVault(); const calls = []; const replacements = []; const resumed = await vault.resume('https://plex.sami/web/?direct=1#home', 'plex', { fetch: async (url, options) => { calls.push({ url, options }); return { ok: true, json: async () => ({ success: true, ssoToken: 'one-time-token' }), }; }, location: { replace: (target) => replacements.push(target) }, }); assert.equal(resumed, true); assert.equal(calls.length, 1); assert.equal(calls[0].url, '/api/v1/auth/sso-handoff?serviceId=plex'); assert.equal(calls[0].options.credentials, 'include'); assert.equal(calls[0].options.cache, 'no-store'); assert.equal( replacements[0], 'https://plex.sami/dashcaddy-sso?token=one-time-token&return=%2Fweb%2F%3Fdirect%3D1%23home', ); }); test('vault handoff rejects an external return URL before minting a token', async () => { const vault = loadVault(); let fetchCalled = false; const resumed = await vault.resume('https://plex.sami.evil.example/phish', 'plex', { fetch: async () => { fetchCalled = true; }, location: { replace: () => assert.fail('must not navigate') }, }); assert.equal(resumed, false); assert.equal(fetchCalled, false); }); test('credential request opens the form and save path calls the tested handoff helper', () => { assert.match(formSource, /params\.get\('credentials'\)/); assert.match(formSource, /openServiceCredsModal\(service, \{ returnUrl \}\)/); assert.match(formSource, /DCCredentialVault\?\.resume\(returnUrl, currentService\.id\)/); assert.match(initSource, /openRequestedCredentialForm\(\)/); assert.match(formSource, /Save to vault & open service/); }); test('actual vault modal save handler stores credentials then resumes the handoff', async () => { const dom = new JSDOM('', { url: 'https://status.sami/?credentials=plex&return=https%3A%2F%2Fplex.sami%2Fweb%2F', runScripts: 'outside-only', }); const { window } = dom; let stored = false; const writes = []; const resumed = []; window.ErrorHandler = class { logError() {} }; window.SITE = { tld: '.sami' }; window.APPS = [{ id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' }]; window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html); window.fetch = async () => ({ ok: true, json: async () => ({ success: true, hasApiKey: false, hasBasicAuth: stored, username: stored ? 'vault-user' : null, }), }); window.secureFetch = async (url, options) => { writes.push({ url, body: JSON.parse(options.body) }); stored = true; return { ok: true, json: async () => ({ success: true }) }; }; window.DCCredentialVault = { isAllowedReturnUrl: () => true, resume: async (returnUrl, serviceId) => { resumed.push({ returnUrl, serviceId }); return true; }, }; window.confirm = () => true; window.eval(formSource); await window.openServiceCredsModal(window.APPS[0], { returnUrl: 'https://plex.sami/web/' }); window.document.getElementById('svc-basic-user').value = 'vault-user'; window.document.getElementById('svc-basic-pass').value = 'vault-password'; window.document.getElementById('svc-creds-save').click(); await new Promise(resolve => setTimeout(resolve, 20)); assert.deepEqual(writes, [{ url: '/api/v1/services/plex/credentials', body: { username: 'vault-user', password: 'vault-password' }, }]); assert.deepEqual(resumed, [{ returnUrl: 'https://plex.sami/web/', serviceId: 'plex' }]); }); test('failed credential write does not mint a handoff or navigate', async () => { const dom = new JSDOM('', { url: 'https://status.sami/', runScripts: 'outside-only', }); const { window } = dom; let resumeCalls = 0; window.ErrorHandler = class { logError() {} }; window.SITE = { tld: '.sami' }; window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html); window.fetch = async () => ({ ok: true, json: async () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }), }); window.secureFetch = async () => ({ ok: false, status: 500, json: async () => ({ success: false, error: 'vault write rejected' }), }); window.DCCredentialVault = { isAllowedReturnUrl: () => true, resume: async () => { resumeCalls++; return true; }, }; window.confirm = () => true; window.eval(formSource); const service = { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' }; await window.openServiceCredsModal(service, { returnUrl: 'https://plex.sami/web/' }); window.document.getElementById('svc-basic-user').value = 'vault-user'; window.document.getElementById('svc-basic-pass').value = 'vault-password'; window.document.getElementById('svc-creds-save').click(); await new Promise(resolve => setTimeout(resolve, 20)); assert.equal(resumeCalls, 0); assert.match(window.document.getElementById('svc-creds-error').textContent, /vault write rejected/); }); test('failed ARR credential write does not mint a handoff or navigate', async () => { await exerciseFailedModalWrite({ service: { id: 'radarr', name: 'Radarr', appTemplate: 'radarr', url: 'https://radarr.sami' }, fetchJson: (url) => url.includes('/services/') ? { success: true, hasApiKey: false, hasBasicAuth: false, username: null } : { success: true, profiles: [] }, setupInputs: (document) => { document.getElementById('svc-apikey-input').value = 'arr-key'; }, expectedEndpoint: '/api/v1/arr/credentials', }); }); test('failed ARR quality-profile write does not mint a handoff or navigate', async () => { await exerciseFailedModalWrite({ service: { id: 'radarr', name: 'Radarr', appTemplate: 'radarr', url: 'https://radarr.sami' }, fetchJson: (url) => url.includes('/services/') ? { success: true, hasApiKey: true, hasBasicAuth: false, username: null } : { success: true, profiles: [{ id: 1, name: 'Default' }], storedProfileId: 1 }, setupInputs: () => {}, expectedEndpoint: '/api/v1/arr/quality-profiles', }); }); test('failed seedhost write does not mint a handoff or navigate', async () => { await exerciseFailedModalWrite({ service: { id: 'torrent', name: 'qBittorrent', isExternal: true, externalUrl: 'https://torrent.sami' }, fetchJson: (url) => url.includes('/seedhost-creds') ? { success: true, hasCredentials: false, username: null } : { success: true, hasApiKey: false, hasBasicAuth: false, username: null }, setupInputs: (document) => { document.getElementById('svc-seedhost-user').value = 'seed-user'; document.getElementById('svc-seedhost-pass').value = 'seed-password'; }, expectedEndpoint: '/api/v1/seedhost-creds', }); }); test('failed generic API-key write does not mint a handoff or navigate', async () => { await exerciseFailedModalWrite({ service: { id: 'custom', name: 'Custom', url: 'https://custom.sami' }, fetchJson: () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }), setupInputs: (document) => { document.getElementById('svc-apikey-input').value = 'custom-key'; document.getElementById('svc-basic-user').value = 'user'; document.getElementById('svc-basic-pass').value = 'password'; }, expectedEndpoint: '/api/v1/services/custom/credentials', }); }); test('HTTP 2xx with malformed JSON does not mint a handoff or navigate', async () => { await exerciseFailedModalWrite({ service: { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' }, fetchJson: () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }), setupInputs: (document) => { document.getElementById('svc-basic-user').value = 'user'; document.getElementById('svc-basic-pass').value = 'password'; }, expectedEndpoint: '/api/v1/services/plex/credentials', writeResponse: { ok: true, status: 200, json: async () => { throw new Error('bad json'); } }, expectedError: /invalid server response/, }); }); test('HTTP 2xx without success:true does not mint a handoff or navigate', async () => { await exerciseFailedModalWrite({ service: { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' }, fetchJson: () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }), setupInputs: (document) => { document.getElementById('svc-basic-user').value = 'user'; document.getElementById('svc-basic-pass').value = 'password'; }, expectedEndpoint: '/api/v1/services/plex/credentials', writeResponse: { ok: true, status: 200, json: async () => ({ message: 'ambiguous' }) }, expectedError: /failed \(200\)/, }); }); test('failed credential clear remains visibly failed and keeps stored-state UI', async () => { const dom = new JSDOM('', { url: 'https://status.sami/', runScripts: 'outside-only', }); const { window } = dom; window.ErrorHandler = class { logError() {} }; window.SITE = { tld: '.sami' }; window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html); window.fetch = async () => ({ ok: true, json: async () => ({ success: true, hasApiKey: false, hasBasicAuth: true, username: 'vault-user' }), }); window.secureFetch = async () => ({ ok: false, status: 500, json: async () => ({ success: false, error: 'clear rejected' }), }); window.DCCredentialVault = { isAllowedReturnUrl: () => false }; window.confirm = () => true; window.eval(formSource); const service = { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' }; await window.openServiceCredsModal(service); window.document.getElementById('svc-creds-clear').click(); await new Promise(resolve => setTimeout(resolve, 20)); assert.match(window.document.getElementById('svc-creds-error').textContent, /clear rejected/); assert.equal(window.document.getElementById('creds-btn-plex').classList.contains('has-creds'), true); }); test('handoff rejects a private-TLD host that is not the requested protected service', async () => { const vault = loadVault(); let fetchCalled = false; const resumed = await vault.resume('https://dns1.sami/', 'plex', { fetch: async () => { fetchCalled = true; }, location: { replace: () => assert.fail('must not navigate') }, }); assert.equal(resumed, false); assert.equal(fetchCalled, false); });