mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Block renderering non http(s):// links via <Icon> (#6588)
* Block renderering non http(s):// links via `<Icon>` Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Still render icon, just without href Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
1861fe2049
commit
56e7897bc4
@ -3,11 +3,13 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import type { CatalogCategorySpec } from "../../../../common/catalog";
|
import type { CatalogCategorySpec } from "../../../../common/catalog";
|
||||||
import { CatalogCategory } from "../../../../common/catalog";
|
import { CatalogCategory } from "../../../../common/catalog";
|
||||||
import { CatalogAddButton } from "../catalog-add-button";
|
import { CatalogAddButton } from "../catalog-add-button";
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import { type DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
|
||||||
class TestCatalogCategory extends CatalogCategory {
|
class TestCatalogCategory extends CatalogCategory {
|
||||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
@ -26,6 +28,14 @@ class TestCatalogCategory extends CatalogCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("CatalogAddButton", () => {
|
describe("CatalogAddButton", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
});
|
||||||
|
|
||||||
it("opens Add menu", async () => {
|
it("opens Add menu", async () => {
|
||||||
const category = new TestCatalogCategory();
|
const category = new TestCatalogCategory();
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,20 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import { Avatar } from "../avatar";
|
import { Avatar } from "../avatar";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import { type DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
|
||||||
describe("<Avatar/>", () => {
|
describe("<Avatar/>", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
});
|
||||||
|
|
||||||
test("renders w/o errors", () => {
|
test("renders w/o errors", () => {
|
||||||
const { container } = render(<Avatar title="John Ferguson"/>);
|
const { container } = render(<Avatar title="John Ferguson"/>);
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,22 @@
|
|||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { fireEvent, render } from "@testing-library/react";
|
import { fireEvent } from "@testing-library/react";
|
||||||
import { ToBottom } from "../to-bottom";
|
import { ToBottom } from "../to-bottom";
|
||||||
import { noop } from "../../../../utils";
|
import { noop } from "../../../../utils";
|
||||||
|
import type { DiRender } from "../../../test-utils/renderFor";
|
||||||
|
import { renderFor } from "../../../test-utils/renderFor";
|
||||||
|
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||||
|
|
||||||
describe("<ToBottom/>", () => {
|
describe("<ToBottom/>", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", () => {
|
||||||
const { container } = render(<ToBottom onClick={noop}/>);
|
const { container } = render(<ToBottom onClick={noop}/>);
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RenderResult } from "@testing-library/react";
|
import type { RenderResult } from "@testing-library/react";
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import { type DiRender, renderFor } from "../test-utils/renderFor";
|
||||||
import { DrawerParamToggler } from "./drawer-param-toggler";
|
import { DrawerParamToggler } from "./drawer-param-toggler";
|
||||||
|
|
||||||
describe("<DrawerParamToggler />", () => {
|
describe("<DrawerParamToggler />", () => {
|
||||||
let result: RenderResult;
|
let result: RenderResult;
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
result = render((
|
result = render((
|
||||||
<DrawerParamToggler
|
<DrawerParamToggler
|
||||||
label="Foo"
|
label="Foo"
|
||||||
|
|||||||
93
src/renderer/components/icon/icon.test.tsx
Normal file
93
src/renderer/components/icon/icon.test.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import type { Logger } from "../../../common/logger";
|
||||||
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import type { DiRender } from "../test-utils/renderFor";
|
||||||
|
import { renderFor } from "../test-utils/renderFor";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
|
describe("<Icon> href technical tests", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
let logger: jest.MockedObject<Logger>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
silly: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
di.override(loggerInjectable, () => logger);
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render an <Icon> with http href", () => {
|
||||||
|
const result = render((
|
||||||
|
<Icon
|
||||||
|
data-testid="my-icon"
|
||||||
|
href="http://localhost"
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const icon = result.queryByTestId("my-icon");
|
||||||
|
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
expect(icon).toHaveAttribute("href", "http://localhost");
|
||||||
|
expect(logger.warn).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render an <Icon> with https href", () => {
|
||||||
|
const result = render((
|
||||||
|
<Icon
|
||||||
|
data-testid="my-icon"
|
||||||
|
href="https://localhost"
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const icon = result.queryByTestId("my-icon");
|
||||||
|
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
expect(icon).toHaveAttribute("href", "https://localhost");
|
||||||
|
expect(logger.warn).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn about ws hrefs", () => {
|
||||||
|
const result = render((
|
||||||
|
<Icon
|
||||||
|
data-testid="my-icon"
|
||||||
|
href="ws://localhost"
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const icon = result.queryByTestId("my-icon");
|
||||||
|
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
expect(icon).not.toHaveAttribute("href", "ws://localhost");
|
||||||
|
expect(logger.warn).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn about javascript: hrefs", () => {
|
||||||
|
const result = render((
|
||||||
|
<Icon
|
||||||
|
data-testid="my-icon"
|
||||||
|
href="javascript:void 0"
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const icon = result.queryByTestId("my-icon");
|
||||||
|
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
expect(icon).not.toHaveAttribute("href", "javascript:void 0");
|
||||||
|
expect(logger.warn).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -34,6 +34,13 @@ import User from "./user.svg";
|
|||||||
import Users from "./users.svg";
|
import Users from "./users.svg";
|
||||||
import Wheel from "./wheel.svg";
|
import Wheel from "./wheel.svg";
|
||||||
import Workloads from "./workloads.svg";
|
import Workloads from "./workloads.svg";
|
||||||
|
import type { Logger } from "../../../common/logger";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
|
||||||
|
const hrefValidation = /https?:\/\//;
|
||||||
|
|
||||||
|
const hrefIsSafe = (href: string) => Boolean(href.match(hrefValidation));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping between the local file names and the svgs
|
* Mapping between the local file names and the svgs
|
||||||
@ -155,7 +162,11 @@ export function isSvg(content: string): boolean {
|
|||||||
return String(content).includes("<svg");
|
return String(content).includes("<svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
const RawIcon = withTooltip((props: IconProps) => {
|
interface Dependencies {
|
||||||
|
logger: Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RawIcon = (props: IconProps & Dependencies) => {
|
||||||
const ref = createRef<HTMLAnchorElement>();
|
const ref = createRef<HTMLAnchorElement>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -165,6 +176,7 @@ const RawIcon = withTooltip((props: IconProps) => {
|
|||||||
focusable = true,
|
focusable = true,
|
||||||
children,
|
children,
|
||||||
interactive, onClick, onKeyDown,
|
interactive, onClick, onKeyDown,
|
||||||
|
logger,
|
||||||
...elemProps
|
...elemProps
|
||||||
} = props;
|
} = props;
|
||||||
const isInteractive = interactive ?? !!(onClick || href || link);
|
const isInteractive = interactive ?? !!(onClick || href || link);
|
||||||
@ -245,6 +257,7 @@ const RawIcon = withTooltip((props: IconProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
|
if (hrefIsSafe(href)) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
{...iconProps}
|
{...iconProps}
|
||||||
@ -254,7 +267,17 @@ const RawIcon = withTooltip((props: IconProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.warn("[ICON]: href prop is unsafe, blocking", { href });
|
||||||
|
}
|
||||||
|
|
||||||
return <i {...iconProps} ref={ref} />;
|
return <i {...iconProps} ref={ref} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InjectedIcon = withInjectables<Dependencies, IconProps>(RawIcon, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
...props,
|
||||||
|
logger: di.inject(loggerInjectable),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Icon = Object.assign(RawIcon, { isSvg });
|
export const Icon = Object.assign(withTooltip(InjectedIcon), { isSvg });
|
||||||
|
|||||||
@ -5,9 +5,11 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { screen, waitFor } from "@testing-library/react";
|
||||||
import { ScrollSpy } from "../scroll-spy";
|
import { ScrollSpy } from "../scroll-spy";
|
||||||
import { RecursiveTreeView } from "../../tree-view";
|
import { RecursiveTreeView } from "../../tree-view";
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import { type DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
|
||||||
const observe = jest.fn();
|
const observe = jest.fn();
|
||||||
|
|
||||||
@ -20,6 +22,14 @@ Object.defineProperty(window, "IntersectionObserver", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("<ScrollSpy/>", () => {
|
describe("<ScrollSpy/>", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", () => {
|
||||||
const { container } = render((
|
const { container } = render((
|
||||||
<ScrollSpy
|
<ScrollSpy
|
||||||
@ -94,6 +104,14 @@ describe("<ScrollSpy/>", () => {
|
|||||||
|
|
||||||
|
|
||||||
describe("<TreeView/> dataTree inside <ScrollSpy/>", () => {
|
describe("<TreeView/> dataTree inside <ScrollSpy/>", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
});
|
||||||
|
|
||||||
it("contains links to all sections", async () => {
|
it("contains links to all sections", async () => {
|
||||||
render((
|
render((
|
||||||
<ScrollSpy
|
<ScrollSpy
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user