Files
cameleer-server/ui/src/pages/AppsTab/AppDeploymentPage/ConfigTabs/readOnly-contract.test.tsx
hsiegeln 165c9f10e3
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m26s
CI / docker (push) Successful in 1m5s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
feat(deploy): externalRouting toggle to keep apps off Traefik
Adds a boolean `externalRouting` flag (default `true`) on
ResolvedContainerConfig. When `false`, TraefikLabelBuilder emits only
the identity labels (`managed-by`, `cameleer.*`) and skips every
`traefik.*` label, so the container is not published by Traefik.
Sibling containers on `cameleer-traefik` / `cameleer-env-{tenant}-{env}`
can still reach it via Docker DNS on whatever port the app listens on.

TDD: new TraefikLabelBuilderTest covers enabled (default labels present),
disabled (zero traefik.* labels), and disabled (identity labels retained)
cases. Full module unit suite: 208/0/0.

Plumbed through ConfigMerger read, DeploymentExecutor snapshot, UI form
state, Resources tab toggle, POST payload, and snapshot-to-form mapping.
Rule files updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:03:48 +02:00

270 lines
7.2 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@cameleer/design-system';
import type { ReactNode } from 'react';
import { MonitoringTab } from './MonitoringTab';
import { ResourcesTab } from './ResourcesTab';
import { VariablesTab } from './VariablesTab';
import { SensitiveKeysTab } from './SensitiveKeysTab';
import type {
MonitoringFormState,
ResourcesFormState,
VariablesFormState,
SensitiveKeysFormState,
} from '../hooks/useDeploymentPageState';
function wrap(ui: ReactNode) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider>{ui}</ThemeProvider>
</QueryClientProvider>,
);
}
const defaultMonitoring: MonitoringFormState = {
engineLevel: 'REGULAR',
payloadCaptureMode: 'BOTH',
payloadSize: '4',
payloadUnit: 'KB',
applicationLogLevel: 'INFO',
agentLogLevel: 'INFO',
metricsEnabled: true,
metricsInterval: '60',
samplingRate: '1.0',
compressSuccess: false,
replayEnabled: true,
routeControlEnabled: true,
};
const defaultResources: ResourcesFormState = {
memoryLimit: '512',
memoryReserve: '',
cpuRequest: '500',
cpuLimit: '',
appPort: '8080',
replicas: '1',
deployStrategy: 'blue-green',
stripPrefix: true,
sslOffloading: true,
externalRouting: true,
runtimeType: 'auto',
customArgs: '',
extraNetworks: [],
};
const defaultVariables: VariablesFormState = {
envVars: [],
};
const defaultSensitiveKeys: SensitiveKeysFormState = {
sensitiveKeys: [],
};
describe('ConfigTabs disabled contract', () => {
describe('MonitoringTab', () => {
it('disables all inputs and selects when disabled=true', () => {
wrap(
<MonitoringTab
value={defaultMonitoring}
onChange={vi.fn()}
disabled={true}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
const comboboxes = screen.queryAllByRole('combobox');
expect(textboxes.length).toBeGreaterThan(0);
expect(comboboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).toBeDisabled();
});
comboboxes.forEach((cb) => {
expect(cb).toBeDisabled();
});
});
it('enables inputs when disabled=false', () => {
wrap(
<MonitoringTab
value={defaultMonitoring}
onChange={vi.fn()}
disabled={false}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
expect(textboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).not.toBeDisabled();
});
});
});
describe('ResourcesTab', () => {
it('disables all inputs and selects when disabled=true', () => {
wrap(
<ResourcesTab
value={defaultResources}
onChange={vi.fn()}
disabled={true}
isProd={true}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
const comboboxes = screen.queryAllByRole('combobox');
expect(textboxes.length).toBeGreaterThan(0);
expect(comboboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).toBeDisabled();
});
comboboxes.forEach((cb) => {
expect(cb).toBeDisabled();
});
});
it('enables inputs when disabled=false', () => {
wrap(
<ResourcesTab
value={defaultResources}
onChange={vi.fn()}
disabled={false}
isProd={true}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
expect(textboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).not.toBeDisabled();
});
});
});
describe('VariablesTab', () => {
it('disables add and import buttons when disabled=true', () => {
wrap(
<VariablesTab
value={defaultVariables}
onChange={vi.fn()}
disabled={true}
/>,
);
const buttons = screen.queryAllByRole('button');
// When disabled, import and clear/add buttons should be disabled
// Copy button is always enabled by design
const importBtn = buttons.find((btn) => btn.title?.includes('Import'));
const clearBtn = buttons.find((btn) => btn.title?.includes('Clear'));
expect(importBtn).toBeDisabled();
expect(clearBtn).toBeDisabled();
});
it('enables inputs when disabled=false', () => {
wrap(
<VariablesTab
value={defaultVariables}
onChange={vi.fn()}
disabled={false}
/>,
);
const buttons = screen.queryAllByRole('button');
const importBtn = buttons.find((btn) => btn.title?.includes('Import'));
const clearBtn = buttons.find((btn) => btn.title?.includes('Clear'));
expect(importBtn).not.toBeDisabled();
expect(clearBtn).not.toBeDisabled();
});
it('with envVars populated, disables textboxes when disabled=true', () => {
const varState: VariablesFormState = {
envVars: [
{ key: 'TEST_VAR', value: 'test-value' },
{ key: 'ANOTHER', value: 'value' },
],
};
wrap(
<VariablesTab
value={varState}
onChange={vi.fn()}
disabled={true}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
expect(textboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).toBeDisabled();
});
});
});
describe('SensitiveKeysTab', () => {
it('disables input and add button when disabled=true', () => {
wrap(
<SensitiveKeysTab
value={defaultSensitiveKeys}
onChange={vi.fn()}
disabled={true}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
expect(textboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).toBeDisabled();
});
const buttons = screen.queryAllByRole('button');
const addButton = buttons.find((btn) => btn.textContent?.includes('Add'));
expect(addButton).toBeDisabled();
});
it('enables input when disabled=false', () => {
wrap(
<SensitiveKeysTab
value={defaultSensitiveKeys}
onChange={vi.fn()}
disabled={false}
/>,
);
const textboxes = screen.queryAllByRole('textbox');
expect(textboxes.length).toBeGreaterThan(0);
textboxes.forEach((box) => {
expect(box).not.toBeDisabled();
});
});
it('with sensitive keys, disables remove tag buttons when disabled=true', () => {
const skState: SensitiveKeysFormState = {
sensitiveKeys: ['Authorization', 'X-API-Key', '*password*'],
};
wrap(
<SensitiveKeysTab
value={skState}
onChange={vi.fn()}
disabled={true}
/>,
);
// Tags have remove buttons that should be disabled
const buttons = screen.queryAllByRole('button');
// Check that at least some buttons exist (the tag removers)
expect(buttons.length).toBeGreaterThan(0);
});
});
});