Files
fredy/test/services/jobs/jobExecutionService.test.js
2025-12-18 19:16:28 +01:00

125 lines
3.8 KiB
JavaScript

/*
* Copyright (c) 2025 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import { expect } from 'chai';
import esmock from 'esmock';
import { EventEmitter } from 'node:events';
describe('services/jobs/jobExecutionService', () => {
/** @type {EventEmitter} */
let bus;
let calls;
let state;
async function initService() {
const root = (await import('node:path')).resolve('.');
const svcPath = root + '/lib/services/jobs/jobExecutionService.js';
const busPath = root + '/lib/services/events/event-bus.js';
const jobStoragePath = root + '/lib/services/storage/jobStorage.js';
const userStoragePath = root + '/lib/services/storage/userStorage.js';
const brokerPath = root + '/lib/services/sse/sse-broker.js';
const utilsPath = root + '/lib/utils.js';
const loggerPath = root + '/lib/services/logger.js';
// esmock the service with all its collaborators
const mod = await esmock(
svcPath,
{},
{
[busPath]: { bus },
[jobStoragePath]: {
getJob: (id) => state.jobsById[id] || null,
getJobs: () => state.jobsList.slice(),
},
[userStoragePath]: {
getUsers: () => state.users.slice(),
getUser: (id) => state.users.find((u) => u.id === id) || null,
},
[brokerPath]: {
sendToUsers: (...args) => calls.sent.push(args),
},
[utilsPath]: {
duringWorkingHoursOrNotSet: () => false, // avoid startup run
},
[loggerPath]: {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
},
[root + '/lib/services/jobs/run-state.js']: {
isRunning: () => false,
markRunning: (id) => {
calls.markRunning.push(id);
return true;
},
markFinished: () => {},
},
},
);
// call initializer with minimal deps
mod.initJobExecutionService({ providers: [], settings: { demoMode: false }, intervalMs: 0 });
return mod;
}
beforeEach(() => {
bus = new EventEmitter();
calls = { sent: [], markRunning: [] };
state = {
jobsById: {},
jobsList: [],
users: [],
};
});
it('forwards SSE jobStatus to owner, shared users and admins', async () => {
state.jobsById['j1'] = { id: 'j1', userId: 'owner1', shared_with_user: ['u2'] };
state.users = [
{ id: 'a1', isAdmin: true },
{ id: 'owner1', isAdmin: false },
{ id: 'u2', isAdmin: false },
];
await initService();
bus.emit('jobs:status', { jobId: 'j1', running: true });
expect(calls.sent.length).to.equal(1, 'sendToUsers should be called once');
const [recipients, event, data] = calls.sent[0];
expect(event).to.equal('jobStatus');
expect(data).to.deep.equal({ jobId: 'j1', running: true });
const got = new Set(recipients);
const expected = new Set(['owner1', 'u2', 'a1']);
expect(got).to.deep.equal(expected);
});
it('runs all jobs for admin; only own jobs for regular user', async () => {
state.jobsList = [
{ id: 'j1', enabled: true, userId: 'u1', provider: [] },
{ id: 'j2', enabled: true, userId: 'u2', provider: [] },
];
state.users = [
{ id: 'u1', isAdmin: false },
{ id: 'u2', isAdmin: false },
{ id: 'admin', isAdmin: true },
];
await initService();
// Non-admin: only own jobs
bus.emit('jobs:runAll', { userId: 'u1' });
// allow microtasks to flush
await new Promise((r) => setTimeout(r, 0));
expect(new Set(calls.markRunning)).to.deep.equal(new Set(['j1']));
// Admin: all jobs
calls.markRunning = [];
bus.emit('jobs:runAll', { userId: 'admin' });
await new Promise((r) => setTimeout(r, 0));
expect(new Set(calls.markRunning)).to.deep.equal(new Set(['j1', 'j2']));
});
});