Release 2.16.1
This commit is contained in:
1 parent
3514d514a3
commit
8ce8228091
25 files changed
+1307
-613
No files matched your search
@@ -0,0 +1,119 @@
|
||||
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; }
|
||||
});
|
||||
Reference in new issue
Block a user