Skip to content

Commit 2e9b83a

Browse files
authored
Merge pull request #2835 from trycompai/main
[comp] Production Deploy
2 parents 4e863fb + a62c19f commit 2e9b83a

7 files changed

Lines changed: 330 additions & 127 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Checkbox } from '@trycompai/design-system';
2+
3+
interface AuthorizationConsentFieldProps {
4+
checked: boolean;
5+
onCheckedChange: (checked: boolean) => void;
6+
errorMessage?: string;
7+
}
8+
9+
export function AuthorizationConsentField({
10+
checked,
11+
onCheckedChange,
12+
errorMessage,
13+
}: AuthorizationConsentFieldProps) {
14+
const hasError = Boolean(errorMessage);
15+
const describedBy = hasError
16+
? 'pt-authorized-help pt-authorized-error'
17+
: 'pt-authorized-help';
18+
19+
return (
20+
<label
21+
htmlFor="pt-authorized"
22+
className="mb-4 flex cursor-pointer items-start gap-2.5 rounded border border-border bg-muted/30 p-3 text-xs leading-relaxed"
23+
>
24+
<Checkbox
25+
id="pt-authorized"
26+
checked={checked}
27+
onCheckedChange={(nextChecked) => onCheckedChange(nextChecked === true)}
28+
aria-describedby={describedBy}
29+
aria-invalid={hasError}
30+
/>
31+
<span className="min-w-0 flex-1">
32+
<span className="block font-medium text-foreground">
33+
I own this target or have written authorization to test it.
34+
</span>
35+
<span
36+
id="pt-authorized-help"
37+
className="mt-0.5 block text-[11px] text-muted-foreground"
38+
>
39+
Unauthorized testing may violate applicable computer-misuse laws and your
40+
provider's terms of service.
41+
</span>
42+
{hasError && (
43+
<span
44+
id="pt-authorized-error"
45+
role="alert"
46+
className="mt-1 block text-[11px] text-destructive"
47+
>
48+
{errorMessage}
49+
</span>
50+
)}
51+
</span>
52+
</label>
53+
);
54+
}

‎apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/CreateRunPanel.test.tsx‎

Lines changed: 122 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ vi.mock('sonner', () => ({
1616
toast: { error: vi.fn() },
1717
}));
1818

