Skip to content

Commit

Permalink
chore(dashboard): add tests for text widget to include url sanitization
Browse files Browse the repository at this point in the history
  • Loading branch information
ssjagad committed Aug 8, 2024
1 parent f766a3b commit 8273c9a
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 159 deletions.
32 changes: 32 additions & 0 deletions packages/dashboard/e2e/tests/widgets/text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,5 +280,37 @@ test.describe('Test Text Widget', () => {
await page.getByText(TEXT_WIDGET_CONTENT).click();
await page.waitForURL(`**${url}`);
});

test('when link is enabled, it should sanitize href and not link if dangerous', async ({
dashboardWithTextWidget,
configPanel,
page,
}) => {
const widget = dashboardWithTextWidget.gridArea.locator(
'[data-gesture=widget]'
);

const url = "javascript://%0aalert('1.com')";
await configPanel.collapsedButton.click();
await widget.dblclick();
await widget.getByRole('textbox').fill(TEXT_WIDGET_CONTENT);
await configPanel.container
.getByTestId('text-widget-link-header')
.click();
await configPanel.container
.getByText('Create link', { exact: true })
.click();
await configPanel.container
.getByLabel('text widget link input')
.fill(url);

// go to preview
await page.getByRole('button', { name: 'preview' }).click();

// clicking on the link
await page.getByText(TEXT_WIDGET_CONTENT).click();

expect(page.getByText(TEXT_WIDGET_CONTENT)).not.toHaveAttribute('href');
});
});
});

This file was deleted.

163 changes: 87 additions & 76 deletions packages/dashboard/src/customization/widgets/text/component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from 'react';

import { render } from '@testing-library/react';
import { useSelector, useDispatch } from 'react-redux';
import { render, screen } from '@testing-library/react';
import TextWidgetComponent from './component';
import { useIsSelected } from '~/customization/hooks/useIsSelected';
import { onChangeDashboardGridEnabledAction } from '~/store/actions';
import * as ReactRedux from 'react-redux';
import type { TextProperties } from '../types';

