forked from lunanting/cangku
209 lines
7.1 KiB
JavaScript
209 lines
7.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
OFFLINE_AFTER_SECONDS,
|
|
aggregateDesktopPresence,
|
|
ensureDesktopPresenceSchema,
|
|
isPresenceOnline,
|
|
listOnlineDesktopUsers,
|
|
markDesktopOffline,
|
|
normalizeHeartbeatPayload,
|
|
recordDesktopHeartbeat,
|
|
resetDesktopPresenceStateForTests,
|
|
} = require('../src/services/desktopPresenceService');
|
|
|
|
const CLIENT_ID_A = '11111111-1111-4111-8111-111111111111';
|
|
const CLIENT_ID_B = '22222222-2222-4222-8222-222222222222';
|
|
|
|
test('desktop presence stays online at 120 seconds and expires immediately after the boundary', () => {
|
|
const now = new Date('2026-08-18T04:00:00.000Z');
|
|
const exactBoundary = new Date(now.getTime() - OFFLINE_AFTER_SECONDS * 1000);
|
|
const overBoundary = new Date(exactBoundary.getTime() - 1);
|
|
|
|
assert.equal(isPresenceOnline({ lastSeenAt: exactBoundary }, now), true);
|
|
assert.equal(isPresenceOnline({ lastSeenAt: overBoundary }, now), false);
|
|
assert.equal(isPresenceOnline({ lastSeenAt: now, offlineAt: now }, now), false);
|
|
});
|
|
|
|
test('desktop presence aggregates multiple online devices without overwriting the user', () => {
|
|
const now = new Date('2026-08-18T04:00:00.000Z');
|
|
const result = aggregateDesktopPresence([
|
|
{
|
|
userId: 7,
|
|
clientId: CLIENT_ID_A,
|
|
clientVersion: '0.0.10',
|
|
deviceName: 'OPS-01',
|
|
name: '香茗',
|
|
departmentName: '运营中心',
|
|
onlineSinceAt: new Date('2026-08-18T03:30:00.000Z'),
|
|
lastSeenAt: new Date('2026-08-18T03:59:55.000Z'),
|
|
},
|
|
{
|
|
userId: 7,
|
|
clientId: CLIENT_ID_B,
|
|
clientVersion: '0.0.11',
|
|
deviceName: 'OPS-02',
|
|
name: '香茗',
|
|
departmentName: '运营中心',
|
|
onlineSinceAt: new Date('2026-08-18T03:50:00.000Z'),
|
|
lastSeenAt: new Date('2026-08-18T03:59:58.000Z'),
|
|
},
|
|
], now);
|
|
|
|
assert.equal(result.onlineUserCount, 1);
|
|
assert.equal(result.onlineDeviceCount, 2);
|
|
assert.equal(result.users[0].activeDeviceCount, 2);
|
|
assert.deepEqual(result.users[0].clientVersions, ['0.0.11', '0.0.10']);
|
|
assert.equal(result.users[0].onlineDurationSeconds, 1800);
|
|
});
|
|
|
|
test('desktop presence retains stale and explicitly offline devices as history', () => {
|
|
const now = new Date('2026-08-18T04:00:00.000Z');
|
|
const result = aggregateDesktopPresence([
|
|
{
|
|
userId: 7,
|
|
clientId: CLIENT_ID_A,
|
|
name: '香茗',
|
|
onlineSinceAt: new Date('2026-08-18T03:50:00.000Z'),
|
|
lastSeenAt: new Date('2026-08-18T03:59:55.000Z'),
|
|
},
|
|
{
|
|
userId: 7,
|
|
clientId: CLIENT_ID_B,
|
|
name: '香茗',
|
|
onlineSinceAt: new Date('2026-08-18T02:00:00.000Z'),
|
|
lastSeenAt: new Date('2026-08-18T03:00:00.000Z'),
|
|
},
|
|
{
|
|
userId: 8,
|
|
clientId: '33333333-3333-4333-8333-333333333333',
|
|
name: '胖大海',
|
|
onlineSinceAt: new Date('2026-08-18T03:10:00.000Z'),
|
|
lastSeenAt: new Date('2026-08-18T03:20:00.000Z'),
|
|
offlineAt: new Date('2026-08-18T03:20:01.000Z'),
|
|
},
|
|
], now);
|
|
|
|
assert.equal(result.knownUserCount, 2);
|
|
assert.equal(result.knownDeviceCount, 3);
|
|
assert.equal(result.onlineUserCount, 1);
|
|
assert.equal(result.onlineDeviceCount, 1);
|
|
assert.equal(result.offlineUserCount, 1);
|
|
assert.equal(result.offlineDeviceCount, 2);
|
|
assert.equal(result.users[0].name, '香茗');
|
|
assert.equal(result.users[0].devices[0].isOnline, true);
|
|
assert.equal(result.users[0].devices[1].isOnline, false);
|
|
assert.equal(result.users[0].devices[1].sessionDurationSeconds, 3600);
|
|
assert.equal(result.users[1].devices[0].isOnline, false);
|
|
});
|
|
|
|
test('desktop presence listing reads historical rows instead of filtering them in SQL', async () => {
|
|
resetDesktopPresenceStateForTests();
|
|
const queries = [];
|
|
const db = {
|
|
async query(sql) {
|
|
queries.push(sql);
|
|
return [{
|
|
userId: 7,
|
|
clientId: CLIENT_ID_A,
|
|
name: '香茗',
|
|
onlineSinceAt: new Date('2026-08-18T02:00:00.000Z'),
|
|
lastSeenAt: new Date('2026-08-18T03:00:00.000Z'),
|
|
}];
|
|
},
|
|
};
|
|
|
|
const result = await listOnlineDesktopUsers({
|
|
db,
|
|
now: new Date('2026-08-18T04:00:00.000Z'),
|
|
});
|
|
|
|
assert.equal(result.knownDeviceCount, 1);
|
|
assert.equal(result.onlineDeviceCount, 0);
|
|
assert.equal(result.offlineDeviceCount, 1);
|
|
assert.doesNotMatch(queries[0], /WHERE p\.offline_at|p\.last_seen_at >=/);
|
|
resetDesktopPresenceStateForTests();
|
|
});
|
|
|
|
test('heartbeat payload requires a UUID and caps untrusted metadata', () => {
|
|
assert.throws(() => normalizeHeartbeatPayload({ clientId: 'not-a-uuid' }), /设备标识无效/);
|
|
const payload = normalizeHeartbeatPayload({
|
|
clientId: CLIENT_ID_A,
|
|
clientName: 'x'.repeat(150),
|
|
});
|
|
assert.equal(payload.clientName.length, 100);
|
|
});
|
|
|
|
test('an existing presence table does not require CREATE permission at startup', async () => {
|
|
resetDesktopPresenceStateForTests();
|
|
const queries = [];
|
|
const db = {
|
|
async query(sql) {
|
|
queries.push(sql);
|
|
if (sql.includes('information_schema.tables')) return [{ present: 1 }];
|
|
throw new Error('CREATE must not run when the table already exists');
|
|
},
|
|
};
|
|
|
|
assert.equal(await ensureDesktopPresenceSchema(db), true);
|
|
assert.equal(queries.length, 1);
|
|
assert.match(queries[0], /information_schema\.tables/);
|
|
resetDesktopPresenceStateForTests();
|
|
});
|
|
|
|
test('heartbeat and explicit logout are scoped by authenticated user and client', async () => {
|
|
resetDesktopPresenceStateForTests();
|
|
const queries = [];
|
|
const db = {
|
|
async query(sql, params) {
|
|
queries.push({ sql, params });
|
|
return [];
|
|
},
|
|
};
|
|
const now = new Date('2026-08-18T04:00:00.000Z');
|
|
|
|
await recordDesktopHeartbeat({
|
|
userId: '7',
|
|
payload: { clientId: CLIENT_ID_A, clientVersion: '0.0.10' },
|
|
ipAddress: '127.0.0.1',
|
|
now,
|
|
}, { db });
|
|
await markDesktopOffline({ userId: '7', clientId: CLIENT_ID_A, now }, { db });
|
|
|
|
assert.equal(queries.length, 2);
|
|
assert.equal(queries[0].params[0], '7');
|
|
assert.equal(queries[0].params[1], CLIENT_ID_A);
|
|
assert.deepEqual(queries[1].params.slice(-2), ['7', CLIENT_ID_A]);
|
|
resetDesktopPresenceStateForTests();
|
|
});
|
|
|
|
test('in-memory presence remains available when the system database cannot create the table', async () => {
|
|
resetDesktopPresenceStateForTests();
|
|
const db = {
|
|
async query() {
|
|
const error = new Error('table is unavailable');
|
|
error.code = 'ER_NO_SUCH_TABLE';
|
|
throw error;
|
|
},
|
|
};
|
|
const now = new Date('2026-08-18T04:00:00.000Z');
|
|
|
|
await recordDesktopHeartbeat({
|
|
userId: '7',
|
|
identity: { name: '香茗', username: 'xiangming', departmentName: '运营中心' },
|
|
payload: { clientId: CLIENT_ID_A, clientVersion: '0.0.10', deviceName: 'OPS-01' },
|
|
now,
|
|
}, { db });
|
|
const online = await require('../src/services/desktopPresenceService').listOnlineDesktopUsers({ db, now });
|
|
|
|
assert.equal(online.onlineUserCount, 1);
|
|
assert.equal(online.users[0].name, '香茗');
|
|
assert.equal(online.users[0].devices[0].deviceName, 'OPS-01');
|
|
|
|
await markDesktopOffline({ userId: '7', clientId: CLIENT_ID_A, now }, { db });
|
|
const offline = await require('../src/services/desktopPresenceService').listOnlineDesktopUsers({ db, now });
|
|
assert.equal(offline.onlineUserCount, 0);
|
|
resetDesktopPresenceStateForTests();
|
|
});
|