Read calendar event creators from the admin module, and archive the old ones
Steps 1 and 3 of docs/calendar-auth-migration.md. The calendar is the last module still authenticating against its own users/sessions tables; this is the groundwork that lets step 4 swap it for the shared admin identity. An event now records its creator twice: created_by_id, the legacy INT into the calendar database's own users table, and created_by_user_id, the admin module's VARCHAR(36) id. The two live in different databases, so there is no foreign key and no join - a cross-schema reference would tie the schemas' lifecycles together, and the name is instead resolved through one lookup per result set against the admin database. The creator is only ever rendered as a name; nothing authorises on it. That is what makes the planned account backfill unnecessary - dropped by decision - and what makes the read degrade rather than fail: an admin id that no longer resolves falls back, and an unreachable admin database costs a name rather than the response. The public calendar is read anonymously by nachklang.art and has never depended on the admin database being up. Since there is no backfill, step 5 dropping the legacy users table would have erased the authorship of every pre-cutover event. Migration 002 brings that part of step 5 forward and snapshots the names onto the events themselves, so the data is safe well before the table holding it goes away. The same SELECT and row mapper existed in four copies; collapsed to one of each first, so the dual read is written once rather than four times. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import {describe, expect, it, vi, beforeEach} from 'vitest';
|
||||
|
||||
vi.mock('../../src/models/calendar/users/users.service.js', () => ({
|
||||
checkSession: vi.fn()
|
||||
}));
|
||||
|
||||
import * as UserService from '../../src/models/calendar/users/users.service.js';
|
||||
import * as CredentialService from '../../src/models/calendar/events/credentials.service.js';
|
||||
|
||||
/**
|
||||
* The public calendar is read anonymously by nachklang.art to show the next
|
||||
* upcoming event. That is a load-bearing property, not an accident: the
|
||||
* calendar auth migration (docs/calendar-auth-migration.md) keeps the shared
|
||||
* credentials only for the iCal export and moves everything else onto session
|
||||
* cookies, and the failure mode of getting that wrong is the public website
|
||||
* silently losing its events feed.
|
||||
*
|
||||
* So this pins both halves: public needs nothing, and the restricted calendars
|
||||
* still need something.
|
||||
*/
|
||||
describe('hasAccess', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
process.env.MEMBER_CREDENTIAL = 'member-secret';
|
||||
process.env.CHOIR_CREDENTIAL = 'choir-secret';
|
||||
process.env.MANAGEMENT_CREDENTIAL = 'management-secret';
|
||||
});
|
||||
|
||||
it('lets anyone read the public calendar with no session and no password', async () => {
|
||||
await expect(CredentialService.hasAccess('public', '', '', '', '127.0.0.1')).resolves.toBe(true);
|
||||
|
||||
// It must not even reach the session check - an anonymous read of the
|
||||
// public calendar should not depend on the users table being available.
|
||||
expect(UserService.checkSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['members', 'member-secret'],
|
||||
['choir', 'choir-secret'],
|
||||
['management', 'management-secret'],
|
||||
['birthdays', 'choir-secret']
|
||||
])('refuses %s without a credential and allows it with one', async (calendar, secret) => {
|
||||
(UserService.checkSession as any).mockResolvedValue(null);
|
||||
|
||||
await expect(CredentialService.hasAccess(calendar, '', '', '', '127.0.0.1')).resolves.toBe(false);
|
||||
await expect(CredentialService.hasAccess(calendar, '', '', 'wrong', '127.0.0.1')).resolves.toBe(false);
|
||||
await expect(CredentialService.hasAccess(calendar, '', '', secret, '127.0.0.1')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('refuses an unknown calendar outright', async () => {
|
||||
await expect(CredentialService.hasAccess('nope', '', '', 'member-secret', '127.0.0.1')).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user