jest.mock('~/store/actions', () => ({
...jest.requireActual('~/store/actions'),
Expand All @@ -13,88 +11,101 @@ jest.mock('~/store/actions', () => ({

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
useDispatch: jest.fn(),
useDispatch: () => jest.fn(),
}));

jest.mock('~/customization/hooks/useIsSelected', () => ({
useIsSelected: jest.fn(),
}));

jest.mock('./link', () => (props: unknown) => (
<div data-mocked='TextLink'>{JSON.stringify(props)}</div>
));
jest.mock('./styledText/textArea', () => (props: unknown) => (
<div data-mocked='StyledTextArea'>{JSON.stringify(props)}</div>
));
jest.mock('./styledText/editableText', () => (props: unknown) => (
<div data-mocked='EditableStyledText'>{JSON.stringify(props)}</div>
));
jest.mock('./styledText', () => (props: unknown) => (
<div data-mocked='StyledText'>{JSON.stringify(props)}</div>
));
function TextTextWidget(props: TextProperties) {
return (
<TextWidgetComponent
id='id'
type='text-widget'
x={0}
y={0}
z={0}
height={100}
width={100}
properties={props}
/>
);
}

function TestReadonlyTextWidget(props: TextProperties) {
jest.spyOn(ReactRedux, 'useSelector').mockReturnValueOnce(true);

return <TextTextWidget {...props} />;
}

function TestEditableTextWidget(props: TextProperties) {
jest.spyOn(ReactRedux, 'useSelector').mockReturnValueOnce(false);

return <TextTextWidget {...props} />;
}

describe('Text Widget', () => {
beforeEach(() => {
jest.resetAllMocks();
it('should render readonly text', () => {
const text = 'text';
render(<TestReadonlyTextWidget value={text} />);

const textWidget = screen.getByText(text);
expect(textWidget).toBeVisible();
expect(textWidget).toHaveAttribute('aria-readonly');
expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});

[
{ readOnly: true, isSelected: true, isUrl: true },
{ readOnly: true, isSelected: true, isUrl: false },
{ readOnly: true, isSelected: false, isUrl: false },
{ readOnly: false, isSelected: false, isUrl: false },
{ readOnly: false, isSelected: false, isUrl: true },
{ readOnly: false, isSelected: true, isUrl: true },
{ readOnly: false, isSelected: true, isUrl: false },
{ readOnly: true, isSelected: false, isUrl: true },
].forEach((configuration) => {
it(`should render ${JSON.stringify(configuration)} correctly`, () => {
const { isSelected, readOnly, isUrl } = configuration;
(useIsSelected as jest.Mock).mockImplementation(() => isSelected);
(useSelector as jest.Mock).mockImplementation(() => readOnly);
(useDispatch as jest.Mock).mockReturnValue(jest.fn());

const { container } = render(
<TextWidgetComponent
id='some-id'
x={1}
y={2}
z={3}
height={100}
width={100}
type='text-widget'
properties={{ isUrl, value: 'abc' }}
/>
);

expect(container).toMatchSnapshot();
});
it('should render editable text', () => {
const text = 'text';
render(<TestEditableTextWidget value={text} />);

const textWidget = screen.getByText(text);
expect(textWidget).toBeVisible();
expect(textWidget).not.toHaveAttribute('aria-readonly');
expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});

it('should exit edit mode when unmounted', () => {
(useDispatch as jest.Mock).mockImplementation(() => jest.fn((cb) => cb())); // short curcuit dispatch

(useIsSelected as jest.Mock).mockImplementation(() => false);
(useSelector as jest.Mock).mockImplementation(() => false);
(useDispatch as jest.Mock).mockReturnValue(jest.fn());

const { unmount } = render(
<TextWidgetComponent
id='some-id'
x={1}
y={2}
z={3}
height={100}
width={100}
type='text-widget'
properties={{ isUrl: false, value: 'abc' }}
/>
);
unmount();

expect(onChangeDashboardGridEnabledAction).toBeCalledWith({
enabled: true,
});
it('should render editable link', () => {
const text = 'text';
const href = 'https://test.com';
render(<TestEditableTextWidget value={text} href={href} isUrl />);

const textWidget = screen.getByText(text);
expect(textWidget).toHaveTextContent(text);
expect(textWidget).not.toHaveAttribute('aria-readonly');
expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});

it('should santize and then render unsafe link that injects javascript', () => {
const text = 'text';
const href = 'javascript://%0Aalert(1)';
render(<TestReadonlyTextWidget value={text} href={href} isUrl />);

const textWidget = screen.getByText(text);
expect(textWidget).toBeVisible();
expect(textWidget).not.toHaveAttribute('href');
expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});

it('should santize and then render unsafe link that injects javascript via hex format', () => {
const text = 'text';
const href = '\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3aalert(1)';
render(<TestReadonlyTextWidget value={text} href={href} isUrl />);

const textWidget = screen.getByText(text);
expect(textWidget).toBeVisible();
expect(textWidget).not.toHaveAttribute('href');
expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});

it('should render a safe link', () => {
const text = 'text';
const href = 'https://test.com';
render(<TestReadonlyTextWidget value={text} href={href} isUrl />);

const textWidget = screen.getByRole('link', { name: text });
expect(textWidget).toBeVisible();
expect(textWidget).toHaveAttribute('href', href);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const TextWidgetComponent: React.FC<TextWidget> = (widget) => {
const { isUrl, value } = widget.properties;

const dispatch = useDispatch();

const [isEditing, setIsEditing] = useState(false);

const handleSetEdit = useCallback(
Expand Down Expand Up @@ -50,7 +51,7 @@ const TextWidgetComponent: React.FC<TextWidget> = (widget) => {
if (isUrl) {
return <TextLink {...widget} />;
} else {
return <StyledText {...widget} />;
return <StyledText {...widget} readonly />;
}
} else {
if (isUrl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { spaceScaledXs } from '@cloudscape-design/design-tokens';
type StyledTextProps = TextWidget & {
onPointerDown?: PointerEventHandler;
onPointerUp?: PointerEventHandler;
readonly?: boolean;
};

const StyledText: React.FC<StyledTextProps> = ({
onPointerDown,
onPointerUp,
readonly,
...widget
}) => {
const { value } = widget.properties;
Expand Down Expand Up @@ -43,7 +45,12 @@ const StyledText: React.FC<StyledTextProps> = ({
};

return (
<p {...pointerListeners} className={className} style={style}>
<p
{...pointerListeners}
className={className}
style={style}
aria-readonly={readonly}
>
{textContent}
</p>
);
Expand Down

0 comments on commit 8273c9a

Please sign in to comment.