Release v2.11.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m47s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s

This commit is contained in:
2026-09-04 15:43:55 +01:00
parent 2c150b5b2e
commit 98f969ca0f
104 changed files with 3559 additions and 447 deletions
+124
View File
@@ -157,4 +157,128 @@ test('successful login returns to the requested link instead of the dashboard',
await handlers['POST /login'][0](rememberedRequest, rememberedResponse, () => {});
assert.equal(rememberedResponse.sessionMaxAgeMs, 7 * 24 * 60 * 60 * 1000);
});
test('accept invite page renders a valid invitation with password requirements', async () => {
const handlers = {};
let rendered = null;
const app = {
get(path, ...routeHandlers) { handlers['GET ' + path] = routeHandlers; },
post(path, ...routeHandlers) { handlers['POST ' + path] = routeHandlers; }
};
registerAuthRoutes(app, {
pool: {
async query(sql) {
if (sql.includes('a_user_invitations')) return [[{ email: 'person@example.com', name: 'Person' }]];
return [[{ setting_key: 'security.password_min_length', setting_value: '12' }]];
}
},
pages: {
renderAcceptInvitePage(...args) { rendered = args; return 'invite form'; }
},
hashSessionToken: (value) => 'hash:' + value
});
const res = { send(value) { this.body = value; }, status() { return this; } };
await handlers['GET /accept-invite'][0]({ query: { token: 'invite-token' } }, res, (error) => { throw error; });
assert.equal(res.body, 'invite form');
assert.equal(rendered[0], '');
assert.equal(rendered[1], 'invite-token');
assert.equal(rendered[2], 'person@example.com');
assert.equal(rendered[3], 'Person');
assert.equal(rendered[4].minimumLength, 12);
assert.equal(rendered[5], true);
});
test('accept invite creates the account, assigns roles, and consumes the invitation', async () => {
const handlers = {};
const queries = [];
let committed = false;
let syncedRoles = null;
const app = {
get(path, ...routeHandlers) { handlers['GET ' + path] = routeHandlers; },
post(path, ...routeHandlers) { handlers['POST ' + path] = routeHandlers; }
};
const connection = {
async query(sql, args) {
queries.push({ sql, args });
if (sql.includes('a_user_invitations')) return [[{ id: 8, email: 'person@example.com', name: 'Invited name', role_ids_json: '[2, 3]' }]];
if (sql.includes('SELECT id FROM a_users')) return [[]];
if (sql.includes('INSERT INTO a_users')) return [{ insertId: 21 }];
return [[]];
},
async beginTransaction() {},
async commit() { committed = true; },
async rollback() {},
release() {}
};
registerAuthRoutes(app, {
pool: {
async query() { return [[]]; },
async getConnection() { return connection; }
},
pages: {
renderAcceptInvitePage() { return ''; }
},
getConnection: async () => connection,
hashSessionToken: (value) => 'hash:' + value,
hashPassword: () => ({ hash: 'hash', salt: 'salt', iterations: 1000 }),
validatePasswordStrength: () => '',
rbacData: {
async syncUserRoles(_connection, userId, roleIds) { syncedRoles = { userId, roleIds }; }
}
});
const req = {
body: {
token: 'invite-token',
username: 'person',
name: 'Person',
password: 'ValidPass!1',
confirm_password: 'ValidPass!1'
}
};
const res = {
redirect(url) { this.redirectedTo = url; },
status() { return this; },
send() { return this; }
};
await handlers['POST /accept-invite'][0](req, res, (error) => { throw error; });
const insert = queries.find((query) => query.sql.includes('INSERT INTO a_users'));
const consumed = queries.find((query) => query.sql.includes('UPDATE a_user_invitations'));
assert.deepEqual(insert.args, ['Person', 'person', 'person@example.com', 'hash', 'salt', 1000]);
assert.deepEqual(syncedRoles, { userId: 21, roleIds: [2, 3] });
assert.deepEqual(consumed.args, [8]);
assert.equal(committed, true);
assert.equal(res.redirectedTo, '/login?message=Account%20created.%20You%20can%20now%20sign%20in.');
});
test('accept invite rejects an invalid token without creating an account', async () => {
const handlers = {};
let renderedMessage = '';
const app = {
get(path, ...routeHandlers) { handlers['GET ' + path] = routeHandlers; },
post(path, ...routeHandlers) { handlers['POST ' + path] = routeHandlers; }
};
const connection = {
async query(sql) {
if (sql.includes('a_user_invitations')) return [[]];
return [[]];
},
async rollback() {},
release() {}
};
registerAuthRoutes(app, {
pool: { async query() { return [[]]; } },
pages: { renderAcceptInvitePage(message) { renderedMessage = message; return ''; } },
getConnection: async () => connection,
hashSessionToken: (value) => 'hash:' + value
});
const res = { status() { return this; }, send() {} };
await handlers['POST /accept-invite'][0]({ body: { token: 'expired-token' } }, res, (error) => { throw error; });
assert.equal(renderedMessage, 'This invitation is invalid or has expired.');
});