diff --git a/Agents.md b/Agents.md index 03f16cb5103..a3e10a11a78 100644 --- a/Agents.md +++ b/Agents.md @@ -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 @@ -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 -``` \ No newline at end of file +``` diff --git a/packages/ra-data-local-forage/src/index.spec.ts b/packages/ra-data-local-forage/src/index.spec.ts new file mode 100644 index 00000000000..e6704287681 --- /dev/null +++ b/packages/ra-data-local-forage/src/index.spec.ts @@ -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}`); + } + ); +}); diff --git a/packages/ra-data-local-storage/src/index.spec.ts b/packages/ra-data-local-storage/src/index.spec.ts new file mode 100644 index 00000000000..a500463d974 --- /dev/null +++ b/packages/ra-data-local-storage/src/index.spec.ts @@ -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}`); + } + ); +});