test(17-03): Tests fuer beide Seiten, Backlog-Punkt abgeschlossen
- RssFeedListForm.test.tsx auf scope umgestellt: personal zeigt eigene Feeds editierbar + plattformweite als schlichte, nicht bedienbare Liste darunter; platform zeigt nur plattformweite Feeds, eigene Feeds tauchen dort gar nicht auf; beide Faelle pruefen den scope-Parameter von createRssFeed - settings-roles.test.tsx (neu): USER sieht Hinweis+Verweis statt Bedienelemente, ADMIN/SUPER_ADMIN sehen Abrufintervall+RSS-Feeds, unbekannte Rolle zeigt weder-noch, entfernte Abschnittsueberschriften kommen nirgends mehr vor - my-sources.test.tsx (neu): alle drei Abschnittsueberschriften vorhanden, Verweis auf die Administrationsseite nur bei ADMIN/SUPER_ADMIN - Erwartete Texte in allen drei Testdateien von Hand geschrieben, nicht aus der gleichen next-intl-Zuordnung abgeleitet, die die Komponenten benutzen - Backlog-Punkt 2026-08-11-tender-radar-einstellungen-mischen-rollen.md nach todos/completed/ verschoben, mit Resolution-Abschnitt: beide Nutzer- Entscheidungen (eigene Quellen je Nutzer, Trefferliste bleibt plattform-global) und die Antwort auf offenen Punkt 4 (eigene nutzerseitige Modulseite als Vorbild fuer kuenftige Module) dokumentiert Verifikation: Web 205/205, API src/tenders 362/362, beide Typpruefungen fehlerfrei.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
created: 2026-08-11
|
||||
completed: 2026-08-12
|
||||
title: Ausschreibungs-Radar — Einstellungsseite mischt Administration und persoenliche Einstellung
|
||||
area: tender-radar
|
||||
severity: minor
|
||||
trigger: wenn das Modul das erste Mal von normalen Nutzern verwendet wird, nicht nur von Admins
|
||||
files:
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx
|
||||
- apps/web/src/app/(portal)/settings/general/account
|
||||
- apps/api/src/tenders/tenders.controller.ts
|
||||
resolution_commits:
|
||||
- 05b1d29
|
||||
- 55ceb24
|
||||
- adb72f6
|
||||
- 9616155
|
||||
- 7dee116
|
||||
- 4100bb5
|
||||
- 150046e
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
`/modules/tender-radar/settings` traegt vier Abschnitte, die drei verschiedenen
|
||||
Zustaendigkeiten gehoeren:
|
||||
|
||||
| Abschnitt | Gehoert |
|
||||
|---|---|
|
||||
| Quelle (Abrufintervall, Aktiv) | Plattform-Administration |
|
||||
| RSS-Feeds | Plattform-Administration (ausdruecklich fuer alle Mandanten gleich) |
|
||||
| E-Mail-Alerts (Postfach) | Mandanten-Administration |
|
||||
| **Benachrichtigungen (Digest-Intervall)** | **dem einzelnen Nutzer** |
|
||||
|
||||
Das Digest-Intervall ist die einzige persoenliche Einstellung auf der Seite —
|
||||
`TenderNotificationPref` haengt an `userId`, nicht am Mandanten (NOTIFY-01/D-03).
|
||||
Sie steht als letzter Abschnitt unter drei Bloecken, die ein normaler Nutzer
|
||||
weder aendern darf noch braucht. Wer nur seinen Digest auf woechentlich stellen
|
||||
will, scrollt an Abrufintervallen, Feed-URLs und Postfach-Zugangsdaten vorbei.
|
||||
|
||||
**Kein Berechtigungsloch:** die zugehoerigen API-Endpunkte (`source-config`,
|
||||
`rss-feeds`, Postfach) sind serverseitig mit `@Roles(ADMIN, SUPER_ADMIN)`
|
||||
abgesichert (`tenders.controller.ts`). Ein normaler Nutzer kann dort also
|
||||
nichts verstellen — er sieht die Bedienelemente aber und laeuft beim Speichern
|
||||
in eine Fehlermeldung, weil die Seite selbst keine Rollenpruefung hat.
|
||||
|
||||
Aufgefallen am 2026-08-11 beim Erklaeren des Moduls: der User fragte, wo man
|
||||
die Benachrichtigungsfrequenz einstellt, und die ehrliche Antwort war "ganz
|
||||
unten auf der Admin-Seite".
|
||||
|
||||
## Solution
|
||||
|
||||
Richtung, nicht beschlossen — der zweite Punkt ist eine Produktfrage:
|
||||
|
||||
1. **Die Admin-Abschnitte hinter eine Rollenpruefung legen**, damit ein
|
||||
normaler Nutzer sie gar nicht erst sieht. Kleinster sinnvoller Schritt,
|
||||
loest den Fehlermeldungs-Fall und den groessten Teil der Verwirrung.
|
||||
2. **Wohin gehoert die persoenliche Einstellung?** Es gibt bereits
|
||||
`/settings/general/account` fuer nutzereigene Einstellungen. Drei Varianten:
|
||||
- Digest-Intervall dorthin verschieben — konsequent, aber modulfremde
|
||||
Einstellungen in einer allgemeinen Seite sammeln sich mit jedem weiteren
|
||||
Modul an.
|
||||
- Im Modul lassen, aber sichtbar abgetrennt und oberhalb der Admin-Bloecke.
|
||||
- Eine eigene, nutzerseitige Modulseite ("Meine Benachrichtigungen")
|
||||
getrennt von der Admin-Seite.
|
||||
|
||||
Die Antwort betrifft nicht nur dieses Modul: DKV-Fleet hat dieselbe Bauform
|
||||
(`modules/dkv-fleet/settings`), und jedes kuenftige Modul mit persoenlichen
|
||||
Einstellungen wird die Frage erneut stellen. Sinnvollerweise einmal
|
||||
grundsaetzlich entscheiden statt pro Modul.
|
||||
|
||||
## Resolution (2026-08-12)
|
||||
|
||||
Beim Durchsprechen dieses Zettels mit dem User stellte sich heraus, dass die
|
||||
urspruengliche Diagnose zu eng war — nicht nur das Digest-Intervall gehoerte
|
||||
dem einzelnen Nutzer, auch das Postfach und die RSS-Feeds waren inhaltlich
|
||||
falsch zugeschnitten ("Wenn Person A ihre Ausschreibungen auf kontoa@mail.de
|
||||
und Person B von kontob@mail.de holt, macht eine gemeinsame Einstellung
|
||||
keinen Sinn"). Der User hat festgehalten, dass seine urspruengliche Zustimmung
|
||||
zur gemeinsamen Konfiguration auf einer missverstaendlichen Erklaerung
|
||||
meinerseits beruhte — keine Meinungsaenderung, eine Korrektur. Daraus wurde
|
||||
Phase 17 mit drei Plaenen statt einer kleinen Rollenpruefung:
|
||||
|
||||
**Zwei Entscheidungen, die der User im Gespraech getroffen hat:**
|
||||
|
||||
- **Eigene Quellen je Nutzer.** `TenderEmailConfig` wechselt von
|
||||
`tenantId @unique` auf `userId @unique` (D-01), `TenderRssFeedSource`
|
||||
bekommt ein nullable `userId` — plattformweite Feeds (der seit Phase 14
|
||||
aktive service.bund.de-Feed) bleiben fuer alle bestehen, jeder Nutzer kann
|
||||
zusaetzlich eigene Feeds anlegen (D-02). Umgesetzt in 05b1d29/55ceb24
|
||||
(Plan 17-01) und adb72f6/9616155/7dee116 (Plan 17-02).
|
||||
- **Trefferliste bleibt plattform-global (D-05, harte Grenze).** Explizit
|
||||
gegengefragt und bestaetigt: "Reicht dir 'jeder speist seine eigenen
|
||||
Quellen ein, gesehen wird alles gemeinsam'?" → "Ja, plane". `Tender` bleibt
|
||||
ohne `tenantId`/RLS — geaendert wird nur, WER Quellen einspeist, nicht WER
|
||||
Treffer sieht. Was ueber das Postfach oder einen Feed von Nutzer A
|
||||
hereinkommt, steht danach auch in der Trefferliste von Nutzer B desselben
|
||||
Mandanten. Diese Konsequenz benennt die Oberflaeche jetzt ehrlich
|
||||
(`mySources.intro`, `mySources.platformFeedsNote`).
|
||||
|
||||
**Offener Punkt 4 (wohin mit den nutzereigenen Abschnitten) ist damit
|
||||
ebenfalls beantwortet:** eine eigene, nutzerseitige Modulseite
|
||||
(`/modules/tender-radar/my-sources`, "Meine Quellen") statt einer Erweiterung
|
||||
von `/settings/general/account` — modulspezifische Einstellungen bleiben beim
|
||||
Modul statt sich auf einer allgemeinen Seite anzusammeln. Das ist ab jetzt die
|
||||
Bauform, an der sich DKV-Fleet und kuenftige Module mit persoenlichen
|
||||
Einstellungen orientieren.
|
||||
|
||||
**Offener Punkt 5 (Rollenpruefung) schliesst Plan 17-03** (4100bb5, 150046e):
|
||||
`/modules/tender-radar/settings` traegt nur noch, was tatsaechlich
|
||||
Plattform-Administration ist — Abrufintervall und plattformweite RSS-Feeds —
|
||||
und prueft die Rolle aus dem Anmelde-Speicher rein zur Anzeige (die
|
||||
verbindliche Pruefung bleibt serverseitig, `@UseModule('tender-radar')` +
|
||||
Inline-Rollencheck fuer `scope: 'platform'`, T-17-08). Ein normaler Nutzer
|
||||
sieht dort keine Bedienelemente mehr, die beim Speichern in eine
|
||||
Fehlermeldung liefen — genau der urspruengliche Anlass dieses Zettels.
|
||||
Postfach, eigene Feeds und Benachrichtigungsintervall leben jetzt vollstaendig
|
||||
auf "Meine Quellen"; das Zahnrad auf der Modulseite fuehrt jeden Nutzer
|
||||
dorthin.
|
||||
|
||||
**Nicht ausgefuehrt:** die Browser-Gegenprobe aus den `human-check`-Schritten
|
||||
der Plaene 17-01 bis 17-03 (kein Browser-Tool in den jeweiligen Sitzungen
|
||||
verfuegbar) — festgehalten in WINDOWS.md (#7 und Folge-Eintraege), vor
|
||||
`/gsd-ship` nachzuholen.
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
created: 2026-08-11
|
||||
title: Ausschreibungs-Radar — Einstellungsseite mischt Administration und persoenliche Einstellung
|
||||
area: tender-radar
|
||||
severity: minor
|
||||
trigger: wenn das Modul das erste Mal von normalen Nutzern verwendet wird, nicht nur von Admins
|
||||
files:
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx
|
||||
- apps/web/src/app/(portal)/settings/general/account
|
||||
- apps/api/src/tenders/tenders.controller.ts
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
`/modules/tender-radar/settings` traegt vier Abschnitte, die drei verschiedenen
|
||||
Zustaendigkeiten gehoeren:
|
||||
|
||||
| Abschnitt | Gehoert |
|
||||
|---|---|
|
||||
| Quelle (Abrufintervall, Aktiv) | Plattform-Administration |
|
||||
| RSS-Feeds | Plattform-Administration (ausdruecklich fuer alle Mandanten gleich) |
|
||||
| E-Mail-Alerts (Postfach) | Mandanten-Administration |
|
||||
| **Benachrichtigungen (Digest-Intervall)** | **dem einzelnen Nutzer** |
|
||||
|
||||
Das Digest-Intervall ist die einzige persoenliche Einstellung auf der Seite —
|
||||
`TenderNotificationPref` haengt an `userId`, nicht am Mandanten (NOTIFY-01/D-03).
|
||||
Sie steht als letzter Abschnitt unter drei Bloecken, die ein normaler Nutzer
|
||||
weder aendern darf noch braucht. Wer nur seinen Digest auf woechentlich stellen
|
||||
will, scrollt an Abrufintervallen, Feed-URLs und Postfach-Zugangsdaten vorbei.
|
||||
|
||||
**Kein Berechtigungsloch:** die zugehoerigen API-Endpunkte (`source-config`,
|
||||
`rss-feeds`, Postfach) sind serverseitig mit `@Roles(ADMIN, SUPER_ADMIN)`
|
||||
abgesichert (`tenders.controller.ts`). Ein normaler Nutzer kann dort also
|
||||
nichts verstellen — er sieht die Bedienelemente aber und laeuft beim Speichern
|
||||
in eine Fehlermeldung, weil die Seite selbst keine Rollenpruefung hat.
|
||||
|
||||
Aufgefallen am 2026-08-11 beim Erklaeren des Moduls: der User fragte, wo man
|
||||
die Benachrichtigungsfrequenz einstellt, und die ehrliche Antwort war "ganz
|
||||
unten auf der Admin-Seite".
|
||||
|
||||
## Solution
|
||||
|
||||
Richtung, nicht beschlossen — der zweite Punkt ist eine Produktfrage:
|
||||
|
||||
1. **Die Admin-Abschnitte hinter eine Rollenpruefung legen**, damit ein
|
||||
normaler Nutzer sie gar nicht erst sieht. Kleinster sinnvoller Schritt,
|
||||
loest den Fehlermeldungs-Fall und den groessten Teil der Verwirrung.
|
||||
2. **Wohin gehoert die persoenliche Einstellung?** Es gibt bereits
|
||||
`/settings/general/account` fuer nutzereigene Einstellungen. Drei Varianten:
|
||||
- Digest-Intervall dorthin verschieben — konsequent, aber modulfremde
|
||||
Einstellungen in einer allgemeinen Seite sammeln sich mit jedem weiteren
|
||||
Modul an.
|
||||
- Im Modul lassen, aber sichtbar abgetrennt und oberhalb der Admin-Bloecke.
|
||||
- Eine eigene, nutzerseitige Modulseite ("Meine Benachrichtigungen")
|
||||
getrennt von der Admin-Seite.
|
||||
|
||||
Die Antwort betrifft nicht nur dieses Modul: DKV-Fleet hat dieselbe Bauform
|
||||
(`modules/dkv-fleet/settings`), und jedes kuenftige Modul mit persoenlichen
|
||||
Einstellungen wird die Frage erneut stellen. Sinnvollerweise einmal
|
||||
grundsaetzlich entscheiden statt pro Modul.
|
||||
@@ -0,0 +1,204 @@
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock @/lib/tender-radar-api — EmailAlertConfigForm, RssFeedListForm and
|
||||
// DigestIntervalForm all call into this module on mount.
|
||||
const mockFetchEmailConfig = vi.fn();
|
||||
const mockSaveEmailConfig = vi.fn();
|
||||
const mockListRssFeeds = vi.fn();
|
||||
const mockCreateRssFeed = vi.fn();
|
||||
const mockDeleteRssFeed = vi.fn();
|
||||
const mockFetchNotificationPref = vi.fn();
|
||||
const mockSaveNotificationPref = vi.fn();
|
||||
|
||||
vi.mock('@/lib/tender-radar-api', () => ({
|
||||
fetchEmailConfig: (...args: unknown[]) => mockFetchEmailConfig(...args),
|
||||
saveEmailConfig: (...args: unknown[]) => mockSaveEmailConfig(...args),
|
||||
listRssFeeds: (...args: unknown[]) => mockListRssFeeds(...args),
|
||||
createRssFeed: (...args: unknown[]) => mockCreateRssFeed(...args),
|
||||
deleteRssFeed: (...args: unknown[]) => mockDeleteRssFeed(...args),
|
||||
fetchNotificationPref: (...args: unknown[]) => mockFetchNotificationPref(...args),
|
||||
saveNotificationPref: (...args: unknown[]) => mockSaveNotificationPref(...args),
|
||||
}));
|
||||
|
||||
const mockAuthStore = vi.fn();
|
||||
vi.mock('@/lib/stores/auth-store', () => ({
|
||||
useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector),
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ href, children, ...rest }: { href: string; children: React.ReactNode }) => (
|
||||
<a href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string, params?: Record<string, string>) => {
|
||||
const translations: Record<string, string> = {
|
||||
'mySources.title': 'Meine Quellen',
|
||||
'mySources.intro': 'Ausschreibungen aus Ihrem Postfach sind mandantenweit sichtbar.',
|
||||
'mySources.mailboxSectionTitle': 'Mein Postfach',
|
||||
'mySources.feedsSectionTitle': 'Meine Feeds',
|
||||
'mySources.notificationSectionTitle': 'Benachrichtigung',
|
||||
'mySources.platformFeedsNote':
|
||||
'Diese Feeds werden von der Administration gepflegt und gelten für alle.',
|
||||
'page.settingsTitle': 'Einstellungen',
|
||||
'emailAlerts.protocolLabel': 'Protokoll',
|
||||
'emailAlerts.protocolImap': 'IMAP',
|
||||
'emailAlerts.protocolExchange': 'Exchange',
|
||||
'emailAlerts.hostLabelImap': 'Server',
|
||||
'emailAlerts.hostLabelExchange': 'EWS-URL',
|
||||
'emailAlerts.hostPlaceholderImap': 'imap.example.com',
|
||||
'emailAlerts.hostPlaceholderExchange': 'https://ews.example.com/EWS/Exchange.asmx',
|
||||
'emailAlerts.portLabel': 'Port',
|
||||
'emailAlerts.encryptionLabel': 'Verschlüsselung',
|
||||
'emailAlerts.encryptionNone': 'Keine',
|
||||
'emailAlerts.encryptionStarttls': 'STARTTLS',
|
||||
'emailAlerts.encryptionSslTls': 'SSL/TLS',
|
||||
'emailAlerts.folderLabel': 'Ordner',
|
||||
'emailAlerts.folderPlaceholderImap': 'INBOX',
|
||||
'emailAlerts.folderPlaceholderExchange': 'Posteingang',
|
||||
'emailAlerts.domainLabel': 'Domäne',
|
||||
'emailAlerts.domainPlaceholder': 'example.local',
|
||||
'emailAlerts.senderFilterLabel': 'Absenderfilter',
|
||||
'emailAlerts.senderFilterPlaceholder': 'noreply@vergabeportal.de',
|
||||
'emailAlerts.usernameLabel': 'Benutzername',
|
||||
'emailAlerts.passwordLabel': 'Passwort',
|
||||
'emailAlerts.passwordPlaceholder': '••••••••',
|
||||
'emailAlerts.showPassword': 'Passwort anzeigen',
|
||||
'emailAlerts.hidePassword': 'Passwort verbergen',
|
||||
'emailAlerts.activeLabel': 'Aktiv',
|
||||
'emailAlerts.saving': 'Wird gespeichert...',
|
||||
'emailAlerts.save': 'Speichern',
|
||||
'emailAlerts.saveSuccess': 'Einstellungen gespeichert.',
|
||||
'emailAlerts.errorSave': 'Einstellungen konnten nicht gespeichert werden',
|
||||
'emailAlerts.notSaved': 'Noch kein Postfach hinterlegt.',
|
||||
'rssFeeds.empty': 'Noch keine RSS-Feeds hinterlegt.',
|
||||
'rssFeeds.errorLoad': 'RSS-Feeds konnten nicht geladen werden',
|
||||
'rssFeeds.urlLabel': 'Feed-URL *',
|
||||
'rssFeeds.labelLabel': 'Bezeichnung *',
|
||||
'rssFeeds.urlPlaceholder': 'https://...',
|
||||
'rssFeeds.labelPlaceholder': 'z. B. service-bund',
|
||||
'rssFeeds.add': 'Feed hinzufügen',
|
||||
'rssFeeds.adding': 'Wird hinzugefügt...',
|
||||
'rssFeeds.validationRequired': 'URL und Bezeichnung sind erforderlich.',
|
||||
'settings.digestIntervalLabel': 'Digest-Intervall',
|
||||
'settings.digestDaily': 'Täglich',
|
||||
'settings.digestWeekly': 'Wöchentlich',
|
||||
'settings.digestOff': 'Aus',
|
||||
'settings.digestHelp': 'Fasst neue Treffer in einer Sammel-Mail zusammen.',
|
||||
'settings.saveSuccess': 'Einstellung gespeichert.',
|
||||
'settings.errorLoad': 'Benachrichtigungseinstellung konnte nicht geladen werden',
|
||||
'settings.errorSave': 'Benachrichtigungseinstellung konnte nicht gespeichert werden',
|
||||
};
|
||||
let result = translations[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
result = result.replace(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) {
|
||||
mockAuthStore.mockImplementation(
|
||||
(selector: (state: { user: typeof user }) => unknown) => selector({ user }),
|
||||
);
|
||||
}
|
||||
|
||||
function mockAllFetchesEmpty() {
|
||||
mockFetchEmailConfig.mockResolvedValue(null);
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
mockFetchNotificationPref.mockResolvedValue({ digestInterval: 'daily' });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockFetchEmailConfig.mockReset();
|
||||
mockSaveEmailConfig.mockReset();
|
||||
mockListRssFeeds.mockReset();
|
||||
mockCreateRssFeed.mockReset();
|
||||
mockDeleteRssFeed.mockReset();
|
||||
mockFetchNotificationPref.mockReset();
|
||||
mockSaveNotificationPref.mockReset();
|
||||
mockAuthStore.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('TenderRadarMySourcesPage', () => {
|
||||
it('renders all three section headings (mailbox, feeds, notification)', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockAllFetchesEmpty();
|
||||
|
||||
const { default: TenderRadarMySourcesPage } = await import('./page');
|
||||
render(<TenderRadarMySourcesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Mein Postfach')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Meine Feeds')).toBeInTheDocument();
|
||||
expect(screen.getByText('Benachrichtigung')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('role USER: does NOT show the link to the administration settings page', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockAllFetchesEmpty();
|
||||
|
||||
const { default: TenderRadarMySourcesPage } = await import('./page');
|
||||
render(<TenderRadarMySourcesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Meine Feeds')).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.queryByRole('link', { name: /Einstellungen/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('role ADMIN: shows the link to the administration settings page', async () => {
|
||||
mockUser({ role: 'ADMIN' });
|
||||
mockAllFetchesEmpty();
|
||||
|
||||
const { default: TenderRadarMySourcesPage } = await import('./page');
|
||||
render(<TenderRadarMySourcesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Meine Feeds')).toBeInTheDocument();
|
||||
});
|
||||
const link = screen.getByRole('link', { name: /Einstellungen/i });
|
||||
expect(link).toHaveAttribute('href', '/modules/tender-radar/settings');
|
||||
});
|
||||
|
||||
it('role SUPER_ADMIN: shows the link to the administration settings page too', async () => {
|
||||
mockUser({ role: 'SUPER_ADMIN' });
|
||||
mockAllFetchesEmpty();
|
||||
|
||||
const { default: TenderRadarMySourcesPage } = await import('./page');
|
||||
render(<TenderRadarMySourcesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Meine Feeds')).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('link', { name: /Einstellungen/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('account data not loaded yet (user === null): omits the admin link instead of flashing it briefly', async () => {
|
||||
mockUser(null);
|
||||
mockAllFetchesEmpty();
|
||||
|
||||
const { default: TenderRadarMySourcesPage } = await import('./page');
|
||||
render(<TenderRadarMySourcesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Meine Feeds')).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.queryByRole('link', { name: /Einstellungen/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+146
-26
@@ -13,7 +13,10 @@ vi.mock('@/lib/tender-radar-api', () => ({
|
||||
}));
|
||||
|
||||
// Mock next-intl useTranslations — mirrors the marketplace test mock
|
||||
// convention (tenant-selector.test.tsx) with {param} interpolation.
|
||||
// convention (tenant-selector.test.tsx) with {param} interpolation. Text is
|
||||
// hand-written here, NOT derived from the component/message files under
|
||||
// test — a test that builds its own expectation from the same helper the
|
||||
// component uses proves nothing.
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string, params?: Record<string, string>) => {
|
||||
const translations: Record<string, string> = {
|
||||
@@ -33,6 +36,8 @@ vi.mock('next-intl', () => ({
|
||||
'rssFeeds.labelPlaceholder': 'z. B. service-bund',
|
||||
'rssFeeds.adding': 'Wird hinzugefügt...',
|
||||
'rssFeeds.add': 'Feed hinzufügen',
|
||||
'mySources.platformFeedsNote':
|
||||
'Diese Feeds werden von der Administration gepflegt und gelten für alle.',
|
||||
};
|
||||
let result = translations[key] ?? key;
|
||||
if (params) {
|
||||
@@ -52,34 +57,45 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const FEED_SERVICE_BUND = {
|
||||
id: 'feed-1',
|
||||
url: 'https://www.service.bund.de/rss.xml',
|
||||
label: 'service-bund',
|
||||
const FEED_OWN = {
|
||||
id: 'feed-own',
|
||||
url: 'https://www.subreport-elvis.de/elvis/secure/rss.pl?id=4615',
|
||||
label: 'meine-quelle',
|
||||
isActive: true,
|
||||
isPlatformWide: false,
|
||||
createdAt: '2026-07-23T00:00:00.000Z',
|
||||
updatedAt: '2026-07-23T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('RssFeedListForm', () => {
|
||||
it('loads feeds on mount and renders each one with its label and URL', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([FEED_SERVICE_BUND]);
|
||||
const FEED_PLATFORM = {
|
||||
id: 'feed-platform',
|
||||
url: 'https://www.service.bund.de/rss.xml',
|
||||
label: 'service-bund',
|
||||
isActive: true,
|
||||
isPlatformWide: true,
|
||||
createdAt: '2026-07-23T00:00:00.000Z',
|
||||
updatedAt: '2026-07-23T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('RssFeedListForm scope="personal"', () => {
|
||||
it('loads feeds on mount and renders the own feed in the editable list', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([FEED_OWN]);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm />);
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('service-bund', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('meine-quelle', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(FEED_SERVICE_BUND.url)).toBeInTheDocument();
|
||||
expect(screen.getByText(FEED_OWN.url)).toBeInTheDocument();
|
||||
expect(mockListRssFeeds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders an empty-state message when there are no feeds', async () => {
|
||||
it('renders an empty-state message when there are no own feeds', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm />);
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -88,7 +104,36 @@ describe('RssFeedListForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('filling the add form and clicking "Feed hinzufügen" calls createRssFeed and appends the new feed to the list', async () => {
|
||||
it('shows the own feed with a Entfernen button and the platform-wide feed below WITHOUT one', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([FEED_PLATFORM, FEED_OWN]);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('meine-quelle', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Own feed has exactly one Entfernen button
|
||||
expect(
|
||||
screen.getAllByRole('button', { name: /entfernen/i }),
|
||||
).toHaveLength(1);
|
||||
|
||||
// Platform-wide feed is listed, but not as a button target
|
||||
expect(screen.getByText('service-bund', { exact: false })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: /RSS-Feed "service-bund" entfernen/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// The "administration maintains this" note is shown
|
||||
expect(
|
||||
screen.getByText(/von der Administration gepflegt/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filling the add form and clicking "Feed hinzufügen" calls createRssFeed with scope "personal" and appends the new feed to the own list', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
mockCreateRssFeed.mockResolvedValue({
|
||||
id: 'feed-2',
|
||||
@@ -97,10 +142,12 @@ describe('RssFeedListForm', () => {
|
||||
isActive: true,
|
||||
createdAt: '2026-07-23T00:00:00.000Z',
|
||||
updatedAt: '2026-07-23T00:00:00.000Z',
|
||||
// deliberately NO isPlatformWide — the create endpoint doesn't map it
|
||||
// (T-17-12 only maps GET); the component must derive it locally.
|
||||
});
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm />);
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Noch keine RSS-Feeds hinterlegt/i)).toBeInTheDocument();
|
||||
@@ -115,14 +162,20 @@ describe('RssFeedListForm', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Feed hinzufügen/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateRssFeed).toHaveBeenCalledWith({
|
||||
url: 'https://www.subreport-elvis.de/elvis/secure/rss.pl?id=4615',
|
||||
label: 'subreport-neuss',
|
||||
});
|
||||
expect(mockCreateRssFeed).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://www.subreport-elvis.de/elvis/secure/rss.pl?id=4615',
|
||||
label: 'subreport-neuss',
|
||||
},
|
||||
'personal',
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('subreport-neuss', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
// The newly created feed lands in the editable (own) list, with a
|
||||
// working Entfernen button — proof isPlatformWide was derived as false.
|
||||
expect(screen.getByRole('button', { name: /entfernen/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces the backend denylist rejection message inline without adding a row', async () => {
|
||||
@@ -134,7 +187,7 @@ describe('RssFeedListForm', () => {
|
||||
);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm />);
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Noch keine RSS-Feeds hinterlegt/i)).toBeInTheDocument();
|
||||
@@ -158,7 +211,7 @@ describe('RssFeedListForm', () => {
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm />);
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Noch keine RSS-Feeds hinterlegt/i)).toBeInTheDocument();
|
||||
@@ -174,24 +227,91 @@ describe('RssFeedListForm', () => {
|
||||
expect(mockCreateRssFeed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clicking "Entfernen" calls deleteRssFeed and removes the row from the list', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([FEED_SERVICE_BUND]);
|
||||
it('clicking "Entfernen" on the own feed calls deleteRssFeed and removes the row from the list', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([FEED_OWN]);
|
||||
mockDeleteRssFeed.mockResolvedValue(undefined);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm />);
|
||||
render(<RssFeedListForm scope="personal" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('service-bund', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('meine-quelle', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /entfernen/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteRssFeed).toHaveBeenCalledWith('feed-1');
|
||||
expect(mockDeleteRssFeed).toHaveBeenCalledWith('feed-own');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Noch keine RSS-Feeds hinterlegt/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RssFeedListForm scope="platform"', () => {
|
||||
it('shows the platform-wide feed as editable and hides the own personal feed entirely', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([FEED_PLATFORM, FEED_OWN]);
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm scope="platform" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('service-bund', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Platform feed IS deletable here
|
||||
expect(screen.getAllByRole('button', { name: /entfernen/i })).toHaveLength(1);
|
||||
|
||||
// The caller's own personal feed never renders on this scope, not even
|
||||
// as a readonly row — the "platform maintains this" note only makes
|
||||
// sense on the personal-scope page.
|
||||
expect(screen.queryByText('meine-quelle', { exact: false })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/von der Administration gepflegt/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filling the add form calls createRssFeed with scope "platform"', async () => {
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
mockCreateRssFeed.mockResolvedValue({
|
||||
id: 'feed-3',
|
||||
url: 'https://www.service.bund.de/rss2.xml',
|
||||
label: 'neuer-plattform-feed',
|
||||
isActive: true,
|
||||
createdAt: '2026-08-12T00:00:00.000Z',
|
||||
updatedAt: '2026-08-12T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const { RssFeedListForm } = await import('./RssFeedListForm');
|
||||
render(<RssFeedListForm scope="platform" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Noch keine RSS-Feeds hinterlegt/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/Feed-URL/i), {
|
||||
target: { value: 'https://www.service.bund.de/rss2.xml' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/Bezeichnung/i), {
|
||||
target: { value: 'neuer-plattform-feed' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /Feed hinzufügen/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateRssFeed).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://www.service.bund.de/rss2.xml',
|
||||
label: 'neuer-plattform-feed',
|
||||
},
|
||||
'platform',
|
||||
);
|
||||
});
|
||||
// The newly created feed appears immediately in the platform-editable
|
||||
// list (proof isPlatformWide was derived as true, not left undefined).
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('neuer-plattform-feed', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('button', { name: /entfernen/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock @/lib/tender-radar-api — SourceConfigForm and RssFeedListForm both
|
||||
// call into this module on mount when the admin content renders.
|
||||
const mockFetchSourceConfig = vi.fn();
|
||||
const mockSaveSourceConfig = vi.fn();
|
||||
const mockListRssFeeds = vi.fn();
|
||||
const mockCreateRssFeed = vi.fn();
|
||||
const mockDeleteRssFeed = vi.fn();
|
||||
|
||||
vi.mock('@/lib/tender-radar-api', () => ({
|
||||
fetchSourceConfig: (...args: unknown[]) => mockFetchSourceConfig(...args),
|
||||
saveSourceConfig: (...args: unknown[]) => mockSaveSourceConfig(...args),
|
||||
listRssFeeds: (...args: unknown[]) => mockListRssFeeds(...args),
|
||||
createRssFeed: (...args: unknown[]) => mockCreateRssFeed(...args),
|
||||
deleteRssFeed: (...args: unknown[]) => mockDeleteRssFeed(...args),
|
||||
}));
|
||||
|
||||
// Mock the auth store — mirrors the marketplace test convention
|
||||
// (marketplace.test.tsx): a selector-passthrough vi.fn() re-implemented
|
||||
// per test case.
|
||||
const mockAuthStore = vi.fn();
|
||||
vi.mock('@/lib/stores/auth-store', () => ({
|
||||
useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector),
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ href, children, ...rest }: { href: string; children: React.ReactNode }) => (
|
||||
<a href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock next-intl useTranslations — hand-written text, not derived from the
|
||||
// component or from messages/*.json.
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string, params?: Record<string, string>) => {
|
||||
const translations: Record<string, string> = {
|
||||
'settings.title': 'Ausschreibungs-Radar — Einstellungen',
|
||||
'settings.accessDeniedText':
|
||||
'Diese Seite verwaltet plattformweite Einstellungen und ist Administratoren vorbehalten.',
|
||||
'settings.rssSectionTitle': 'RSS-Feeds',
|
||||
'settings.rssSectionBody': 'Öffentliche, plattformweite RSS-Quellen.',
|
||||
'settings.rssSectionUserNote': 'Persönliche Feeds legen Sie auf „Meine Quellen" an.',
|
||||
'mySources.title': 'Meine Quellen',
|
||||
'sourceConfig.sourceLabel': 'Quelle',
|
||||
'sourceConfig.lastIngestedLabel': 'Zuletzt erfasster Tag',
|
||||
'sourceConfig.neverIngested': 'Noch keine Erfassung',
|
||||
'sourceConfig.pollIntervalLabel': 'Abrufintervall (Minuten) *',
|
||||
'sourceConfig.minutesSuffix': 'Min.',
|
||||
'sourceConfig.pollIntervalHelp': 'Cron-Taktfrequenz.',
|
||||
'sourceConfig.activeLabel': 'Aktiv',
|
||||
'sourceConfig.saving': 'Speichert...',
|
||||
'sourceConfig.save': 'Speichern',
|
||||
'sourceConfig.saveSuccess': 'Einstellungen gespeichert.',
|
||||
'sourceConfig.errorLoad': 'Konfiguration konnte nicht geladen werden',
|
||||
'rssFeeds.empty': 'Noch keine RSS-Feeds hinterlegt.',
|
||||
'rssFeeds.errorLoad': 'RSS-Feeds konnten nicht geladen werden',
|
||||
'rssFeeds.urlLabel': 'Feed-URL *',
|
||||
'rssFeeds.labelLabel': 'Bezeichnung *',
|
||||
'rssFeeds.urlPlaceholder': 'https://...',
|
||||
'rssFeeds.labelPlaceholder': 'z. B. service-bund',
|
||||
'rssFeeds.add': 'Feed hinzufügen',
|
||||
'rssFeeds.adding': 'Wird hinzugefügt...',
|
||||
'rssFeeds.validationRequired': 'URL und Bezeichnung sind erforderlich.',
|
||||
};
|
||||
let result = translations[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
result = result.replace(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) {
|
||||
mockAuthStore.mockImplementation(
|
||||
(selector: (state: { user: typeof user }) => unknown) => selector({ user }),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockFetchSourceConfig.mockReset();
|
||||
mockSaveSourceConfig.mockReset();
|
||||
mockListRssFeeds.mockReset();
|
||||
mockCreateRssFeed.mockReset();
|
||||
mockDeleteRssFeed.mockReset();
|
||||
mockAuthStore.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('TenderRadarSettingsPage role gating', () => {
|
||||
it('role USER: renders the access-denied text and a link to "Meine Quellen", no admin controls', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
|
||||
const { default: TenderRadarSettingsPage } = await import('./page');
|
||||
render(<TenderRadarSettingsPage />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Administratoren vorbehalten/i),
|
||||
).toBeInTheDocument();
|
||||
const link = screen.getByRole('link', { name: /Meine Quellen/i });
|
||||
expect(link).toHaveAttribute('href', '/modules/tender-radar/my-sources');
|
||||
|
||||
// No admin section renders at all
|
||||
expect(screen.queryByText('RSS-Feeds')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/Abrufintervall/i)).not.toBeInTheDocument();
|
||||
expect(mockFetchSourceConfig).not.toHaveBeenCalled();
|
||||
expect(mockListRssFeeds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('role ADMIN: renders the poll-interval and platform RSS-feed sections, not the access-denied text', async () => {
|
||||
mockUser({ role: 'ADMIN' });
|
||||
mockFetchSourceConfig.mockResolvedValue({
|
||||
sourceType: 'doe-opendata',
|
||||
pollIntervalMin: 60,
|
||||
isActive: true,
|
||||
lastIngestedDay: null,
|
||||
});
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
|
||||
const { default: TenderRadarSettingsPage } = await import('./page');
|
||||
render(<TenderRadarSettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/Abrufintervall/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('RSS-Feeds')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Administratoren vorbehalten/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('role SUPER_ADMIN: renders the admin content the same as ADMIN', async () => {
|
||||
mockUser({ role: 'SUPER_ADMIN' });
|
||||
mockFetchSourceConfig.mockResolvedValue({
|
||||
sourceType: 'doe-opendata',
|
||||
pollIntervalMin: 90,
|
||||
isActive: false,
|
||||
lastIngestedDay: '2026-08-01T00:00:00.000Z',
|
||||
});
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
|
||||
const { default: TenderRadarSettingsPage } = await import('./page');
|
||||
render(<TenderRadarSettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/Abrufintervall/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('RSS-Feeds')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('account data not loaded yet: shows neither the admin content nor the access-denied text', async () => {
|
||||
mockUser(null);
|
||||
|
||||
const { default: TenderRadarSettingsPage } = await import('./page');
|
||||
render(<TenderRadarSettingsPage />);
|
||||
|
||||
expect(screen.queryByText('RSS-Feeds')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Administratoren vorbehalten/i),
|
||||
).not.toBeInTheDocument();
|
||||
expect(mockFetchSourceConfig).not.toHaveBeenCalled();
|
||||
expect(mockListRssFeeds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('the removed mailbox/notification section headings never render on this page, even for ADMIN', async () => {
|
||||
mockUser({ role: 'ADMIN' });
|
||||
mockFetchSourceConfig.mockResolvedValue({
|
||||
sourceType: 'doe-opendata',
|
||||
pollIntervalMin: 60,
|
||||
isActive: true,
|
||||
lastIngestedDay: null,
|
||||
});
|
||||
mockListRssFeeds.mockResolvedValue([]);
|
||||
|
||||
const { default: TenderRadarSettingsPage } = await import('./page');
|
||||
render(<TenderRadarSettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/Abrufintervall/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('E-Mail-Alerts')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Benachrichtigungen')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user