119 lines
6.9 KiB
JavaScript
119 lines
6.9 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { buildApiGroupPayload, saveApiGroup } = require('../src/data/api-source-groups');
|
|
const { getPendingMigrations } = require('../src/db/migrations');
|
|
const { fetchApiSourcesData, fetchApiSourceById, testApiSourceAuthentication } = require('../src/data/api-sources');
|
|
|
|
function payload(requests, auth) {
|
|
return buildApiGroupPayload(Object.assign({ name: 'Tiltify', auth_method: 'none', requests_json: JSON.stringify(requests) }, auth));
|
|
}
|
|
|
|
function createPool(existing, slides) {
|
|
const calls = [];
|
|
const connection = {
|
|
async beginTransaction() { calls.push(['begin']); },
|
|
async commit() { calls.push(['commit']); },
|
|
async rollback() { calls.push(['rollback']); },
|
|
release() { calls.push(['release']); },
|
|
async query(sql, params) {
|
|
calls.push([sql, params]);
|
|
if (sql.includes('SELECT id FROM i_api_source_groups WHERE id')) return [[{ id: 5 }]];
|
|
if (sql.includes('SELECT id FROM i_api_source_groups WHERE name')) return [[]];
|
|
if (sql.includes('SELECT id FROM i_api_sources')) return [existing.map(id => ({ id }))];
|
|
if (sql.includes('SELECT content_json')) return [(slides || []).map(value => ({ content_json: value }))];
|
|
if (sql.startsWith('INSERT INTO i_api_sources')) return [{ insertId: 99 }];
|
|
if (sql.startsWith('INSERT INTO i_api_source_groups')) return [{ insertId: 10 }];
|
|
return [{}];
|
|
}
|
|
};
|
|
return { calls, async getConnection() { return connection; } };
|
|
}
|
|
|
|
test('group payload supports public requests and independent request settings', () => {
|
|
const group = payload([{ id: 5, name: 'Event', api_url: 'https://example.com/event', update_interval_value: 2, update_interval_unit: 'hours' }, { name: 'Donors', api_url: 'https://example.com/donors', items_path: 'data', update_interval_value: 30, update_interval_unit: 'seconds', enabled: false }]);
|
|
assert.equal(group.auth.auth_method, 'none');
|
|
assert.equal(group.requests[0].id, 5);
|
|
assert.equal(group.requests[0].updateIntervalUnit, 'hours');
|
|
assert.equal(group.requests[1].itemsPath, 'data');
|
|
assert.equal(group.requests[1].enabled, 0);
|
|
assert.throws(() => payload([]), /between 1 and 50/);
|
|
assert.throws(() => payload([{ name: 'Event', api_url: 'https://example.com' }, { name: 'event', api_url: 'https://example.com' }]), /unique name/);
|
|
assert.throws(() => payload([{ id: 5, name: 'Event', api_url: 'https://example.com' }, { id: 5, name: 'Donors', api_url: 'https://example.com' }]), /repeated/);
|
|
});
|
|
|
|
test('request-level auth cannot override group authentication', () => {
|
|
const group = payload([{ name: 'Event', api_url: 'https://example.com', auth_method: 'none' }, { name: 'Donors', api_url: 'https://example.com', auth_bearer_token: 'wrong-token' }], { auth_method: 'bearer', auth_bearer_token: 'shared-token' });
|
|
assert.ok(group.requests.every(request => request.authBearerToken === 'shared-token'));
|
|
});
|
|
|
|
test('saving a group preserves request IDs and does not overwrite response snapshots', async () => {
|
|
const pool = createPool([5]);
|
|
const result = await saveApiGroup(pool, 5, payload([{ id: 5, name: 'Event', api_url: 'https://example.com/event' }, { name: 'Donors', api_url: 'https://example.com/donors' }]), 1, true);
|
|
assert.deepEqual(result.requests.map(request => request.id), [5, 99]);
|
|
assert.equal(result.id, 5);
|
|
const update = pool.calls.find(([sql]) => sql.startsWith('UPDATE i_api_sources'));
|
|
assert.ok(update);
|
|
assert.doesNotMatch(update[0], /last_response|auth_password|auth_bearer_token/);
|
|
assert.deepEqual(update[1].slice(-2), [5, 5]);
|
|
assert.ok(pool.calls.some(([sql]) => sql === 'commit'));
|
|
});
|
|
|
|
test('request ownership, slide usage, and delete permission are enforced before writes', async () => {
|
|
const request = { id: 5, name: 'Event', api_url: 'https://example.com/event' };
|
|
await assert.rejects(saveApiGroup(createPool([5]), 5, payload([{ ...request, id: 6 }]), 1, true), /does not belong/);
|
|
const used = createPool([5, 6], [JSON.stringify({ region: { type: 'api', source_id: 6 } })]);
|
|
await assert.rejects(saveApiGroup(used, 5, payload([request]), 1, true), /still used/);
|
|
assert.ok(!used.calls.some(([sql]) => sql.startsWith('UPDATE')));
|
|
await assert.rejects(saveApiGroup(createPool([5, 6]), 5, payload([request]), 1, false), /permission/);
|
|
const unused = createPool([5, 6]);
|
|
const result = await saveApiGroup(unused, 5, payload([request]), 1, true);
|
|
assert.deepEqual(result.removed, [6]);
|
|
assert.ok(unused.calls.some(([sql]) => sql.startsWith('DELETE FROM i_api_sources')));
|
|
});
|
|
|
|
test('2.16.1 migration preserves source IDs and backfills parent authentication', async () => {
|
|
const calls = [];
|
|
const pool = { async query(sql, params) {
|
|
calls.push([sql, params]);
|
|
if (sql.includes('information_schema.COLUMNS')) return [[{ column_count: 1 }]];
|
|
if (sql.includes('information_schema.TABLES')) return [[{ table_count: 0 }]];
|
|
if (sql.includes('information_schema.KEY_COLUMN_USAGE')) return [[{ fk_count: 1 }]];
|
|
return [[]];
|
|
} };
|
|
const pending = await getPendingMigrations(pool, { currentVersion: '2.16.0' });
|
|
assert.equal(pending.length, 1);
|
|
assert.equal(pending[0].version, '2.16.1');
|
|
await pending[0].run(pool);
|
|
assert.ok(calls.some(([sql]) => sql.includes('INSERT IGNORE INTO i_api_source_groups (id,') && sql.includes('SELECT id,')));
|
|
assert.ok(calls.some(([sql]) => sql.includes('SET auth_source_id = id WHERE auth_source_id IS NULL')));
|
|
assert.ok(!calls.some(([sql]) => /UPDATE i_api_sources SET id\s*=/.test(sql)));
|
|
});
|
|
|
|
test('editor/player API lists exclude credentials while refresh lookup resolves group authentication', async () => {
|
|
const queries = [];
|
|
const pool = { async query(sql) { queries.push(sql); return [[]]; } };
|
|
await fetchApiSourcesData(pool);
|
|
await fetchApiSourceById(pool, 5);
|
|
assert.doesNotMatch(queries[0], /auth_password|token_request_body_json|request\.\*/);
|
|
assert.match(queries[0], /group_name/);
|
|
assert.match(queries[1], /auth\.auth_password/);
|
|
assert.match(queries[1], /WHERE request.id = \?/);
|
|
});
|
|
|
|
test('authentication-only tests never call a data endpoint', async () => {
|
|
const originalFetch = global.fetch;
|
|
const calls = [];
|
|
global.fetch = async function (url) {
|
|
calls.push(url);
|
|
return { status: 200, ok: true, headers: { get: () => 'application/json' }, text: async () => '{"access_token":"private-token"}' };
|
|
};
|
|
try {
|
|
const result = await testApiSourceAuthentication({ auth_method: 'token_login', token_url: 'https://example.com/token', token_request_body_json: '{"secret":"private-secret"}', api_url: 'https://example.com/donors' });
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ['https://example.com/token']);
|
|
assert.doesNotMatch(JSON.stringify(result), /private-token|private-secret/);
|
|
const publicAuth = await testApiSourceAuthentication({ auth_method: 'none' });
|
|
assert.equal(publicAuth.ok, true);
|
|
assert.equal(calls.length, 1);
|
|
} finally { global.fetch = originalFetch; }
|
|
}); |