Skip to content

Commit 404bc05

Browse files
committed
Ensure PDF cache fallback is sanitized
1 parent 63c1f65 commit 404bc05

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

app/pdf-viewer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useRefToLatest } from "@/components/use-ref-to-latest";
99
import { useAuth } from "@/lib/auth-context";
1010
import { cacheFileDestination } from "@/lib/file-utils";
1111
import { updateMediaLocation } from "@/lib/media-location";
12-
import { File, Paths } from "expo-file-system";
12+
import { File } from "expo-file-system";
1313
import * as Sharing from "expo-sharing";
1414
import * as Sentry from "@sentry/react-native";
1515
import { useQueryClient } from "@tanstack/react-query";
@@ -54,7 +54,7 @@ export default function PdfViewerScreen() {
5454
const [isDownloading, setIsDownloading] = useState(true);
5555

5656
const downloadDestination = useCallback(
57-
() => (productFileId ? cacheFileDestination(productFileId, fileName ?? DEFAULT_PDF_FILE_NAME) : Paths.cache),
57+
() => cacheFileDestination(productFileId ?? "pdf-viewer", fileName ?? DEFAULT_PDF_FILE_NAME),
5858
[productFileId, fileName],
5959
);
6060

tests/app/pdf-viewer.test.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import { fireEvent, screen, waitFor } from "@testing-library/react-native";
22
import { renderWithQueryClient } from "../render-with-query-client";
33

4+
type MockSearchParams = {
5+
uri: string;
6+
title?: string;
7+
fileName?: string;
8+
productFileId?: string;
9+
};
10+
11+
const defaultSearchParams: MockSearchParams = {
12+
uri: "https://example.com/test.pdf",
13+
title: "Test PDF",
14+
fileName: "HOW TO BECOME AN ELITE PLAYER AND BE BETTER THAN 99%.pdf",
15+
productFileId: "pf1",
16+
};
17+
let mockSearchParams: MockSearchParams = { ...defaultSearchParams };
18+
419
jest.mock("expo-router", () => ({
5-
useLocalSearchParams: () => ({
6-
uri: "https://example.com/test.pdf",
7-
title: "Test PDF",
8-
fileName: "HOW TO BECOME AN ELITE PLAYER AND BE BETTER THAN 99%.pdf",
9-
productFileId: "pf1",
10-
}),
20+
useLocalSearchParams: () => mockSearchParams,
1121
Stack: {
1222
Screen: ({ options }: { options?: { headerRight?: () => React.ReactNode } }) => {
1323
const React = require("react");
@@ -101,6 +111,7 @@ describe("PdfViewerScreen", () => {
101111
beforeEach(() => {
102112
const { File } = require("expo-file-system");
103113
const Sharing = require("expo-sharing");
114+
mockSearchParams = { ...defaultSearchParams };
104115
mockOnError = null;
105116
File.downloadFileAsync.mockReset();
106117
File.downloadFileAsync.mockResolvedValue({ uri: "file:///cache/test.pdf" });
@@ -207,6 +218,24 @@ describe("PdfViewerScreen", () => {
207218
expect(destination.uri).not.toContain("%");
208219
});
209220

221+
it("downloads to a sanitized fallback cache destination when product file id is missing", async () => {
222+
const { File } = require("expo-file-system");
223+
mockSearchParams = {
224+
uri: "https://example.com/test.pdf",
225+
title: "Test PDF",
226+
fileName: "100% bonus #1.pdf",
227+
};
228+
229+
renderWithProviders();
230+
231+
await waitFor(() => expect(screen.getByTestId("pdf-component")).toBeTruthy());
232+
233+
const destination = File.downloadFileAsync.mock.calls[0][1];
234+
expect(destination.uri).toBe("/cache/pdf-viewer/100_ bonus _1.pdf");
235+
expect(destination.uri).not.toContain("%");
236+
expect(destination.uri).not.toContain("#");
237+
});
238+
210239
it("shares the cached PDF file when available", async () => {
211240
const { File } = require("expo-file-system");
212241
const Sharing = require("expo-sharing");

0 commit comments

Comments
 (0)