Skip to content

Commit

Permalink
Merge pull request #10293 from marmelab/fix-double-decoding-ids
Browse files Browse the repository at this point in the history
Fix double decoding of ids in URLs
  • Loading branch information
slax57 authored Oct 24, 2024
2 parents a8ed9f3 + 7117be5 commit 0d1e162
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 70 deletions.
58 changes: 24 additions & 34 deletions packages/ra-core/src/controller/edit/useEditController.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
CanAccess,
DisableAuthentication,
} from './useEditController.security.stories';
import { EncodedId } from './useEditController.stories';

describe('useEditController', () => {
const defaultProps = {
Expand Down Expand Up @@ -55,42 +56,31 @@ describe('useEditController', () => {
});
});

it('should decode the id from the route params', async () => {
const getOne = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({ data: { id: 'test?', title: 'hello' } })
);
const dataProvider = { getOne } as unknown as DataProvider;
it.each([
{ id: 'test?', url: '/posts/test%3F' },
{ id: 'test%', url: '/posts/test%25' },
])(
'should decode the id $id from the route params',
async ({ id, url }) => {
const getOne = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({ data: { id, title: 'hello' } })
);
const dataProvider = { getOne } as unknown as DataProvider;

render(
<TestMemoryRouter initialEntries={['/posts/test%3F']}>
<CoreAdminContext dataProvider={dataProvider}>
<Routes>
<Route
path="/posts/:id"
element={
<EditController resource="posts">
{({ record }) => (
<div>{record && record.title}</div>
)}
</EditController>
}
/>
</Routes>
</CoreAdminContext>
</TestMemoryRouter>
);
await waitFor(() => {
expect(getOne).toHaveBeenCalledWith('posts', {
id: 'test?',
signal: undefined,
render(<EncodedId id={id} url={url} dataProvider={dataProvider} />);
await waitFor(() => {
expect(getOne).toHaveBeenCalledWith('posts', {
id,
signal: undefined,
});
});
});
await waitFor(() => {
expect(screen.queryAllByText('hello')).toHaveLength(1);
});
});
await waitFor(() => {
expect(screen.queryAllByText('Title: hello')).toHaveLength(1);
});
}
);

it('should use the id provided through props if any', async () => {
const getOne = jest
Expand Down
85 changes: 85 additions & 0 deletions packages/ra-core/src/controller/edit/useEditController.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as React from 'react';
import { Route, Routes, useLocation } from 'react-router';
import {
CoreAdminContext,
EditController,
testDataProvider,
TestMemoryRouter,
} from '../..';

export default {
title: 'ra-core/controller/useEditController',
};

export const EncodedId = ({
id = 'test?',
url = '/posts/test%3F',
dataProvider = testDataProvider({
// @ts-expect-error
getOne: () => Promise.resolve({ data: { id, title: 'hello' } }),
}),
}) => {
return (
<TestMemoryRouter initialEntries={[url]}>
<CoreAdminContext dataProvider={dataProvider}>
<Routes>
<Route
path="/posts/:id"
element={
<EditController resource="posts">
{({ record }) => (
<>
<LocationInspector />
<p>Id: {record && record.id}</p>
<p>Title: {record && record.title}</p>
</>
)}
</EditController>
}
/>
</Routes>
</CoreAdminContext>
</TestMemoryRouter>
);
};

export const EncodedIdWithPercentage = ({
id = 'test%',
url = '/posts/test%25',
dataProvider = testDataProvider({
// @ts-expect-error
getOne: () => Promise.resolve({ data: { id, title: 'hello' } }),
}),
}) => {
return (
<TestMemoryRouter initialEntries={[url]}>
<CoreAdminContext dataProvider={dataProvider}>
<Routes>
<Route
path="/posts/:id"
element={
<EditController resource="posts">
{({ record }) => (
<>
<LocationInspector />
<p>Id: {record && record.id}</p>
<p>Title: {record && record.title}</p>
</>
)}
</EditController>
}
/>
</Routes>
</CoreAdminContext>
</TestMemoryRouter>
);
};

const LocationInspector = () => {
const location = useLocation();
return (
<p>
Location: <code>{location.pathname}</code>
</p>
);
};
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/edit/useEditController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const useEditController = <
'useEditController requires an id prop or a route with an /:id? parameter.'
);
}
const id = propsId ?? decodeURIComponent(routeId!);
const id = propsId ?? routeId;
const { meta: queryMeta, ...otherQueryOptions } = queryOptions;
const {
meta: mutationMeta,
Expand Down
58 changes: 24 additions & 34 deletions packages/ra-core/src/controller/show/useShowController.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CanAccess,
DisableAuthentication,
} from './useShowController.security.stories';
import { EncodedId } from './useShowController.stories';

describe('useShowController', () => {
const defaultProps = {
Expand Down Expand Up @@ -41,41 +42,30 @@ describe('useShowController', () => {
});
});

it('should decode the id from the route params', async () => {
const getOne = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({ data: { id: 'test?', title: 'hello' } })
);
const dataProvider = { getOne } as unknown as DataProvider;
render(
<TestMemoryRouter initialEntries={['/posts/test%3F']}>
<CoreAdminContext dataProvider={dataProvider}>
<Routes>
<Route
path="posts/:id"
element={
<ShowController resource="posts">
{({ record }) => (
<div>{record && record.title}</div>
)}
</ShowController>
}
/>
</Routes>
</CoreAdminContext>
</TestMemoryRouter>
);
await waitFor(() => {
expect(getOne).toHaveBeenCalledWith('posts', {
id: 'test?',
signal: undefined,
it.each([
{ id: 'test?', url: '/posts/test%3F' },
{ id: 'test%', url: '/posts/test%25' },
])(
'should decode the id $id from the route params',
async ({ id, url }) => {
const getOne = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({ data: { id, title: 'hello' } })
);
const dataProvider = { getOne } as unknown as DataProvider;
render(<EncodedId id={id} url={url} dataProvider={dataProvider} />);
await waitFor(() => {
expect(getOne).toHaveBeenCalledWith('posts', {
id,
signal: undefined,
});
});
});
await waitFor(() => {
expect(screen.queryAllByText('hello')).toHaveLength(1);
});
});
await waitFor(() => {
expect(screen.queryAllByText('Title: hello')).toHaveLength(1);
});
}
);

it('should use the id provided through props if any', async () => {
const getOne = jest
Expand Down
85 changes: 85 additions & 0 deletions packages/ra-core/src/controller/show/useShowController.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as React from 'react';
import { Route, Routes, useLocation } from 'react-router';
import {
CoreAdminContext,
ShowController,
testDataProvider,
TestMemoryRouter,
} from '../..';

export default {
title: 'ra-core/controller/useShowController',
};

export const EncodedId = ({
id = 'test?',
url = '/posts/test%3F',
dataProvider = testDataProvider({
// @ts-expect-error
getOne: () => Promise.resolve({ data: { id, title: 'hello' } }),
}),
}) => {
return (
<TestMemoryRouter initialEntries={[url]}>
<CoreAdminContext dataProvider={dataProvider}>
<Routes>
<Route
path="/posts/:id"
element={
<ShowController resource="posts">
{({ record }) => (
<>
<LocationInspector />
<p>Id: {record && record.id}</p>
<p>Title: {record && record.title}</p>
</>
)}
</ShowController>
}
/>
</Routes>
</CoreAdminContext>
</TestMemoryRouter>
);
};

export const EncodedIdWithPercentage = ({
id = 'test%',
url = '/posts/test%25',
dataProvider = testDataProvider({
// @ts-expect-error
getOne: () => Promise.resolve({ data: { id, title: 'hello' } }),
}),
}) => {
return (
<TestMemoryRouter initialEntries={[url]}>
<CoreAdminContext dataProvider={dataProvider}>
<Routes>
<Route
path="/posts/:id"
element={
<ShowController resource="posts">
{({ record }) => (
<>
<LocationInspector />
<p>Id: {record && record.id}</p>
<p>Title: {record && record.title}</p>
</>
)}
</ShowController>
}
/>
</Routes>
</CoreAdminContext>
</TestMemoryRouter>
);
};

const LocationInspector = () => {
const location = useLocation();
return (
<p>
Location: <code>{location.pathname}</code>
</p>
);
};
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/show/useShowController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const useShowController = <RecordType extends RaRecord = any>(
'useShowController requires an id prop or a route with an /:id? parameter.'
);
}
const id = propsId != null ? propsId : decodeURIComponent(routeId!);
const id = propsId != null ? propsId : routeId;
const { meta, ...otherQueryOptions } = queryOptions;

const {
Expand Down

0 comments on commit 0d1e162

Please sign in to comment.