

























@@ -0,0 +1,124 @@
1+import type { Page } from "playwright-core";
2+import { afterEach, describe, expect, it, vi } from "vitest";
3+import { SsrFBlockedError } from "../infra/net/ssrf.js";
4+import {
5+assertBrowserNavigationRedirectChainAllowed,
6+assertBrowserNavigationResultAllowed,
7+} from "./navigation-guard.js";
8+import { assertPageNavigationCompletedSafely } from "./pw-session.js";
9+10+vi.mock("./navigation-guard.js", async (importOriginal) => {
11+const actual = await importOriginal<Record<string, unknown>>();
12+return {
13+ ...actual,
14+assertBrowserNavigationRedirectChainAllowed: vi.fn(async () => {}),
15+assertBrowserNavigationResultAllowed: vi.fn(async () => {}),
16+};
17+});
18+19+const mockedRedirectChain = vi.mocked(assertBrowserNavigationRedirectChainAllowed);
20+const mockedResultAllowed = vi.mocked(assertBrowserNavigationResultAllowed);
21+22+afterEach(() => {
23+mockedRedirectChain.mockReset();
24+mockedRedirectChain.mockImplementation(async () => {});
25+mockedResultAllowed.mockReset();
26+mockedResultAllowed.mockImplementation(async () => {});
27+});
28+29+function fakePage(url = "https://blocked.example/admin"): {
30+page: Page;
31+close: ReturnType<typeof vi.fn>;
32+} {
33+const close = vi.fn(async () => {});
34+const page = {
35+url: vi.fn(() => url),
36+ close,
37+} as unknown as Page;
38+return { page, close };
39+}
40+41+describe("assertPageNavigationCompletedSafely", () => {
42+it("does not close the tab when a read-only caller hits an SSRF-blocked URL (response: null)", async () => {
43+// A read-only caller (snapshot/screenshot/interactions) passes response: null
44+// and must never lose the user's tab when the policy guard rejects.
45+mockedResultAllowed.mockRejectedValueOnce(new SsrFBlockedError("blocked by policy"));
46+47+const { page, close } = fakePage();
48+49+await expect(
50+assertPageNavigationCompletedSafely({
51+cdpUrl: "http://127.0.0.1:18792",
52+ page,
53+response: null,
54+ssrfPolicy: { allowPrivateNetwork: false },
55+targetId: "tab-1",
56+}),
57+).rejects.toBeInstanceOf(SsrFBlockedError);
58+59+expect(close).not.toHaveBeenCalled();
60+});
61+62+it("does not close the tab when a navigate caller hits an SSRF-blocked URL (response: non-null)", async () => {
63+// Even when the helper is invoked with a real Response (i.e. on the
64+// navigate path), the close decision now belongs to the caller. The
65+// helper must only quarantine + rethrow; the caller's try/catch is
66+// responsible for closing if it owns the navigation lifecycle.
67+mockedResultAllowed.mockRejectedValueOnce(new SsrFBlockedError("blocked by policy"));
68+69+const { page, close } = fakePage();
70+const response = { request: () => undefined } as unknown as Parameters<
71+typeof assertPageNavigationCompletedSafely
72+>[0]["response"];
73+74+await expect(
75+assertPageNavigationCompletedSafely({
76+cdpUrl: "http://127.0.0.1:18792",
77+ page,
78+ response,
79+ssrfPolicy: { allowPrivateNetwork: false },
80+targetId: "tab-1",
81+}),
82+).rejects.toBeInstanceOf(SsrFBlockedError);
83+84+expect(close).not.toHaveBeenCalled();
85+});
86+87+it("rethrows non-policy errors without touching the tab", async () => {
88+const boom = new Error("transient playwright error");
89+mockedResultAllowed.mockRejectedValueOnce(boom);
90+91+const { page, close } = fakePage();
92+93+await expect(
94+assertPageNavigationCompletedSafely({
95+cdpUrl: "http://127.0.0.1:18792",
96+ page,
97+response: null,
98+ssrfPolicy: { allowPrivateNetwork: false },
99+targetId: "tab-1",
100+}),
101+).rejects.toBe(boom);
102+103+expect(close).not.toHaveBeenCalled();
104+});
105+106+it("returns silently when both guards pass", async () => {
107+const { page, close } = fakePage("https://allowed.example/");
108+109+await expect(
110+assertPageNavigationCompletedSafely({
111+cdpUrl: "http://127.0.0.1:18792",
112+ page,
113+response: null,
114+ssrfPolicy: { allowPrivateNetwork: false },
115+targetId: "tab-1",
116+}),
117+).resolves.toBeUndefined();
118+119+expect(close).not.toHaveBeenCalled();
120+expect(mockedResultAllowed).toHaveBeenCalledWith(
121+expect.objectContaining({ url: "https://allowed.example/" }),
122+);
123+});
124+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。