Skip to content
4 changes: 3 additions & 1 deletion Agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ React-admin is a comprehensive frontend framework for building B2B and admin app
- No children inspection — violates React patterns (exception: Datagrid)
- No features achievable in pure React — keep the API surface small
- No comments when code is self-explanatory
- No dead code — trust your preconditions. Don't guard against conditions that prior code already prevents
- DRY — don't duplicate knowledge. Coincidental code similarity is not duplication. Only deduplicate when the same decision or fact is expressed in multiple places. Code that looks alike but could evolve independently should stay separate

## Codebase Organization

Expand Down Expand Up @@ -83,4 +85,4 @@ Every new feature or API change must be documented.
make lint # ESLint checks
make typecheck # TypeScript type checking
make prettier # Prettier formatting
```
```
100 changes: 100 additions & 0 deletions packages/ra-data-local-forage/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import expect from 'expect';
import localforage from 'localforage';

import localForageDataProvider from './index';

jest.mock('localforage', () => ({
__esModule: true,
default: {
keys: jest.fn(),
getItem: jest.fn(),
setItem: jest.fn(),
},
}));

describe('ra-data-local-forage', () => {
beforeEach(() => {
jest.resetAllMocks();
(localforage.keys as jest.Mock).mockResolvedValue([]);
(localforage.getItem as jest.Mock).mockResolvedValue(undefined);
(localforage.setItem as jest.Mock).mockResolvedValue(undefined);
});

it('creates missing resource collections safely', async () => {
const dataProvider = localForageDataProvider();

const response = await dataProvider.create('posts', {
data: { title: 'Hello world' },
} as any);

expect(response.data.title).toEqual('Hello world');
expect(localforage.setItem).toHaveBeenCalledWith(
'ra-data-local-forage-posts',
[expect.objectContaining({ title: 'Hello world' })]
);
});

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in update',
async unsafeKey => {
const dataProvider = localForageDataProvider();
await expect(
dataProvider.update(unsafeKey, {
id: 1,
data: { title: 'bad' },
previousData: { id: 1 },
} as any)
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in updateMany',
async unsafeKey => {
const dataProvider = localForageDataProvider();
await expect(
dataProvider.updateMany(unsafeKey, {
ids: [1],
data: { title: 'bad' },
} as any)
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in create',
async unsafeKey => {
const dataProvider = localForageDataProvider();
await expect(
dataProvider.create(unsafeKey, {
data: { title: 'bad' },
} as any)
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in delete',
async unsafeKey => {
const dataProvider = localForageDataProvider();
await expect(
dataProvider.delete(unsafeKey, {
id: 1,
previousData: { id: 1 },
} as any)
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in deleteMany',
async unsafeKey => {
const dataProvider = localForageDataProvider();
await expect(
dataProvider.deleteMany(unsafeKey, {
ids: [1],
} as any)
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
}
);
});
95 changes: 95 additions & 0 deletions packages/ra-data-local-storage/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import expect from 'expect';

import localStorageDataProvider from './index';

describe('ra-data-local-storage', () => {
beforeEach(() => {
localStorage.clear();
});

it('creates missing resource collections safely', async () => {
const dataProvider = localStorageDataProvider({
localStorageKey: 'ra-data-local-storage-test',
localStorageUpdateDelay: 0,
});

const response = await dataProvider.create('posts', {
data: { title: 'Hello world' },
} as any);

await new Promise(resolve => setTimeout(resolve, 0));

expect(response.data.title).toEqual('Hello world');
expect(
JSON.parse(
localStorage.getItem('ra-data-local-storage-test') || '{}'
)
).toMatchObject({
posts: [expect.objectContaining({ title: 'Hello world' })],
});
});

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in update',
unsafeKey => {
const dataProvider = localStorageDataProvider();
expect(() =>
dataProvider.update(unsafeKey, {
id: 1,
data: { title: 'bad' },
previousData: { id: 1 },
} as any)
).toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in updateMany',
unsafeKey => {
const dataProvider = localStorageDataProvider();
expect(() =>
dataProvider.updateMany(unsafeKey, {
ids: [1],
data: { title: 'bad' },
} as any)
).toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in create',
unsafeKey => {
const dataProvider = localStorageDataProvider();
expect(() =>
dataProvider.create(unsafeKey, {
data: { title: 'bad' },
} as any)
).toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in delete',
unsafeKey => {
const dataProvider = localStorageDataProvider();
expect(() =>
dataProvider.delete(unsafeKey, {
id: 1,
previousData: { id: 1 },
} as any)
).toThrow(`Invalid resource key: ${unsafeKey}`);
}
);

it.each(['__proto__', 'constructor', 'prototype'])(
'rejects unsafe resource key %s in deleteMany',
unsafeKey => {
const dataProvider = localStorageDataProvider();
expect(() =>
dataProvider.deleteMany(unsafeKey, {
ids: [1],
} as any)
).toThrow(`Invalid resource key: ${unsafeKey}`);
}
);
});
Loading