Skip to content

Commit

Permalink
Refactor update state validation process and add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
david0xd committed Nov 14, 2022
1 parent e70ecb0 commit 9ba9781
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 49 deletions.
6 changes: 3 additions & 3 deletions packages/rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ module.exports = deepmerge(baseConfig, {
coveragePathIgnorePatterns: ['./src/index.ts'],
coverageThreshold: {
global: {
branches: 88.37,
branches: 88.63,
functions: 86.53,
lines: 78.75,
statements: 78.75,
lines: 79.23,
statements: 79.23,
},
},
testTimeout: 2500,
Expand Down
108 changes: 95 additions & 13 deletions packages/rpc-methods/src/restricted/manageState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
getValidatedParams,
ManageStateOperation,
specificationBuilder,
STORAGE_SIZE_LIMIT,
} from './manageState';

describe('snap_manageState', () => {
const MOCK_SMALLER_STORAGE_SIZE_LIMIT = 10; // In bytes
describe('specification', () => {
it('builds specification', () => {
const methodHooks = {
Expand Down Expand Up @@ -170,45 +172,125 @@ describe('snap_manageState', () => {

describe('getValidatedParams', () => {
it('throws an error if the params is not an object', () => {
expect(() => getValidatedParams([])).toThrow(
'Expected params to be a single object.',
);
expect(() =>
getValidatedParams([], 'snap_manageState', STORAGE_SIZE_LIMIT),
).toThrow('Expected params to be a single object.');
});

it('throws an error if the operation type is missing from params object', () => {
expect(() => getValidatedParams({ operation: undefined })).toThrow(
'Must specify a valid manage state "operation".',
);
expect(() =>
getValidatedParams(
{ operation: undefined },
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toThrow('Must specify a valid manage state "operation".');
});

it('throws an error if the operation type is not valid', () => {
expect(() =>
getValidatedParams({ operation: 'unspecifiedOperation' }),
getValidatedParams(
{ operation: 'unspecifiedOperation' },
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toThrow('Must specify a valid manage state "operation".');
});

it('returns valid parameters for get operation', () => {
expect(getValidatedParams({ operation: 'get' })).toStrictEqual({
expect(
getValidatedParams(
{ operation: 'get' },
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toStrictEqual({
operation: 'get',
});
});

it('returns valid parameters for clear operation', () => {
expect(getValidatedParams({ operation: 'clear' })).toStrictEqual({
expect(
getValidatedParams(
{ operation: 'clear' },
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toStrictEqual({
operation: 'clear',
});
});

it('returns valid parameters for update operation', () => {
expect(
getValidatedParams({
operation: 'update',
newState: { data: 'updated data' },
}),
getValidatedParams(
{
operation: 'update',
newState: { data: 'updated data' },
},
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toStrictEqual({
operation: 'update',
newState: { data: 'updated data' },
});
});

it('throws an error if the new state object is not valid plain object', () => {
const mockInvalidNewStateObject = (a: unknown) => {
return a;
};

expect(() =>
getValidatedParams(
{ operation: 'update', newState: mockInvalidNewStateObject },
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toThrow(
'Invalid snap_manageState "updateState" parameter: The new state must be a plain object.',
);
});

it('throws an error if the new state object is not valid JSON serializable object', () => {
const mockInvalidNewStateObject = {
something: {
something: {
invalidJson: () => 'a',
},
},
};

expect(() =>
getValidatedParams(
{ operation: 'update', newState: mockInvalidNewStateObject },
'snap_manageState',
STORAGE_SIZE_LIMIT,
),
).toThrow(
'Invalid snap_manageState "updateState" parameter: The new state must be JSON serializable.',
);
});

it('throws an error if the new state object is exceeding the JSON size limit', () => {
const mockInvalidNewStateObject = {
something: {
something: {
whatever: 'whatever',
},
},
};

expect(() =>
getValidatedParams(
{ operation: 'update', newState: mockInvalidNewStateObject },
'snap_manageState',
MOCK_SMALLER_STORAGE_SIZE_LIMIT,
),
).toThrow(
`Invalid snap_manageState "updateState" parameter: The new state must not exceed ${MOCK_SMALLER_STORAGE_SIZE_LIMIT} bytes in size.`,
);
});
});
});
77 changes: 44 additions & 33 deletions packages/rpc-methods/src/restricted/manageState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ export function getManageStateImplementation({
method,
context: { origin },
} = options;
const { operation, newState } = getValidatedParams(params);
const { operation, newState } = getValidatedParams(
params,
method,
STORAGE_SIZE_LIMIT,
);

switch (operation) {
case ManageStateOperation.clearState:
Expand All @@ -133,36 +137,7 @@ export function getManageStateImplementation({
return await getSnapState(origin);

case ManageStateOperation.updateState: {
if (!isObject(newState)) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must be a plain object.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
}
const [isValid, plainTextSizeInBytes] =
validateJsonAndGetSize(newState);
if (!isValid) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must be JSON serializable.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
} else if (plainTextSizeInBytes > STORAGE_SIZE_LIMIT) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must not exceed ${STORAGE_SIZE_LIMIT} bytes in size.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
}

await updateSnapState(origin, newState);
await updateSnapState(origin, newState as Record<string, Json>);
return null;
}
default:
Expand All @@ -178,16 +153,22 @@ export function getManageStateImplementation({
* type. Throws if validation fails.
*
* @param params - The unvalidated params object from the method request.
* @param method - RPC method name used for debugging errors.
* @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.
* @returns The validated method parameter object.
*/
export function getValidatedParams(params: unknown): ManageStateArgs {
export function getValidatedParams(
params: unknown,
method: string,
storageSizeLimit: number,
): ManageStateArgs {
if (!isObject(params)) {
throw ethErrors.rpc.invalidParams({
message: 'Expected params to be a single object.',
});
}

const { operation } = params;
const { operation, newState } = params;

if (
!operation ||
Expand All @@ -199,5 +180,35 @@ export function getValidatedParams(params: unknown): ManageStateArgs {
});
}

if (operation === ManageStateOperation.updateState) {
if (!isObject(newState)) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must be a plain object.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
}
const [isValid, plainTextSizeInBytes] = validateJsonAndGetSize(newState);
if (!isValid) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must be JSON serializable.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
} else if (plainTextSizeInBytes > storageSizeLimit) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
}
}

return params as ManageStateArgs;
}

0 comments on commit 9ba9781

Please sign in to comment.