13a0c07d1b
Jenkins Production Deployment
Reviewed-on: #14 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
192 lines
6.6 KiB
TypeScript
192 lines
6.6 KiB
TypeScript
import {describe, expect, it, vi, beforeEach} from 'vitest';
|
|
|
|
const connection = {
|
|
query: vi.fn(),
|
|
execute: vi.fn(),
|
|
beginTransaction: vi.fn(),
|
|
commit: vi.fn(),
|
|
rollback: vi.fn(),
|
|
end: vi.fn()
|
|
};
|
|
|
|
vi.mock('../../src/models/calendar/Calendar.db.js', () => ({
|
|
NachklangCalendarDB: {getConnection: vi.fn(async () => connection)}
|
|
}));
|
|
|
|
vi.mock('../../src/models/admin/users/users.admin.service.js', () => ({
|
|
findDisplayNames: vi.fn()
|
|
}));
|
|
|
|
import {NachklangCalendarDB} from '../../src/models/calendar/Calendar.db.js';
|
|
import * as AdminUsersService from '../../src/models/admin/users/users.admin.service.js';
|
|
import * as EventService from '../../src/models/calendar/events/events.service.js';
|
|
|
|
/**
|
|
* Step 3 of docs/calendar-auth-migration.md. The property under test is that
|
|
* an event's creator resolves from whichever of its three possible sources is
|
|
* strongest - the live admin name, then the snapshot from migration 002, then
|
|
* the legacy join - and that a failure to reach the admin database costs a name
|
|
* rather than the whole response: the public calendar is read anonymously by
|
|
* the website and has never depended on the admin database being up.
|
|
*/
|
|
|
|
// One row of the shape the shared SELECT produces.
|
|
const row = (over: Record<string, unknown> = {}) => ({
|
|
event_id: 1,
|
|
calendar_id: 1,
|
|
uuid: 'uuid-1',
|
|
name: 'Konzert',
|
|
description: '',
|
|
start_datetime: new Date('2026-04-18T19:00:00Z'),
|
|
end_datetime: new Date('2026-04-18T21:00:00Z'),
|
|
created_date: new Date('2026-01-01T00:00:00Z'),
|
|
version_created_at: new Date('2026-01-02T00:00:00Z'),
|
|
location: '',
|
|
created_by_id: 7,
|
|
created_by_user_id: null,
|
|
created_by_name: null,
|
|
legacy_created_by_name: 'Legacy Person',
|
|
version_created_by_id: 7,
|
|
version_created_by_user_id: null,
|
|
version_created_by_name: null,
|
|
legacy_last_modified_by_name: 'Legacy Person',
|
|
url: '',
|
|
whole_day: 0,
|
|
repeat_frequency: '',
|
|
status: 'PUBLIC',
|
|
...over
|
|
});
|
|
|
|
/** getAllEvents runs the calendars lookup first, then the events query. */
|
|
const givenEvents = (...rows: unknown[]) => {
|
|
connection.query.mockReset();
|
|
connection.query
|
|
.mockResolvedValueOnce([{calendar_id: 1, includes_calendars: '[]'}])
|
|
.mockResolvedValueOnce(rows);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
connection.end.mockResolvedValue(undefined);
|
|
(NachklangCalendarDB.getConnection as any).mockResolvedValue(connection);
|
|
});
|
|
|
|
describe('creator names', () => {
|
|
it('uses the legacy join when the row has no admin id', async () => {
|
|
givenEvents(row());
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events[0].createdBy).toBe('Legacy Person');
|
|
expect(events[0].createdById).toBe(7);
|
|
expect(events[0].createdByUserId).toBeNull();
|
|
// Nothing to resolve, so the admin database is not touched at all.
|
|
expect(AdminUsersService.findDisplayNames).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('prefers the admin name when the row carries an admin id', async () => {
|
|
givenEvents(row({
|
|
created_by_user_id: 'admin-1',
|
|
version_created_by_user_id: 'admin-2'
|
|
}));
|
|
(AdminUsersService.findDisplayNames as any).mockResolvedValue(
|
|
new Map([['admin-1', 'Neue Person'], ['admin-2', 'Andere Person']])
|
|
);
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events[0].createdBy).toBe('Neue Person');
|
|
expect(events[0].lastModifiedBy).toBe('Andere Person');
|
|
// The legacy id is still reported during the transition.
|
|
expect(events[0].createdById).toBe(7);
|
|
expect(events[0].createdByUserId).toBe('admin-1');
|
|
});
|
|
|
|
it('prefers the snapshot over the legacy join', async () => {
|
|
givenEvents(row({
|
|
created_by_name: 'Archived Person',
|
|
version_created_by_name: 'Archived Person'
|
|
}));
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events[0].createdBy).toBe('Archived Person');
|
|
expect(events[0].lastModifiedBy).toBe('Archived Person');
|
|
});
|
|
|
|
it('prefers the live admin name over the snapshot', async () => {
|
|
// A renamed account has to win over an archive that was correct when it
|
|
// was taken - otherwise renaming someone would leave stale names behind.
|
|
givenEvents(row({created_by_user_id: 'admin-1', created_by_name: 'Archived Person'}));
|
|
(AdminUsersService.findDisplayNames as any).mockResolvedValue(new Map([['admin-1', 'Neue Person']]));
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events[0].createdBy).toBe('Neue Person');
|
|
});
|
|
|
|
it('keeps the snapshot when step 5 has removed the legacy join', async () => {
|
|
// What a post-step-5 row looks like: no legacy id, no join, snapshot only.
|
|
givenEvents(row({
|
|
created_by_id: null,
|
|
legacy_created_by_name: undefined,
|
|
legacy_last_modified_by_name: undefined,
|
|
created_by_name: 'Archived Person',
|
|
version_created_by_name: 'Archived Person'
|
|
}));
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events[0].createdBy).toBe('Archived Person');
|
|
expect(events[0].lastModifiedBy).toBe('Archived Person');
|
|
});
|
|
|
|
it('falls back to the legacy name when the admin account is gone', async () => {
|
|
givenEvents(row({created_by_user_id: 'deleted'}));
|
|
(AdminUsersService.findDisplayNames as any).mockResolvedValue(new Map());
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events[0].createdBy).toBe('Legacy Person');
|
|
});
|
|
|
|
it('resolves a mixed result set in a single lookup', async () => {
|
|
givenEvents(
|
|
row({event_id: 1}),
|
|
row({event_id: 2, created_by_user_id: 'admin-1', version_created_by_user_id: 'admin-1'}),
|
|
row({event_id: 3, created_by_user_id: 'admin-1', version_created_by_user_id: 'admin-1'})
|
|
);
|
|
(AdminUsersService.findDisplayNames as any).mockResolvedValue(new Map([['admin-1', 'Neue Person']]));
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events.map(e => e.createdBy)).toEqual(['Legacy Person', 'Neue Person', 'Neue Person']);
|
|
expect(AdminUsersService.findDisplayNames).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('still returns the events when the admin database is unreachable', async () => {
|
|
givenEvents(row({created_by_user_id: 'admin-1'}));
|
|
(AdminUsersService.findDisplayNames as any).mockRejectedValue(new Error('ECONNREFUSED'));
|
|
|
|
const events = await EventService.getAllEvents(1);
|
|
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].name).toBe('Konzert');
|
|
// Degrades to the legacy name rather than failing the request.
|
|
expect(events[0].createdBy).toBe('Legacy Person');
|
|
});
|
|
});
|
|
|
|
describe('status', () => {
|
|
it('is omitted from the public listing and present in the admin one', async () => {
|
|
givenEvents(row());
|
|
const publicEvents = await EventService.getAllEvents(1);
|
|
expect(publicEvents[0].status).toBeUndefined();
|
|
|
|
connection.query.mockReset();
|
|
connection.query.mockResolvedValueOnce([row()]);
|
|
const adminEvents = await EventService.getAllEventsAdmin(1);
|
|
expect(adminEvents[0].status).toBe('PUBLIC');
|
|
});
|
|
});
|