175 lines
7.4 KiB
JavaScript
175 lines
7.4 KiB
JavaScript
process.env.TZ = 'Europe/Berlin';
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const database = require('../src/config/database');
|
|
const repository = require('../src/repositories/WorklogRepository');
|
|
const service = require('../src/services/TimeEntryService');
|
|
const RealDate = Date;
|
|
|
|
function freezeTime(t, time = '2026-10-27T14:00:00') {
|
|
global.Date = class extends RealDate {
|
|
constructor(...args) { super(...(args.length ? args : [time])); }
|
|
static now() { return new RealDate(time).getTime(); }
|
|
};
|
|
t.after(() => { global.Date = RealDate; });
|
|
}
|
|
|
|
function mock(t, object, key, value) {
|
|
const original = object[key];
|
|
object[key] = value;
|
|
t.after(() => { object[key] = original; });
|
|
}
|
|
|
|
for (const [name, model, method] of [
|
|
['Krankheit', 'Sick', 'getSickByUserInDateRange'],
|
|
['Urlaub', 'Vacation', 'getVacationsByUserInDateRange']
|
|
]) {
|
|
test(`${name}: letzter Tag nach Zeitumstellung wird berücksichtigt`, async t => {
|
|
mock(t, database, 'getModels', () => ({
|
|
[model]: { findAll: async () => [{ first_day: '2026-10-06', last_day: '2026-10-26', vacation_type: 2 }] }
|
|
}));
|
|
const days = await repository[method](1, new Date('2026-10-26T00:00:00'), new Date('2026-11-01T23:59:59'));
|
|
assert.deepEqual(days.map(day => day.date.toISOString().slice(0, 10)), ['2026-10-26']);
|
|
});
|
|
test(`${name}: eintägige Abwesenheit an der Wochengrenze`, async t => {
|
|
mock(t, database, 'getModels', () => ({
|
|
[model]: { findAll: async () => [{ first_day: '2026-10-26', last_day: '2026-10-26', vacation_type: 1 }] }
|
|
}));
|
|
const days = await repository[method](1, new Date('2026-10-26T00:00:00'), new Date('2026-11-01T23:59:59'));
|
|
assert.equal(days.length, 1);
|
|
assert.equal(days[0].date.toISOString().slice(0, 10), '2026-10-26');
|
|
});
|
|
}
|
|
|
|
function setupWeek(t, { pause = false, sick = true, halfVacation = false } = {}) {
|
|
freezeTime(t);
|
|
const entries = [
|
|
{ id: 1, state: 'start work', tstamp: '2026-10-27 08:00:00' },
|
|
{ id: 2, state: 'start pause', tstamp: '2026-10-27 10:00:00', relatedTo_id: 1 },
|
|
...(!pause ? [{ id: 3, state: 'stop pause', tstamp: '2026-10-27 10:30:00', relatedTo_id: 2 }] : [])
|
|
];
|
|
mock(t, repository, 'findByDateRange', async () => entries);
|
|
mock(t, repository, 'findByUser', async () => entries);
|
|
mock(t, repository, 'getTimefixesByWorklogIds', async () => new Map());
|
|
mock(t, repository, 'getSickByUserInDateRange', async () => sick ? [{ date: new Date('2026-10-26'), sick_type: 'self' }] : []);
|
|
mock(t, repository, 'getVacationsByUserInDateRange', async () => halfVacation ? [{ date: new Date('2026-10-27'), half_day: 1 }] : []);
|
|
mock(t, repository, 'getHolidaysInDateRange', async () => []);
|
|
}
|
|
|
|
test('Wochenansicht: lokale Wochengrenzen, laufende Arbeit und Krankheit genau einmal', async t => {
|
|
setupWeek(t);
|
|
const week = await service.getWeekOverview(1);
|
|
assert.equal(week.weekStart, '2026-10-26');
|
|
assert.equal(week.weekEnd, '2026-11-01');
|
|
assert.equal(week.days[0].sick.hours, 8);
|
|
assert.equal(week.days[1].workBlocks[0].netWorkTime, '5:30');
|
|
assert.equal(week.weekTotal, '5:30');
|
|
assert.equal(week.nonWorkingTotal, '8:00');
|
|
assert.equal(week.totalAll, '13:30');
|
|
});
|
|
|
|
test('Wochenansicht: laufende Pause friert Nettozeit ein', async t => {
|
|
setupWeek(t, { pause: true });
|
|
const week = await service.getWeekOverview(1);
|
|
assert.equal(week.days[1].workBlocks[0].netWorkTime, '2:00');
|
|
assert.equal(week.weekTotal, '2:00');
|
|
});
|
|
|
|
test('Wochenansicht: Arbeit am halben Urlaubstag geht nicht verloren', async t => {
|
|
setupWeek(t, { sick: false, halfVacation: true });
|
|
const week = await service.getWeekOverview(1);
|
|
assert.equal(week.weekTotal, '5:30');
|
|
assert.equal(week.nonWorkingTotal, '4:00');
|
|
assert.equal(week.totalAll, '9:30');
|
|
});
|
|
|
|
test('Status: keine Fehlzeit für kranken Montag, Pausen angerechnet, laufende Wochenzeit', async t => {
|
|
setupWeek(t);
|
|
mock(t, database, 'getModels', () => ({
|
|
User: { findByPk: async () => ({ daily_hours: 8 }) },
|
|
Timewish: { findOne: async () => null, findAll: async () => [] }
|
|
}));
|
|
mock(t, service, '_calculateTotalOvertime', async () => ({ minutes: 349, formatted: '+5:49' }));
|
|
const stats = await service.getStatistics(1);
|
|
assert.equal(stats.currentlyWorked, '05:30:00');
|
|
assert.equal(stats.overtime, '+0:00');
|
|
assert.equal(stats.weekWorktime, '5:30');
|
|
assert.equal(stats.nonWorkingHours, '8:00');
|
|
assert.equal(stats.missingBreakMinutes, 0);
|
|
assert.equal(stats.open, '02:30:00');
|
|
assert.equal(stats.openForWeek, '2:30');
|
|
assert.equal(stats.adjustedEndTodayWeek, '16:30');
|
|
assert.equal(stats.totalOvertime, '+5:49');
|
|
});
|
|
|
|
// Repository nicht ersetzen: Genau dort muss die Datumsabfrage funktionieren.
|
|
const { Op } = require('sequelize');
|
|
const weekController = require('../src/controllers/WeekOverviewController');
|
|
const timeController = require('../src/controllers/TimeEntryController');
|
|
const actualFindByDateRange = repository.findByDateRange;
|
|
|
|
function responseRecorder() {
|
|
return {
|
|
statusCode: 200,
|
|
status(code) { this.statusCode = code; return this; },
|
|
json(body) { this.body = body; return this; }
|
|
};
|
|
}
|
|
|
|
function setupRepositoryFlow(t) {
|
|
setupWeek(t);
|
|
mock(t, repository, 'findByDateRange', actualFindByDateRange);
|
|
mock(t, database, 'getModels', () => ({
|
|
Worklog: {
|
|
findAll: async options => {
|
|
assert.equal(options.where.user_id, 1);
|
|
const range = options.where.tstamp[Op.between];
|
|
assert.equal(range[0].getTime(), new Date('2026-10-26T00:00:00').getTime());
|
|
assert.equal(range[1].getTime(), new Date('2026-11-01T23:59:59.999').getTime());
|
|
return [{ id: 1, state: 'start work', tstamp: '2026-10-27 08:00:00' }];
|
|
}
|
|
},
|
|
User: { findByPk: async () => ({ daily_hours: 8 }) },
|
|
Timewish: { findOne: async () => null, findAll: async () => [] }
|
|
}));
|
|
mock(t, service, '_calculateTotalOvertime', async () => ({ minutes: 349, formatted: '+5:49' }));
|
|
}
|
|
|
|
test('Wochen-API durchläuft echte Repository-Abfrage und liefert aktuelle Woche', async t => {
|
|
setupRepositoryFlow(t);
|
|
const res = responseRecorder();
|
|
await weekController.getWeekOverview({ user: { userId: 1 }, query: { weekOffset: '0' } }, res);
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.body.data.weekStart, '2026-10-26');
|
|
assert.equal(res.body.data.weekEnd, '2026-11-01');
|
|
assert.equal(res.body.data.days[0].sick.hours, 8);
|
|
});
|
|
|
|
test('Statistik-API durchläuft echte Repository-Abfrage und liefert Werte statt Fehlerobjekt', async t => {
|
|
setupRepositoryFlow(t);
|
|
const res = responseRecorder();
|
|
await timeController.getStats({ user: { userId: 1 } }, res);
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.body.overtime, '+0:00');
|
|
assert.equal(res.body.nonWorkingHours, '8:00');
|
|
assert.match(res.body.open, /^\d{2}:\d{2}:\d{2}$/);
|
|
});
|
|
|
|
test('Repository: Arbeitszeitpaare verwenden den vollständigen Datumsbereich', async t => {
|
|
const start = new Date('2026-10-26T00:00:00');
|
|
const end = new Date('2026-11-01T23:59:59.999');
|
|
let queried = false;
|
|
mock(t, database, 'getModels', () => ({ Worklog: { findAll: async options => {
|
|
queried = true;
|
|
assert.deepEqual(options.where.tstamp[Op.between], [start, end]);
|
|
return [
|
|
{ id: 1, state: 'start work', tstamp: '2026-10-27 08:00:00' },
|
|
{ id: 2, state: 'stop work', tstamp: '2026-10-27 16:00:00', relatedTo_id: 1 }
|
|
];
|
|
} } }));
|
|
const pairs = await repository.findPairsByUserInDateRange(1, start, end);
|
|
assert.equal(queried, true);
|
|
assert.equal(pairs.length, 1);
|
|
assert.equal(pairs[0].end_id, 2);
|
|
});
|