Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: param validation for all RPC methods #29

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TriggerResponseTypes,
RpcResponseTypes,
} from '../../src/types';
import { CreateTokenError, PromptRejectedError } from '../../src/errors';
import { CreateTokenError, PromptRejectedError, InvalidParamsError } from '../../src/errors';

function toCamelCase(params: Pick<CreateTokenRpcRequest, 'params'>['params']) {
return {
Expand Down Expand Up @@ -176,5 +176,86 @@ describe('createToken', () => {

expect(wallet.isAddressMine).toHaveBeenCalledWith('changeAddress123');
});

describe('parameter validation', () => {
beforeEach(() => {
(wallet.isAddressMine as jest.Mock).mockResolvedValue(true);
triggerHandler.mockResolvedValue({
type: TriggerResponseTypes.CreateTokenConfirmationResponse,
data: { accepted: true },
});
});

it('should reject when required parameters are missing', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
symbol: 'MTK',
amount: 1000,
// name is missing
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when amount is not positive', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'My Token',
symbol: 'MTK',
amount: 0,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when parameters have wrong types', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'My Token',
symbol: 'MTK',
amount: '1000' as unknown as number,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when optional parameters have wrong types', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'My Token',
symbol: 'MTK',
amount: 1000,
create_mint: 'yes' as unknown as boolean,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when name or symbol are empty strings', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: '',
symbol: 'MTK',
amount: 1000,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});
});
});

235 changes: 162 additions & 73 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/getAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,110 +5,199 @@
* LICENSE file in the root directory of this source tree.
*/

import { NotImplementedError, PromptRejectedError } from '../../src/errors';
import { getAddress } from '../../src/rpcMethods/getAddress';
import { HathorWallet } from '@hathor/wallet-lib';
import { TriggerTypes, GetAddressRpcRequest, RpcMethods } from '../../src/types';
import {
RpcMethods,
GetAddressRpcRequest,
TriggerResponseTypes,
AddressRequestClientResponse,
} from '../../src/types';
import { getAddress } from '../../src/rpcMethods/getAddress';
import { InvalidParamsError, NotImplementedError, PromptRejectedError } from '../../src/errors';

export const mockPromptHandler = jest.fn();
describe('getAddress parameter validation', () => {
const mockWallet = {
getNetwork: jest.fn().mockReturnValue('testnet'),
getCurrentAddress: jest.fn().mockResolvedValue('test-address'),
getAddressAtIndex: jest.fn().mockResolvedValue('test-address'),
} as unknown as HathorWallet;

describe('getAddress', () => {
let promptHandler: jest.Mock;
let mockWallet: jest.Mocked<HathorWallet>;
const mockTriggerHandler = jest.fn().mockResolvedValue(true);

beforeEach(() => {
promptHandler = jest.fn();
mockWallet = {
getAddressAtIndex: jest.fn().mockReturnValue('mocked_address'),
getCurrentAddress: jest.fn().mockReturnValue({
address: 'address1',
index: 0,
addressPath: `m/44'/280'/0'/0/10`,
}),
getNetwork: jest.fn().mockReturnValue('mainnet')
} as unknown as HathorWallet;
jest.clearAllMocks();
});

it('should return the current address for type "first_empty"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'first_empty', network: 'mainnet' },
it('should reject when network is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockReturnValueOnce(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
params: {
type: 'first_empty',
// network is missing
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

expect(address.response).toBe('current-address');
expect(mockWallet.getCurrentAddress).toHaveBeenCalled();
it('should reject when type is invalid', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
params: {
type: 'invalid_type',
network: 'testnet',
},
} as unknown as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should throw NotImplementedError for type "full_path"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'full_path', network: 'mainnet' },
it('should reject when type is index but index is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
};
params: {
type: 'index',
network: 'testnet',
// index is missing
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

await expect(getAddress(rpcRequest, mockWallet, {}, promptHandler)).rejects.toThrow(NotImplementedError);
it('should reject when type is index but index is negative', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
params: {
type: 'index',
network: 'testnet',
index: -1,
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should return the address at index for type "index"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'index', index: 5, network: 'mainnet' },
it('should reject when type is full_path but full_path is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getAddressAtIndex.mockResolvedValue('address-at-index');
promptHandler.mockReturnValueOnce(true);
params: {
type: 'full_path',
network: 'testnet',
// full_path is missing
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
it('should reject when type is full_path but full_path is empty', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
params: {
type: 'full_path',
network: 'testnet',
full_path: '',
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

expect(address.response).toBe('address-at-index');
expect(mockWallet.getAddressAtIndex).toHaveBeenCalledWith(5);
it('should throw NotImplementedError when type is full_path', async () => {
const request = {
method: RpcMethods.GetAddress,
params: {
type: 'full_path',
network: 'testnet',
full_path: 'm/44/0/0',
},
} as GetAddressRpcRequest;

await expect(
getAddress(request, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(NotImplementedError);
});

it('should return the client address for type "client"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'client', network: 'mainnet' },
it('should accept valid first_empty request', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
};
const clientPromptResponse = { data: { address: 'client-address' } };
promptHandler.mockResolvedValue(clientPromptResponse);
params: {
type: 'first_empty',
network: 'testnet',
},
} as GetAddressRpcRequest;

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).resolves.toBeDefined();

expect(address.response).toBe('client-address');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.AddressRequestClientPrompt,
method: RpcMethods.GetAddress,
}, {});
expect(mockWallet.getCurrentAddress).toHaveBeenCalled();
});

it('should throw PromptRejectedError if address confirmation is rejected', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'first_empty', network: 'mainnet' },
it('should accept valid index request', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockResolvedValueOnce(false);
params: {
type: 'index',
network: 'testnet',
index: 0,
},
} as GetAddressRpcRequest;

await expect(getAddress(rpcRequest, mockWallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError);
await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).resolves.toBeDefined();

expect(mockWallet.getAddressAtIndex).toHaveBeenCalledWith(0);
});

it('should confirm the address if type is not "client"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'first_empty', network: 'mainnet' },
it('should accept valid client request', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockResolvedValue(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
params: {
type: 'client',
network: 'testnet',
},
} as GetAddressRpcRequest;

mockTriggerHandler.mockResolvedValueOnce({
type: TriggerResponseTypes.AddressRequestClientResponse,
data: {
address: 'client-address',
},
} as AddressRequestClientResponse);

await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).resolves.toBeDefined();
});

expect(address.response).toBe('current-address');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.AddressRequestPrompt,
it('should throw PromptRejectedError when user rejects non-client address', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
data: { address: 'current-address' },
}, {});
params: {
type: 'first_empty',
network: 'testnet',
},
} as GetAddressRpcRequest;

mockTriggerHandler.mockResolvedValueOnce(false);

await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(PromptRejectedError);
});
});
Loading