feat(17-01): move TenderEmailConfig ownership from tenant to user
Alert-Postfach gehoert jetzt dem einzelnen Nutzer (userId @unique) statt
dem Mandanten (D-01) — ein zweiter Kollege desselben Mandanten kann sein
eigenes Postfach anbinden. tenantId bleibt denormalisiert (SMTP-Aufloesung,
Herkunftsmarkierung), wird auf create UND update mitgeschrieben.
- Handgeschriebene Migration (prisma migrate dev verweigert die
nicht-interaktive Shell): befuellt Bestandszeilen mit dem aeltesten
aktiven Administrator ihres Mandanten, entfernt verwaiste Zeilen ohne
Administrator, ersetzt die tenantId-Eindeutigkeit durch userId.
Lokal getestet (0 Bestandszeilen lokal und auf alpha — Zaehlung im
Task-1-Checkpoint), Index-Ergebnis verifiziert.
- TenderEmailConfigService.getConfigForApi/saveConfig auf userId als
Schluessel umgestellt; saveConfig nimmt {userId, tenantId}.
- TendersController: email-config-Routen von @Roles(ADMIN,SUPER_ADMIN)
auf @UseModule('tender-radar') umgestellt (Postfach ist jetzt
Nutzereinstellung); Route-Reihenfolge vor @Get(':id') unveraendert.
- Neue Seite /modules/tender-radar/my-sources ("Meine Quellen") mit dem
unveraenderten EmailAlertConfigForm; Hinweistext benennt D-05 (Tender
bleibt plattform-global — nur wer Quellen einspeist aendert sich).
- tenders.controller.spec.ts an neue Service-Signatur angepasst (Rule 3,
nicht im Plan gelistet, aber zum Kompilieren/Bestehen erforderlich).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,13 +104,17 @@ function makeFakeRssFeedService() {
|
||||
|
||||
/**
|
||||
* Fake TenderEmailConfigService for controller-level wiring tests (Plan
|
||||
* 14-03, D-06/D-07/CONFIG-02). Default stubs echo/return null; individual
|
||||
* tests override via `.mockResolvedValueOnce`/reassigning the mock.
|
||||
* 14-03, D-06/D-07/CONFIG-02; per-user ownership since Phase 17, Plan 01,
|
||||
* D-01). Default stubs echo/return null; individual tests override via
|
||||
* `.mockResolvedValueOnce`/reassigning the mock.
|
||||
*/
|
||||
function makeFakeEmailConfigService() {
|
||||
return {
|
||||
getConfigForApi: vi.fn(async (_tenantId: string) => null as any),
|
||||
saveConfig: vi.fn(async (_tenantId: string, dto: any) => ({ id: 'ec-1', ...dto })),
|
||||
getConfigForApi: vi.fn(async (_userId: string) => null as any),
|
||||
saveConfig: vi.fn(async (_ctx: { userId: string; tenantId: string }, dto: any) => ({
|
||||
id: 'ec-1',
|
||||
...dto,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -959,12 +963,13 @@ describe('TendersController — RSS-feeds admin CRUD (Plan 14-02, D-14/D-08)', (
|
||||
});
|
||||
});
|
||||
|
||||
describe('TendersController — email-config (Plan 14-03, per-tenant, D-06/D-07/T-14-03-05)', () => {
|
||||
it('GET /email-config resolves tenantId from the auth context and delegates to tenderEmailConfig.getConfigForApi(tenantId)', async () => {
|
||||
describe('TendersController — email-config (Plan 14-03, per-user since Phase 17 Plan 01 D-01, T-14-03-05/T-17-01)', () => {
|
||||
it('GET /email-config resolves userId from the auth context and delegates to tenderEmailConfig.getConfigForApi(userId)', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
|
||||
const emailConfigService = makeFakeEmailConfigService();
|
||||
emailConfigService.getConfigForApi.mockResolvedValueOnce({
|
||||
userId: 'u1',
|
||||
tenantId: 'tenant1',
|
||||
protocol: 'imap',
|
||||
hasPassword: true,
|
||||
@@ -981,11 +986,16 @@ describe('TendersController — email-config (Plan 14-03, per-tenant, D-06/D-07/
|
||||
|
||||
const result = await controller.getEmailConfig(makeFakeRequest('u1', 'tenant1'));
|
||||
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenCalledWith('tenant1');
|
||||
expect(result).toEqual({ tenantId: 'tenant1', protocol: 'imap', hasPassword: true });
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenCalledWith('u1');
|
||||
expect(result).toEqual({
|
||||
userId: 'u1',
|
||||
tenantId: 'tenant1',
|
||||
protocol: 'imap',
|
||||
hasPassword: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT /email-config delegates to tenderEmailConfig.saveConfig with tenantId from the auth context, never the body', async () => {
|
||||
it('PUT /email-config delegates to tenderEmailConfig.saveConfig with {userId, tenantId} from the auth context, never the body', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
|
||||
const emailConfigService = makeFakeEmailConfigService();
|
||||
@@ -1002,7 +1012,31 @@ describe('TendersController — email-config (Plan 14-03, per-tenant, D-06/D-07/
|
||||
const dto = { protocol: 'imap', encryption: 'ssl-tls', host: 'imap.example.test' } as any;
|
||||
await controller.saveEmailConfig(dto, makeFakeRequest('u1', 'tenant1'));
|
||||
|
||||
expect(emailConfigService.saveConfig).toHaveBeenCalledWith('tenant1', dto);
|
||||
expect(emailConfigService.saveConfig).toHaveBeenCalledWith(
|
||||
{ userId: 'u1', tenantId: 'tenant1' },
|
||||
dto,
|
||||
);
|
||||
});
|
||||
|
||||
it('two different users of the same tenant each resolve their own userId — never the other user\'s (T-17-01, IDOR)', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
|
||||
const emailConfigService = makeFakeEmailConfigService();
|
||||
const controller = new TendersController(
|
||||
prisma as any,
|
||||
scheduler,
|
||||
makeFakeTriageService() as any,
|
||||
makeFakeSavedSearchService() as any,
|
||||
makeFakeNotificationPrefService() as any,
|
||||
makeFakeRssFeedService() as any,
|
||||
emailConfigService as any,
|
||||
);
|
||||
|
||||
await controller.getEmailConfig(makeFakeRequest('user-a', 'tenant1'));
|
||||
await controller.getEmailConfig(makeFakeRequest('user-b', 'tenant1'));
|
||||
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenNthCalledWith(1, 'user-a');
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenNthCalledWith(2, 'user-b');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user