19+
async function confirmAuthorization(user: ReturnType<typeof userEvent.setup>) {
20+
await user.click(
21+
screen.getByText('I own this target or have written authorization to test it.'),
22+
);
23+
}
24+
25+
function checkInput(label: RegExp) {
26+
return screen.getByLabelText(label, { selector: 'input' });
27+
}
28+
1929
describe('CreateRunPanel', () => {
2030
beforeEach(() => {
2131
vi.clearAllMocks();
@@ -48,6 +58,7 @@ describe('CreateRunPanel', () => {
4858
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={onSubmit} />);
4959

5060
await user.type(screen.getByLabelText(/target url/i), 'app.example.com');
61+
await confirmAuthorization(user);
5162
await user.click(screen.getByRole('button', { name: /start scan/i }));
5263

5364
await waitFor(() => {
@@ -85,8 +96,7 @@ describe('CreateRunPanel', () => {
8596
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
8697

8798
await user.click(screen.getByText('Quick'));
88-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
89-
await user.click(screen.getByLabelText(/^xss$/i));
99+
await user.click(checkInput(/^xss$/i));
90100

91101
expect(screen.getByText('Custom · 11 min-25 min')).toBeInTheDocument();
92102
expect(screen.queryByText(/based on/i)).not.toBeInTheDocument();
@@ -103,20 +113,20 @@ describe('CreateRunPanel', () => {
103113
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={onSubmit} />);
104114

105115
await user.click(screen.getByText('Quick'));
106-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
107-
await user.click(validationLevel().getByLabelText(/safe proof/i));
108-
await user.click(screen.getByLabelText(/^xss$/i));
109-
await user.click(screen.getByLabelText(/^injection$/i));
110-
await user.click(screen.getByLabelText(/^authentication$/i));
111-
await user.click(screen.getByLabelText(/^authorization$/i));
112-
await user.click(screen.getByLabelText(/idor \/ bola/i));
113-
await user.click(screen.getByLabelText(/ssrf \/ xxe/i));
114-
await user.click(screen.getByLabelText(/^csrf$/i));
116+
await user.click(validationLevel().getByLabelText(/safe proof/i, { selector: 'input' }));
117+
await user.click(checkInput(/^xss$/i));
118+
await user.click(checkInput(/^injection$/i));
119+
await user.click(checkInput(/^authentication$/i));
120+
await user.click(checkInput(/^authorization$/i));
121+
await user.click(checkInput(/^idor \/ bola$/i));
122+
await user.click(checkInput(/^ssrf \/ xxe$/i));
123+
await user.click(checkInput(/^csrf$/i));
115124

116125
expect(screen.getByText('Standard · 30-90 min')).toBeInTheDocument();
117126
expect(screen.queryByText(/based on/i)).not.toBeInTheDocument();
118127

119128
await user.type(screen.getByLabelText(/target url/i), 'app.example.com');
129+
await confirmAuthorization(user);
120130
await user.click(screen.getByRole('button', { name: /start scan/i }));
121131

122132
await waitFor(() => {
@@ -142,12 +152,11 @@ describe('CreateRunPanel', () => {
142152
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
143153

144154
await user.click(screen.getByText('Quick'));
145-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
146-
await user.click(screen.getByLabelText(/^xss$/i));
155+
await user.click(checkInput(/^xss$/i));
147156

148157
expect(screen.getByText('Custom · 11 min-25 min')).toBeInTheDocument();
149158

150-
await user.click(validationLevel().getByLabelText(/safe proof/i));
159+
await user.click(validationLevel().getByLabelText(/safe proof/i, { selector: 'input' }));
151160

152161
expect(screen.getByText('Custom · 17 min-38 min')).toBeInTheDocument();
153162
});
@@ -158,10 +167,9 @@ describe('CreateRunPanel', () => {
158167
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
159168

160169
await user.click(screen.getByText('Quick'));
161-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
162-
await user.click(screen.getByLabelText(/secrets & info disclosure/i));
163-
await user.click(screen.getByLabelText(/technology config/i));
164-
await user.click(screen.getByLabelText(/^discovery$/i));
170+
await user.click(checkInput(/^secrets & info disclosure$/i));
171+
await user.click(checkInput(/^technology config$/i));
172+
await user.click(checkInput(/^discovery$/i));
165173

166174
expect(screen.getByText('Custom · 5 min-5 min')).toBeInTheDocument();
167175
});
@@ -172,13 +180,10 @@ describe('CreateRunPanel', () => {
172180
expect(screen.getByLabelText(/repository/i)).toBeInTheDocument();
173181
});
174182

175-
it('uses design-system radio styling for evidence options', async () => {
176-
const user = userEvent.setup();
183+
it('uses design-system radio styling for evidence options', () => {
177184
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
178185

179-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
180-
181-
expect(validationLevel().getByLabelText(/safe proof/i).closest('label')).toHaveClass(
186+
expect(validationLevel().getByLabelText(/safe proof/i, { selector: 'input' }).closest('label')).toHaveClass(
182187
'has-[[data-checked]]:border-primary',
183188
'has-[[data-checked]]:bg-primary/5',
184189
'has-[[data-checked]]:text-primary',
@@ -191,50 +196,51 @@ describe('CreateRunPanel', () => {
191196
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
192197

193198
await user.click(screen.getByText('Quick'));
194-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
195-
await user.click(screen.getByLabelText(/secrets & info disclosure/i));
196-
await user.click(screen.getByLabelText(/technology config/i));
197-
await user.click(screen.getByLabelText(/^discovery$/i));
198-
await user.click(screen.getByLabelText(/^xss$/i));
199+
await user.click(checkInput(/^secrets & info disclosure$/i));
200+
await user.click(checkInput(/^technology config$/i));
201+
await user.click(checkInput(/^discovery$/i));
202+
await user.click(checkInput(/^xss$/i));
199203

200-
expect(screen.getByLabelText(/^discovery$/i)).toBeChecked();
204+
expect(checkInput(/^discovery$/i)).toBeChecked();
201205
});
202206

203-
it('shows report-only helper text', async () => {
207+
it('shows report-only helper text when report_only is selected', async () => {
204208
const user = userEvent.setup();
205209

206210
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
207211

208212
await user.click(screen.getByText('Quick'));
209-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
210213

211214
expect(
212-
screen.getByText(/fastest and lowest risk/i),
215+
screen.getByText(/findings are reported without exploitation/i),
213216
).toBeInTheDocument();
214217
});
215218

216-
it('shows validation level choices and selected helper copy', async () => {
219+
it('renders the Scan coverage panel open by default and exposes validation level options', async () => {
217220
const user = userEvent.setup();
218221

219222
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
220223

221-
expect(screen.getByText('Customize scan')).toBeInTheDocument();
222-
223-
await user.click(screen.getByRole('button', { name: /customize scan/i }));
224-
224+
expect(screen.getByText('Scan coverage')).toBeInTheDocument();
225225
expect(screen.getByText('Validation level')).toBeInTheDocument();
226-
expect(validationLevel().getByLabelText(/report only/i)).toBeInTheDocument();
227-
expect(validationLevel().getByLabelText(/safe proof/i)).toBeInTheDocument();
228-
expect(validationLevel().getByLabelText(/impact proof/i)).toBeInTheDocument();
226+
expect(validationLevel().getByLabelText(/report only/i, { selector: 'input' })).toBeInTheDocument();
227+
expect(validationLevel().getByLabelText(/safe proof/i, { selector: 'input' })).toBeInTheDocument();
228+
expect(validationLevel().getByLabelText(/impact proof/i, { selector: 'input' })).toBeInTheDocument();
229229

230-
await user.click(validationLevel().getByLabelText(/report only/i));
231-
expect(screen.getByText(/fastest and lowest risk/i)).toBeInTheDocument();
230+
await user.click(validationLevel().getByLabelText(/report only/i, { selector: 'input' }));
231+
expect(
232+
screen.getByText(/findings are reported without exploitation/i),
233+
).toBeInTheDocument();
232234

233-
await user.click(validationLevel().getByLabelText(/safe proof/i));
234-
expect(screen.getByText(/balanced validation/i)).toBeInTheDocument();
235+
await user.click(validationLevel().getByLabelText(/safe proof/i, { selector: 'input' }));
236+
expect(
237+
screen.getByText(/findings are validated with non-destructive proofs/i),
238+
).toBeInTheDocument();
235239

236-
await user.click(validationLevel().getByLabelText(/impact proof/i));
237-
expect(screen.getByText(/highest confidence, longer runtime/i)).toBeInTheDocument();
240+
await user.click(validationLevel().getByLabelText(/impact proof/i, { selector: 'input' }));
241+
expect(
242+
screen.getByText(/findings are validated with active exploitation/i),
243+
).toBeInTheDocument();
238244
});
239245

240246
it('keeps preset cards concise without validation metadata', () => {
@@ -245,21 +251,64 @@ describe('CreateRunPanel', () => {
245251
expect(screen.queryByText('Impact proof · 12 checks')).not.toBeInTheDocument();
246252
});
247253

248-
it('requires confirmation for Deep profile before submit', async () => {
254+
it('blocks submit when authorization is not confirmed', async () => {
255+
const user = userEvent.setup();
256+
const onSubmit = vi.fn(async () => ({ id: 'never' }));
257+
258+
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={onSubmit} />);
259+
260+
await user.type(screen.getByLabelText(/target url/i), 'app.example.com');
261+
await user.click(screen.getByRole('button', { name: /start scan/i }));
262+
263+
await waitFor(() => {
264+
expect(
265+
screen.getByText(/confirm you own or are authorized to test this target/i),
266+
).toBeInTheDocument();
267+
});
268+
expect(onSubmit).not.toHaveBeenCalled();
269+
});
270+
271+
it('exposes authorization error to assistive tech via aria-invalid and aria-describedby', async () => {
272+
const user = userEvent.setup();
273+
274+
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
275+
276+
const checkbox = screen.getByRole('checkbox', { name: /i own this target/i });
277+
expect(checkbox).toHaveAttribute('aria-invalid', 'false');
278+
expect(checkbox).toHaveAttribute('aria-describedby', 'pt-authorized-help');
279+
280+
await user.type(screen.getByLabelText(/target url/i), 'app.example.com');
281+
await user.click(screen.getByRole('button', { name: /start scan/i }));
282+
283+
await waitFor(() => {
284+
expect(checkbox).toHaveAttribute('aria-invalid', 'true');
285+
});
286+
expect(checkbox.getAttribute('aria-describedby')).toContain('pt-authorized-error');
287+
288+
const alert = screen.getByRole('alert');
289+
expect(alert).toHaveAttribute('id', 'pt-authorized-error');
290+
expect(alert).toHaveTextContent(/confirm you own or are authorized to test this target/i);
291+
});
292+
293+
it('requires confirmation for impact-proof validation before submit', async () => {
249294
const user = userEvent.setup();
250295
const onSubmit = vi.fn(async () => ({ id: 'run_deep' }));
251296

252297
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={onSubmit} />);
253298

254299
await user.type(screen.getByLabelText(/target url/i), 'app.example.com');
255300
await user.click(screen.getByText('Deep'));
301+
await confirmAuthorization(user);
256302
await user.click(screen.getByRole('button', { name: /start scan/i }));
257303

258304
expect(onSubmit).not.toHaveBeenCalled();
259305
const dialog = screen.getByRole('alertdialog');
260-
expect(within(dialog).getByText(/confirm scan intensity/i)).toBeInTheDocument();
306+
expect(within(dialog).getByText(/confirm impact-proof scan/i)).toBeInTheDocument();
307+
expect(
308+
within(dialog).getByText(/actively exploits findings/i),
309+
).toBeInTheDocument();
261310

262-
await user.click(within(dialog).getByRole('button', { name: /start scan/i }));
311+
await user.click(within(dialog).getByRole('button', { name: /run impact-proof scan/i }));
263312

264313
await waitFor(() => {
265314
expect(onSubmit).toHaveBeenCalledWith(
@@ -270,6 +319,30 @@ describe('CreateRunPanel', () => {
270319
);
271320
});
272321
});
322+
323+
it('does not show the impact-proof modal for Standard profile', async () => {
324+
const user = userEvent.setup();
325+
const onSubmit = vi.fn(async () => ({ id: 'run_standard' }));
326+
327+
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={onSubmit} />);
328+
329+
await user.type(screen.getByLabelText(/target url/i), 'app.example.com');
330+
await confirmAuthorization(user);
331+
await user.click(screen.getByRole('button', { name: /start scan/i }));
332+
333+
await waitFor(() => {
334+
expect(onSubmit).toHaveBeenCalled();
335+
});
336+
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
337+
});
338+
339+
it('exposes a tooltip trigger for each vulnerability check', () => {
340+
render(<CreateRunPanel orgId="org_1" balance={1} onSubmit={vi.fn()} />);
341+
342+
expect(screen.getByRole('button', { name: /about xss/i })).toBeInTheDocument();
343+
expect(screen.getByRole('button', { name: /about csrf/i })).toBeInTheDocument();
344+
expect(screen.getByRole('button', { name: /about business logic/i })).toBeInTheDocument();
345+
});
273346
});
274347

275348
function validationLevel() {

0 commit comments

Comments
 (0)