diff --git a/apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx b/apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx
new file mode 100644
index 0000000..0849d05
--- /dev/null
+++ b/apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx
@@ -0,0 +1,136 @@
+import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+// Mock @/lib/tender-radar-api
+const mockFetchSourceConfig = vi.fn();
+const mockSaveSourceConfig = vi.fn();
+
+vi.mock('@/lib/tender-radar-api', () => ({
+ fetchSourceConfig: (...args: unknown[]) => mockFetchSourceConfig(...args),
+ saveSourceConfig: (...args: unknown[]) => mockSaveSourceConfig(...args),
+}));
+
+afterEach(() => {
+ cleanup();
+ // Reset call history AND implementations between tests — restoreAllMocks
+ // alone does not clear vi.fn() call counts for these module-level mocks,
+ // which would otherwise leak a prior test's saveSourceConfig call into a
+ // later "not.toHaveBeenCalled()" assertion.
+ mockFetchSourceConfig.mockReset();
+ mockSaveSourceConfig.mockReset();
+ vi.restoreAllMocks();
+});
+
+describe('SourceConfigForm', () => {
+ it('fetches config on mount and renders the returned pollIntervalMin in the interval input', async () => {
+ mockFetchSourceConfig.mockResolvedValue({
+ sourceType: 'doe-opendata',
+ pollIntervalMin: 90,
+ isActive: true,
+ lastIngestedDay: null,
+ });
+
+ const { SourceConfigForm } = await import('./SourceConfigForm');
+ render();
+
+ await waitFor(() => {
+ expect(
+ screen.getByLabelText(/Abrufintervall/i) as HTMLInputElement,
+ ).toHaveValue(90);
+ });
+
+ expect(mockFetchSourceConfig).toHaveBeenCalledTimes(1);
+ });
+
+ it('editing the interval and clicking Speichern calls saveSourceConfig with the new pollIntervalMin and isActive', async () => {
+ mockFetchSourceConfig.mockResolvedValue({
+ sourceType: 'doe-opendata',
+ pollIntervalMin: 60,
+ isActive: false,
+ lastIngestedDay: '2026-07-20T00:00:00.000Z',
+ });
+ mockSaveSourceConfig.mockResolvedValue({
+ sourceType: 'doe-opendata',
+ pollIntervalMin: 120,
+ isActive: true,
+ lastIngestedDay: '2026-07-20T00:00:00.000Z',
+ });
+
+ const { SourceConfigForm } = await import('./SourceConfigForm');
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByLabelText(/Abrufintervall/i)).toHaveValue(60);
+ });
+
+ fireEvent.change(screen.getByLabelText(/Abrufintervall/i), {
+ target: { value: '120' },
+ });
+ fireEvent.click(screen.getByRole('switch'));
+
+ fireEvent.click(screen.getByRole('button', { name: 'Speichern' }));
+
+ await waitFor(() => {
+ expect(mockSaveSourceConfig).toHaveBeenCalledWith({
+ pollIntervalMin: 120,
+ isActive: true,
+ });
+ });
+ });
+
+ it('rejects an interval below 5 client-side without calling saveSourceConfig', async () => {
+ mockFetchSourceConfig.mockResolvedValue({
+ sourceType: 'doe-opendata',
+ pollIntervalMin: 60,
+ isActive: true,
+ lastIngestedDay: null,
+ });
+
+ const { SourceConfigForm } = await import('./SourceConfigForm');
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByLabelText(/Abrufintervall/i)).toHaveValue(60);
+ });
+
+ fireEvent.change(screen.getByLabelText(/Abrufintervall/i), {
+ target: { value: '2' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Speichern' }));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Intervall muss zwischen 5 und 1440 Minuten liegen/i),
+ ).toBeInTheDocument();
+ });
+ expect(mockSaveSourceConfig).not.toHaveBeenCalled();
+ });
+
+ it('rejects an interval above 1440 client-side without calling saveSourceConfig', async () => {
+ mockFetchSourceConfig.mockResolvedValue({
+ sourceType: 'doe-opendata',
+ pollIntervalMin: 60,
+ isActive: true,
+ lastIngestedDay: null,
+ });
+
+ const { SourceConfigForm } = await import('./SourceConfigForm');
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByLabelText(/Abrufintervall/i)).toHaveValue(60);
+ });
+
+ fireEvent.change(screen.getByLabelText(/Abrufintervall/i), {
+ target: { value: '1441' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Speichern' }));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Intervall muss zwischen 5 und 1440 Minuten liegen/i),
+ ).toBeInTheDocument();
+ });
+ expect(mockSaveSourceConfig).not.toHaveBeenCalled();
+ });
+});