mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
5345b50d17
The two new test files mocked the db module and built VacayService by hand, which the test-mocks and new-service ratchets refuse for files without a baseline entry. The cases move back into vacay.service.test.ts and vacay.test.ts unchanged; VACAY-037 asks for a year no other case caches. The unrelated admin addon test change is reverted and two comments name the new provider.
672 lines
27 KiB
TypeScript
672 lines
27 KiB
TypeScript
/**
|
|
* Vacay integration tests.
|
|
* Covers VACAY-001 to VACAY-025.
|
|
*/
|
|
import { buildApp } from '../../src/bootstrap';
|
|
import { db as testDb } from '../../src/db/database';
|
|
import { VacayHolidayCalendars } from '../../src/db/entities/VacayHolidayCalendars.entity';
|
|
import { VacayService } from '../../src/nest/vacay/vacay.service';
|
|
import { authCookie } from '../helpers/auth';
|
|
import { createUser } from '../helpers/factories';
|
|
import type { FactoryOrm } from '../helpers/factories/context';
|
|
import { findRows } from '../helpers/factories/rows';
|
|
import { resetTestDb, resetRateLimits } from '../helpers/test-db';
|
|
import { MikroORM } from '@mikro-orm/core';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
|
|
import type { Application } from 'express';
|
|
import request from 'supertest';
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
|
|
// Prevent real HTTP calls (holiday API etc.)
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve([
|
|
{
|
|
date: '2025-01-01',
|
|
name: "New Year's Day",
|
|
countryCode: 'DE',
|
|
nationalHoliday: true,
|
|
subdivisionCodes: null,
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
|
|
// Stub VacayService.getCountries to avoid a real HTTP call to nager.at (the
|
|
// legacy path-level partial mock; the service is DI-native now, so spy on the
|
|
// prototype instead).
|
|
vi.spyOn(VacayService.prototype, 'getCountries').mockResolvedValue({
|
|
data: [
|
|
{ countryCode: 'DE', name: 'Germany' },
|
|
{ countryCode: 'FR', name: 'France' },
|
|
],
|
|
});
|
|
|
|
let nestApp: INestApplication;
|
|
let app: Application;
|
|
let orm: FactoryOrm;
|
|
|
|
beforeAll(async () => {
|
|
nestApp = await buildApp();
|
|
app = nestApp.getHttpAdapter().getInstance();
|
|
orm = nestApp.get(MikroORM);
|
|
});
|
|
|
|
/** The id of the holiday calendar created last. */
|
|
async function latestCalendarId(): Promise<number | undefined> {
|
|
const [latest] = await findRows(orm, VacayHolidayCalendars, {}, { id: 'desc' });
|
|
return latest?.id;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
resetTestDb(testDb);
|
|
await resetRateLimits(nestApp);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await nestApp.close();
|
|
testDb.close();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('Vacay plan', () => {
|
|
it('VACAY-001 — GET /api/addons/vacay/plan auto-creates plan on first access', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
const res = await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.plan).toBeDefined();
|
|
expect(res.body.plan.owner_id).toBe(user.id);
|
|
});
|
|
|
|
it('VACAY-001 — second GET returns same plan (no duplicate creation)', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
const res = await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.plan).toBeDefined();
|
|
});
|
|
|
|
it('VACAY-002 — PUT /api/addons/vacay/plan updates plan settings', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
// Ensure plan exists
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app)
|
|
.put('/api/addons/vacay/plan')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ vacation_days: 30, carry_over_days: 5 });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Vacay years', () => {
|
|
it('VACAY-007 — POST /api/addons/vacay/years adds a year to the plan', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/years')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ year: 2025 });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.years).toBeDefined();
|
|
});
|
|
|
|
it('VACAY-025 — GET /api/addons/vacay/years lists years in plan', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app).get('/api/addons/vacay/years').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.years)).toBe(true);
|
|
expect(res.body.years.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('VACAY-008 — DELETE /api/addons/vacay/years/:year removes year', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2026 });
|
|
|
|
const res = await request(app).delete('/api/addons/vacay/years/2026').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.years).toBeDefined();
|
|
});
|
|
|
|
it('VACAY-011 — PUT /api/addons/vacay/stats/:year updates allowance', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app)
|
|
.put('/api/addons/vacay/stats/2025')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ vacation_days: 28 });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Vacay entries', () => {
|
|
it('VACAY-003 — POST /api/addons/vacay/entries/toggle marks a day as vacation', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/entries/toggle')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ date: '2025-06-16', year: 2025, type: 'vacation' });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('VACAY-004 — POST /api/addons/vacay/entries/toggle on a weekend is rejected when block_weekends is on', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
// 2025-06-21 is a Saturday and plans default to block_weekends = 1 (I-02)
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/entries/toggle')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ date: '2025-06-21', year: 2025, type: 'vacation' });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body).toEqual({ error: 'Weekend days are blocked on this plan' });
|
|
});
|
|
|
|
it('VACAY-006 — GET /api/addons/vacay/entries/:year returns vacation entries', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app).get('/api/addons/vacay/entries/2025').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.entries)).toBe(true);
|
|
});
|
|
|
|
it('VACAY-009 — GET /api/addons/vacay/stats/:year returns stats for year', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app).get('/api/addons/vacay/stats/2025').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toHaveProperty('stats');
|
|
});
|
|
});
|
|
|
|
describe('Vacay color', () => {
|
|
it('VACAY-024 — PUT /api/addons/vacay/color sets user color in plan', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app)
|
|
.put('/api/addons/vacay/color')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ color: '#3b82f6' });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Vacay invite flow', () => {
|
|
it('VACAY-022 — cannot invite yourself', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/invite')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ user_id: user.id });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('VACAY-016 — send invite to another user', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: invitee } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/invite')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ user_id: invitee.id });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
|
|
it('VACAY-023 — GET /api/addons/vacay/available-users returns users who can be invited', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app).get('/api/addons/vacay/available-users').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.users)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Vacay holidays', () => {
|
|
it('VACAY-014 — GET /api/addons/vacay/holidays/countries returns available countries', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
const res = await request(app).get('/api/addons/vacay/holidays/countries').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body)).toBe(true);
|
|
});
|
|
|
|
it('VACAY-012 — POST /api/addons/vacay/plan/holiday-calendars adds a holiday calendar', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/plan/holiday-calendars')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ region: 'DE', label: 'Germany Holidays' });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Vacay dissolve plan', () => {
|
|
it('VACAY-020 — POST /api/addons/vacay/dissolve removes user from plan', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app).post('/api/addons/vacay/dissolve').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Vacay holiday calendar CRUD', () => {
|
|
it('VACAY-026 — PUT /plan/holiday-calendars/:id updates an existing calendar', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
// Create a calendar first
|
|
const createRes = await request(app)
|
|
.post('/api/addons/vacay/plan/holiday-calendars')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ region: 'US', label: 'US Holidays' });
|
|
expect(createRes.status).toBe(200);
|
|
const calId = createRes.body.plan?.holiday_calendars?.at(-1)?.id ?? (await latestCalendarId());
|
|
|
|
const res = await request(app)
|
|
.put(`/api/addons/vacay/plan/holiday-calendars/${calId}`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ label: 'Updated Label', color: '#ff0000' });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('VACAY-027 — DELETE /plan/holiday-calendars/:id removes the calendar', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const createRes = await request(app)
|
|
.post('/api/addons/vacay/plan/holiday-calendars')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ region: 'FR', label: 'French Holidays' });
|
|
expect(createRes.status).toBe(200);
|
|
const calId = await latestCalendarId();
|
|
|
|
const res = await request(app)
|
|
.delete(`/api/addons/vacay/plan/holiday-calendars/${calId}`)
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
|
|
it('VACAY-027b — DELETE /plan/holiday-calendars/:id non-existent returns 404', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
|
|
const res = await request(app)
|
|
.delete('/api/addons/vacay/plan/holiday-calendars/99999')
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('Vacay invite full flow', () => {
|
|
it('VACAY-028 — POST /invite/accept joins the invitee to the owner plan', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: invitee } = createUser(testDb);
|
|
|
|
// Owner creates plan
|
|
const planRes = await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
const planId = planRes.body.plan.id;
|
|
|
|
// Owner invites invitee
|
|
await request(app)
|
|
.post('/api/addons/vacay/invite')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ user_id: invitee.id });
|
|
|
|
// Invitee accepts
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/invite/accept')
|
|
.set('Cookie', authCookie(invitee.id))
|
|
.send({ plan_id: planId });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
|
|
it('VACAY-029 — POST /invite/decline removes the pending invite', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: invitee } = createUser(testDb);
|
|
|
|
const planRes = await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
const planId = planRes.body.plan.id;
|
|
|
|
await request(app)
|
|
.post('/api/addons/vacay/invite')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ user_id: invitee.id });
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/invite/decline')
|
|
.set('Cookie', authCookie(invitee.id))
|
|
.send({ plan_id: planId });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
|
|
it('VACAY-030 — POST /invite/cancel removes the pending invite from owner side', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: invitee } = createUser(testDb);
|
|
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
|
|
await request(app)
|
|
.post('/api/addons/vacay/invite')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ user_id: invitee.id });
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/invite/cancel')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ user_id: invitee.id });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Vacay company holidays', () => {
|
|
it('VACAY-032 — POST /entries/company-holiday toggles a company holiday', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/entries/company-holiday')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ date: '2025-12-25', note: 'Christmas' });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('VACAY-032b — POST /entries/company-holiday takes a half day and refuses any other size (#2439)', async () => {
|
|
const { user } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(user.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(user.id)).send({ year: 2025 });
|
|
const half = await request(app)
|
|
.post('/api/addons/vacay/entries/company-holiday')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ date: '2025-12-24', fraction: 0.5 });
|
|
expect(half.status).toBe(200);
|
|
expect(half.body).toMatchObject({ action: 'added', fraction: 0.5 });
|
|
const odd = await request(app)
|
|
.post('/api/addons/vacay/entries/company-holiday')
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ date: '2025-12-24', fraction: 0.25 });
|
|
expect(odd.status).toBe(400);
|
|
});
|
|
|
|
it('VACAY-033 — POST /entries/toggle with target_user_id not in plan returns 403', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: outsider } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(owner.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/entries/toggle')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ date: '2025-07-14', target_user_id: outsider.id });
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
describe('Vacay stats restrictions', () => {
|
|
it('VACAY-034 — PUT /stats/:year for user not in plan returns 403', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: outsider } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(owner.id)).send({ year: 2025 });
|
|
|
|
const res = await request(app)
|
|
.put('/api/addons/vacay/stats/2025')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ vacation_days: 25, target_user_id: outsider.id });
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
describe('Vacay color restriction', () => {
|
|
it('VACAY-036 — PUT /color with target_user_id not in plan returns 403', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: outsider } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
|
|
const res = await request(app)
|
|
.put('/api/addons/vacay/color')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ color: '#ff0000', target_user_id: outsider.id });
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
describe('Vacay toggle entry for plan member', () => {
|
|
it('VACAY-038 — POST /entries/toggle with target_user_id in plan toggles their entry', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: invitee } = createUser(testDb);
|
|
|
|
const planRes = await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(owner.id));
|
|
const planId = planRes.body.plan.id;
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(owner.id)).send({ year: 2025 });
|
|
|
|
// Invite and accept so invitee is in the plan
|
|
await request(app)
|
|
.post('/api/addons/vacay/invite')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ user_id: invitee.id });
|
|
await request(app)
|
|
.post('/api/addons/vacay/invite/accept')
|
|
.set('Cookie', authCookie(invitee.id))
|
|
.send({ plan_id: planId });
|
|
|
|
// Owner toggles an entry for the invitee (who is now in the plan)
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/entries/toggle')
|
|
.set('Cookie', authCookie(owner.id))
|
|
.send({ date: '2025-06-10', target_user_id: invitee.id });
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Vacay read-only calendar shares', () => {
|
|
it('VACAY-039 — POST /shares shares the calendar with another user', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/shares')
|
|
.set('Cookie', authCookie(a.id))
|
|
.send({ user_id: b.id });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
|
|
it('VACAY-040 — GET /shares lists the share as outgoing for A and incoming for B', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
await request(app).post('/api/addons/vacay/shares').set('Cookie', authCookie(a.id)).send({ user_id: b.id });
|
|
|
|
const aRes = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(a.id));
|
|
expect(aRes.status).toBe(200);
|
|
expect(aRes.body.outgoing).toHaveLength(1);
|
|
expect(aRes.body.outgoing[0].user_id).toBe(b.id);
|
|
expect(aRes.body.incoming).toHaveLength(0);
|
|
|
|
const bRes = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(b.id));
|
|
expect(bRes.status).toBe(200);
|
|
expect(bRes.body.incoming).toHaveLength(1);
|
|
expect(bRes.body.incoming[0].owner_id).toBe(a.id);
|
|
expect(bRes.body.incoming[0].hidden).toBe(false);
|
|
expect(bRes.body.outgoing).toHaveLength(0);
|
|
});
|
|
|
|
it('VACAY-041 — GET /shares/calendars/:year exposes A entries and company holidays to B', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
await request(app).post('/api/addons/vacay/years').set('Cookie', authCookie(a.id)).send({ year: 2025 });
|
|
await request(app)
|
|
.put('/api/addons/vacay/plan')
|
|
.set('Cookie', authCookie(a.id))
|
|
.send({ company_holidays_enabled: true });
|
|
await request(app)
|
|
.post('/api/addons/vacay/entries/toggle')
|
|
.set('Cookie', authCookie(a.id))
|
|
.send({ date: '2025-06-16' });
|
|
await request(app)
|
|
.post('/api/addons/vacay/entries/company-holiday')
|
|
.set('Cookie', authCookie(a.id))
|
|
.send({ date: '2025-12-25', note: 'Christmas' });
|
|
await request(app).post('/api/addons/vacay/shares').set('Cookie', authCookie(a.id)).send({ user_id: b.id });
|
|
|
|
const res = await request(app).get('/api/addons/vacay/shares/calendars/2025').set('Cookie', authCookie(b.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.calendars).toHaveLength(1);
|
|
const cal = res.body.calendars[0];
|
|
expect(cal.owner_id).toBe(a.id);
|
|
expect(cal.entries.map((e: { date: string }) => e.date)).toContain('2025-06-16');
|
|
expect(cal.companyHolidays.map((h: { date: string }) => h.date)).toContain('2025-12-25');
|
|
});
|
|
|
|
it('VACAY-042 — PUT /shares/:id lets B hide the shared calendar', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
await request(app).post('/api/addons/vacay/shares').set('Cookie', authCookie(a.id)).send({ user_id: b.id });
|
|
const list = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(b.id));
|
|
const shareId = list.body.incoming[0].id;
|
|
|
|
const res = await request(app)
|
|
.put(`/api/addons/vacay/shares/${shareId}`)
|
|
.set('Cookie', authCookie(b.id))
|
|
.send({ hidden: true });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
|
|
const after = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(b.id));
|
|
expect(after.body.incoming[0].hidden).toBe(true);
|
|
});
|
|
|
|
it('VACAY-043 — DELETE /shares/:id lets B remove the share', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
await request(app).post('/api/addons/vacay/shares').set('Cookie', authCookie(a.id)).send({ user_id: b.id });
|
|
const list = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(b.id));
|
|
const shareId = list.body.incoming[0].id;
|
|
|
|
const res = await request(app).delete(`/api/addons/vacay/shares/${shareId}`).set('Cookie', authCookie(b.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
|
|
const bAfter = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(b.id));
|
|
expect(bAfter.body.incoming).toHaveLength(0);
|
|
const aAfter = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(a.id));
|
|
expect(aAfter.body.outgoing).toHaveLength(0);
|
|
});
|
|
|
|
it('VACAY-044 — PUT/DELETE on a foreign share id returns 404', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
const { user: c } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
await request(app).post('/api/addons/vacay/shares').set('Cookie', authCookie(a.id)).send({ user_id: b.id });
|
|
const list = await request(app).get('/api/addons/vacay/shares').set('Cookie', authCookie(b.id));
|
|
const shareId = list.body.incoming[0].id;
|
|
|
|
const put = await request(app)
|
|
.put(`/api/addons/vacay/shares/${shareId}`)
|
|
.set('Cookie', authCookie(c.id))
|
|
.send({ hidden: true });
|
|
expect(put.status).toBe(404);
|
|
|
|
const del = await request(app).delete(`/api/addons/vacay/shares/${shareId}`).set('Cookie', authCookie(c.id));
|
|
expect(del.status).toBe(404);
|
|
});
|
|
|
|
it('VACAY-045 — sharing with the same user twice returns 400', async () => {
|
|
const { user: a } = createUser(testDb);
|
|
const { user: b } = createUser(testDb);
|
|
await request(app).get('/api/addons/vacay/plan').set('Cookie', authCookie(a.id));
|
|
await request(app).post('/api/addons/vacay/shares').set('Cookie', authCookie(a.id)).send({ user_id: b.id });
|
|
|
|
const res = await request(app)
|
|
.post('/api/addons/vacay/shares')
|
|
.set('Cookie', authCookie(a.id))
|
|
.send({ user_id: b.id });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toBe('Already shared');
|
|
});
|
|
});
|
|
|
|
describe('Vacay public holidays', () => {
|
|
it('VACAY-035 — returns 502 when the external API fetch fails', async () => {
|
|
const { user } = createUser(testDb);
|
|
vi.mocked(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
|
|
|
|
const res = await request(app).get('/api/addons/vacay/holidays/2099/ZZ').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(502);
|
|
});
|
|
|
|
it('VACAY-037 — adapts a Nager v4 record when the external API succeeds', async () => {
|
|
const { user } = createUser(testDb);
|
|
vi.mocked(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve([
|
|
{
|
|
date: '2025-05-01',
|
|
name: 'Labour Day',
|
|
nationalHoliday: true,
|
|
subdivisionCodes: null,
|
|
},
|
|
]),
|
|
});
|
|
|
|
const res = await request(app).get('/api/addons/vacay/holidays/2087/AT').set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual([
|
|
{
|
|
date: '2025-05-01',
|
|
name: 'Labour Day',
|
|
localName: 'Labour Day',
|
|
global: true,
|
|
counties: null,
|
|
},
|
|
]);
|
|
});
|
|
});
|