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>
270 lines
7.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|