Read calendar event creators from the admin module, and archive the old ones (#14)
Jenkins Production Deployment
Jenkins Production Deployment
Reviewed-on: #14 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user