feat(laa-02): binde die fuenf Nutzer-CRUD-Dienste des Bereichs tenders an forTenant()
tender-saved-search.service.ts, tender-triage.service.ts, tender-notification-pref.service.ts und tender-email-config.service.ts laufen jetzt vollstaendig ueber forTenant() — vier neue Parameter (list, update, remove, listForUser, favoriteIds, getForUser, getConfigForApi, testConnection bekommen tenantId), die anwendungsseitige userId-Filterung bleibt unveraendert (Befund E: die Policies haben keine Benutzerdimension). tender-rss-feed.service.ts bindet nur createForUser (Zaehler + Anlage, beide ausschliesslich auf persoenlichen Zeilen); listForUser, createPlatform und remove bleiben mit Codekommentar bewusst ungebunden (WINDOWS #19 — eine gebundene plattformweite Zeile waere unter jedem Mandanten unsichtbar, ein gebundenes Einfuegen ohne Mandant wuerde abgewiesen). tender-notification-pref.service.ts und tender-email-config.service.ts uebersetzen eine P2002-Verletzung auf dem tenantlosen upsert-Schluessel (Befund F) in eine verstaendliche deutsche Meldung statt eines rohen Fehlers. tenders.controller.ts reicht tenantId an den acht betroffenen Aufrufstellen durch extractTriageContext() durch (kein neuer Aufloesungsweg); die drei RSS-Aufrufstellen bleiben unveraendert, da ihre Dienstmethoden nicht binden. Alle sieben angefassten Testdateien bekommen den Zwei-Client-Nachweis (__makeBoundClient ueber demselben Speicher) und Bindungstests je umgestellter Methode; tender-rss-feed.service.spec.ts zusaetzlich den Gegentest, dass die drei unveraendert bleibenden Pfade forTenant() NICHT aufrufen. Falsifiziert: ein probeweiser Rueckbau der list()-Bindung in tender-saved-search.service.ts machte genau den erwarteten Bindungstest rot, danach zurueckgenommen. docs/mandantentrennung-zugriffsklassifikation.md: Stand der fuenf Paare auf gebunden bzw. gemischt nachgezogen; tenderRssFeedSource von muss-mandantengebunden auf beides umklassifiziert (derselbe Praezedenzfall wie ldapConfig in 260909-ipc). 761 Tests gruen (743 + 18 neue Bindungsnachweise), Typpruefung sauber, Wegwerf-Werkzeug 32/32, kein Schema-/Migrations-/Compose-/ Umgebungsdatei-Diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -76,8 +76,10 @@ function makeFakePrisma() {
|
||||
*/
|
||||
function makeFakeTriageService() {
|
||||
return {
|
||||
favoriteIds: vi.fn(async (_userId: string) => [] as string[]),
|
||||
listForUser: vi.fn(async (_userId: string, _tenderIds: string[]) => [] as any[]),
|
||||
favoriteIds: vi.fn(async (_userId: string, _tenantId: string) => [] as string[]),
|
||||
listForUser: vi.fn(
|
||||
async (_userId: string, _tenantId: string, _tenderIds: string[]) => [] as any[],
|
||||
),
|
||||
setTriage: vi.fn(async (_userId: string, _tenantId: string, _tenderId: string, _dto: any) => ({})),
|
||||
};
|
||||
}
|
||||
@@ -131,12 +133,14 @@ function makeFakeRssFeedService() {
|
||||
*/
|
||||
function makeFakeEmailConfigService() {
|
||||
return {
|
||||
getConfigForApi: vi.fn(async (_userId: string) => null as any),
|
||||
getConfigForApi: vi.fn(async (_userId: string, _tenantId: string) => null as any),
|
||||
saveConfig: vi.fn(async (_ctx: { userId: string; tenantId: string }, dto: any) => ({
|
||||
id: 'ec-1',
|
||||
...dto,
|
||||
})),
|
||||
testConnection: vi.fn(async (_userId: string, _dto: any) => ({ success: true }) as any),
|
||||
testConnection: vi.fn(
|
||||
async (_userId: string, _tenantId: string, _dto: any) => ({ success: true }) as any,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,16 +151,16 @@ function makeFakeEmailConfigService() {
|
||||
*/
|
||||
function makeFakeSavedSearchService() {
|
||||
return {
|
||||
list: vi.fn(async (_userId: string) => [] as any[]),
|
||||
list: vi.fn(async (_userId: string, _tenantId: string) => [] as any[]),
|
||||
create: vi.fn(async (_userId: string, _tenantId: string, dto: any) => ({
|
||||
id: 'ss-1',
|
||||
...dto,
|
||||
})),
|
||||
update: vi.fn(async (_id: string, _userId: string, dto: any) => ({
|
||||
update: vi.fn(async (_id: string, _userId: string, _tenantId: string, dto: any) => ({
|
||||
id: 'ss-1',
|
||||
...dto,
|
||||
})),
|
||||
remove: vi.fn(async (_id: string, _userId: string) => undefined),
|
||||
remove: vi.fn(async (_id: string, _userId: string, _tenantId: string) => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,7 +172,7 @@ function makeFakeSavedSearchService() {
|
||||
*/
|
||||
function makeFakeNotificationPrefService() {
|
||||
return {
|
||||
getForUser: vi.fn(async (_userId: string) => ({ digestInterval: 'daily' })),
|
||||
getForUser: vi.fn(async (_userId: string, _tenantId: string) => ({ digestInterval: 'daily' })),
|
||||
setForUser: vi.fn(async (_userId: string, _tenantId: string, digestInterval: string) => ({
|
||||
digestInterval,
|
||||
})),
|
||||
@@ -490,7 +494,7 @@ describe('TendersController — GET/PUT notification-pref (NOTIFY-01, per-user,
|
||||
|
||||
await controller.getNotificationPref(makeFakeRequest('u-real'));
|
||||
|
||||
expect(prefService.getForUser).toHaveBeenCalledWith('u-real');
|
||||
expect(prefService.getForUser).toHaveBeenCalledWith('u-real', 'tenant1');
|
||||
});
|
||||
|
||||
it('PUT /notification-pref delegates to tenderNotificationPref.setForUser with userId/tenantId from the auth context, not the body', async () => {
|
||||
@@ -535,7 +539,7 @@ describe('TendersController — saved-searches CRUD (FILTER-06, per-user, T-11-1
|
||||
|
||||
await controller.listSavedSearches(makeFakeRequest('u-real'));
|
||||
|
||||
expect(savedSearchService.list).toHaveBeenCalledWith('u-real');
|
||||
expect(savedSearchService.list).toHaveBeenCalledWith('u-real', 'tenant1');
|
||||
});
|
||||
|
||||
it('POST /saved-searches delegates to tenderSavedSearch.create with userId/tenantId from the auth context, not the body', async () => {
|
||||
@@ -585,7 +589,7 @@ describe('TendersController — saved-searches CRUD (FILTER-06, per-user, T-11-1
|
||||
makeFakeRequest('u1'),
|
||||
);
|
||||
|
||||
expect(savedSearchService.update).toHaveBeenCalledWith('ss-1', 'u1', {
|
||||
expect(savedSearchService.update).toHaveBeenCalledWith('ss-1', 'u1', 'tenant1', {
|
||||
name: 'Neuer Name',
|
||||
});
|
||||
});
|
||||
@@ -607,7 +611,7 @@ describe('TendersController — saved-searches CRUD (FILTER-06, per-user, T-11-1
|
||||
|
||||
const result = await controller.removeSavedSearch('ss-1', makeFakeRequest('u1'));
|
||||
|
||||
expect(savedSearchService.remove).toHaveBeenCalledWith('ss-1', 'u1');
|
||||
expect(savedSearchService.remove).toHaveBeenCalledWith('ss-1', 'u1', 'tenant1');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
@@ -757,7 +761,7 @@ describe('TendersController — GET /triage (batch, per-user, T-11-10/11)', () =
|
||||
|
||||
await controller.listTriage('t1, t2 ,t3', makeFakeRequest('u1'));
|
||||
|
||||
expect(triageService.listForUser).toHaveBeenCalledWith('u1', ['t1', 't2', 't3']);
|
||||
expect(triageService.listForUser).toHaveBeenCalledWith('u1', 'tenant1', ['t1', 't2', 't3']);
|
||||
});
|
||||
|
||||
it('derives userId from req.user, never from the query string (V4 / IDOR)', async () => {
|
||||
@@ -776,7 +780,7 @@ describe('TendersController — GET /triage (batch, per-user, T-11-10/11)', () =
|
||||
|
||||
await controller.listTriage('t1', makeFakeRequest('u-real'));
|
||||
|
||||
expect(triageService.listForUser).toHaveBeenCalledWith('u-real', ['t1']);
|
||||
expect(triageService.listForUser).toHaveBeenCalledWith('u-real', 'tenant1', ['t1']);
|
||||
});
|
||||
|
||||
it('caps the ids batch at MAX_TRIAGE_BATCH_IDS (T-11-11 DoS)', async () => {
|
||||
@@ -796,7 +800,7 @@ describe('TendersController — GET /triage (batch, per-user, T-11-10/11)', () =
|
||||
const manyIds = Array.from({ length: 300 }, (_, i) => `t${i}`).join(',');
|
||||
await controller.listTriage(manyIds, makeFakeRequest('u1'));
|
||||
|
||||
const calledIds = triageService.listForUser.mock.calls[0][1];
|
||||
const calledIds = triageService.listForUser.mock.calls[0][2];
|
||||
expect(calledIds.length).toBeLessThanOrEqual(200);
|
||||
});
|
||||
});
|
||||
@@ -846,7 +850,7 @@ describe('TendersController — listTenders favOnly wiring (UI-04, T-11-10/11)',
|
||||
|
||||
await controller.listTenders({ favOnly: true } as any, makeFakeRequest('u1'));
|
||||
|
||||
expect(triageService.favoriteIds).toHaveBeenCalledWith('u1');
|
||||
expect(triageService.favoriteIds).toHaveBeenCalledWith('u1', 'tenant1');
|
||||
const callArgs = prisma.tender.findMany.mock.calls[0][0];
|
||||
expect(callArgs.where.AND).toEqual(
|
||||
expect.arrayContaining([{ id: { in: ['t1'] } }]),
|
||||
@@ -1081,7 +1085,7 @@ describe('TendersController — email-config (Plan 14-03, per-user since Phase 1
|
||||
|
||||
const result = await controller.getEmailConfig(makeFakeRequest('u1', 'tenant1'));
|
||||
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenCalledWith('u1');
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenCalledWith('u1', 'tenant1');
|
||||
expect(result).toEqual({
|
||||
userId: 'u1',
|
||||
tenantId: 'tenant1',
|
||||
@@ -1130,8 +1134,8 @@ describe('TendersController — email-config (Plan 14-03, per-user since Phase 1
|
||||
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');
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenNthCalledWith(1, 'user-a', 'tenant1');
|
||||
expect(emailConfigService.getConfigForApi).toHaveBeenNthCalledWith(2, 'user-b', 'tenant1');
|
||||
});
|
||||
|
||||
it('POST /email-config/test resolves userId from the auth context, even when the body carries a different identity field (T-QT16-01, IDOR)', async () => {
|
||||
@@ -1155,7 +1159,7 @@ describe('TendersController — email-config (Plan 14-03, per-user since Phase 1
|
||||
} as any;
|
||||
await controller.testEmailConnection(dto, makeFakeRequest('u1', 'tenant1'));
|
||||
|
||||
expect(emailConfigService.testConnection).toHaveBeenCalledWith('u1', dto);
|
||||
expect(emailConfigService.testConnection).toHaveBeenCalledWith('u1', 'tenant1', dto);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user