Release v2.19.1: repair managed font references
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m17s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
lzstealth committed 2026-10-24 20:26:00 +01:00
1 parent 90253e98a4
commit 9ca372429b
11 files changed
+212 -23

No files matched your search

+54
View File
@@ -38,6 +38,20 @@ test('content reference extraction deduplicates repeated references within a reg
]);
});
test('content reference extraction decodes HTML-encoded font names before parsing CSS', () => {
const references = collectContentReferences('slide', 12, {
numeric: { value: '<span style="font-family: &#39;Ubuntu&#39;;">Ubuntu</span>', font_family: 'Arial' },
named: { value: '<span style="font-family: &quot;Ubuntu Sans&quot;, sans-serif;">Ubuntu</span>' },
hex: { value: '<span style="font-family: &#x27;Ubuntu&#x27;;">Ubuntu</span>', font_family: 'Ubuntu' }
});
assert.deepEqual(references.map(reference => [reference.owner_key, reference.reference_value]), [
['numeric', 'ubuntu'],
['named', 'ubuntu sans'],
['hex', 'ubuntu']
]);
});
test('content reference replacement preserves unchanged rows and only deletes or inserts the delta', async () => {
const calls = [];
const connection = {
@@ -66,4 +80,44 @@ test('content reference replacement preserves unchanged rows and only deletes or
assert.equal(insertion[1][0].length, 2);
assert.deepEqual(insertion[1][0].map(row => [row[3], row[4]]), [['api-request', '5'], ['rss-feed', '8']]);
assert.ok(!calls.some(([sql]) => sql === 'DELETE FROM c_content_references WHERE owner_type = ? AND owner_id = ?'));
});
test('content reference replacement repairs encoded font references and removes the base font', async () => {
const calls = [];
const connection = {
async query(sql, params) {
calls.push([sql, params]);
if (sql.startsWith('SELECT id, owner_key')) {
return [[
{ id: 101, owner_key: 'second_place', reference_type: 'font-family', reference_value: '&#39' },
{ id: 102, owner_key: 'second_place', reference_type: 'font-family', reference_value: 'arial' }
]];
}
return [{}];
}
};
await replaceContentReferences(connection, 'slide', 22, {
second_place: { value: '<span style="font-family: &#39;Ubuntu&#39;;">Second</span>', font_family: 'Arial' }
});
const deletion = calls.find(([sql]) => sql.startsWith('DELETE FROM c_content_references'));
assert.deepEqual(deletion[1], ['slide', 22, [101, 102]]);
const insertion = calls.find(([sql]) => sql.startsWith('INSERT IGNORE INTO c_content_references'));
assert.deepEqual(insertion[1][0].map(row => [row[2], row[3], row[4]]), [
['second_place', 'font-family', 'ubuntu']
]);
});
test('content references omit built-in font stacks but retain managed font families', () => {
const references = collectContentReferences('slide', 117, {
base: { font_family: 'Arial, Helvetica, sans-serif' },
quoted: { fontFamily: "'TREBUCHET MS', Geneva, sans-serif" },
generic: { value: '<p style="font-family: system-ui, serif, monospace, cursive, fantasy;">Base</p>' },
managed: { font_family: 'Arial', value: '<span style="font-family: &#39;Ubuntu Regular&#39;, Helvetica, sans-serif;">Second</span>' }
});
assert.deepEqual(references.map(reference => [reference.owner_key, reference.reference_value]), [
['managed', 'ubuntu regular']
]);
});
+63
View File
@@ -9,6 +9,11 @@ function createPool(responses) {
const queries = [];
return {
queries,
async getConnection() { return this; },
async beginTransaction() { queries.push(['START TRANSACTION']); },
async commit() { queries.push(['COMMIT']); },
async rollback() { queries.push(['ROLLBACK']); },
release() { queries.push(['RELEASE CONNECTION']); },
async query(sql, params) {
queries.push([sql, params]);
const text = String(sql);
@@ -200,4 +205,62 @@ test('2.17.1 migration removes obsolete dashboard.allow grants and permission',
assert.ok(pool.queries.some(function ([sql, params]) {
return String(sql).includes('DELETE FROM a_permissions') && params[0] === 'dashboard.allow';
}));
});
test('2.19.1 migration rebuilds only managed font references for all content in a transaction', async () => {
const pool = createPool([
{ match: sql => sql.includes('information_schema.COLUMNS'), result: [[{ column_count: 1 }]] },
{ match: sql => sql.includes('information_schema.TABLES'), result: [[{ table_count: 0 }]] },
{
match: (sql, params) => sql.startsWith('SELECT id, content_json') && params[0] === 0,
result: [[{ id: 117, content_json: JSON.stringify({ second_place: {
font_family: 'Arial', value: '<span style="font-family: &#39;Ubuntu Regular&#39;, Helvetica, sans-serif;">Second</span>',
source_id: 4, image: '/media/uploads/keep.png'
} }) }]]
},
{
match: (sql, params) => sql.startsWith('SELECT id, content_json') && params[0] === 117,
result: [[{ id: 118, content_json: { hero: { font_family: 'Acme Display' } } }]]
},
{
match: (sql, params) => sql.startsWith('SELECT id, template_id, region_key') && params[0] === 0,
result: [[{ id: 900, template_id: 42, region_key: 'hero', default_content_json: { font_family: 'Acme Display', feed_id: 8 } }]]
}
]);
const pending = await getPendingMigrations(pool, { currentVersion: '2.19.0' });
assert.deepEqual(pending.map(migration => migration.version), ['2.19.1']);
await pending[0].run(pool);
const deletions = pool.queries.filter(([sql]) => sql.startsWith('DELETE'));
assert.deepEqual(deletions, [['DELETE FROM c_content_references WHERE reference_type = ?', ['font-family']]]);
const inserted = pool.queries.filter(([sql]) => sql.startsWith('INSERT IGNORE INTO c_content_references'))
.flatMap(([, params]) => params[0]);
assert.deepEqual(inserted.map(row => row.slice(0, 5)), [
['slide', 117, 'second_place', 'font-family', 'ubuntu regular'],
['slide', 118, 'hero', 'font-family', 'acme display'],
['template', 42, 'hero', 'font-family', 'acme display']
]);
assert.ok(pool.queries.some(([sql, params]) => sql.startsWith('SELECT id, content_json') && params[0] === 118));
assert.ok(pool.queries.some(([sql, params]) => sql.startsWith('SELECT id, template_id, region_key') && params[0] === 900));
assert.ok(!pool.queries.some(([sql]) => /^(UPDATE|ALTER|TRUNCATE)/.test(sql)));
assert.deepEqual(pool.queries.slice(-2), [['COMMIT'], ['RELEASE CONNECTION']]);
assert.deepEqual(await getPendingMigrations(pool, { currentVersion: '2.19.1' }), []);
});
test('2.19.1 font repair rolls back and releases its connection on failure', async () => {
const pool = createPool([
{ match: sql => sql.includes('information_schema.COLUMNS'), result: [[{ column_count: 1 }]] },
{ match: sql => sql.includes('information_schema.TABLES'), result: [[{ table_count: 0 }]] }
]);
const [migration] = await getPendingMigrations(pool, { currentVersion: '2.19.0' });
const query = pool.query.bind(pool);
pool.query = async function (sql, params) {
if (sql.startsWith('SELECT id, content_json')) throw new Error('read failed');
return query(sql, params);
};
await assert.rejects(migration.run(pool), /read failed/);
assert.deepEqual(pool.queries.slice(-2), [['ROLLBACK'], ['RELEASE CONNECTION']]);
assert.ok(!pool.queries.some(([sql]) => sql === 'COMMIT'));
});