Skip to content

Commit

Permalink
Merge pull request #44 from PolymathNetwork/feat/MSDK-102-role-checking
Browse files Browse the repository at this point in the history
feat: refine role checking in procedures
  • Loading branch information
monitz87 authored Mar 19, 2020
2 parents 3e27186 + 283d563 commit 528474c
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 135 deletions.
2 changes: 1 addition & 1 deletion src/Polymesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class Polymesh {
/**
* Retrieve a Ticker Reservation
*
* @param ticker - Security Token ticker
* @param args.ticker - Security Token ticker
*/
public async getTickerReservation(args: { ticker: string }): Promise<TickerReservation> {
const { ticker } = args;
Expand Down
43 changes: 42 additions & 1 deletion src/api/entities/Identity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { BigNumber } from 'bignumber.js';

import { Entity } from '~/base';
import { SecurityToken } from '~/api/entities/SecurityToken';
import { TickerReservation } from '~/api/entities/TickerReservation';
import { Entity, PolymeshError } from '~/base';
import { Context } from '~/context';
import { ErrorCode, isTickerOwnerRole, isTokenOwnerRole, Role } from '~/types';
import { balanceToBigNumber } from '~/utils';

/**
Expand Down Expand Up @@ -50,4 +53,42 @@ export class Identity extends Entity<UniqueIdentifiers> {

return balanceToBigNumber(balance);
}

/**
* Check whether this Identity possesses the specified Role
*/
public async hasRole(role: Role): Promise<boolean> {
const { context, did } = this;

if (isTickerOwnerRole(role)) {
const { ticker } = role;

const reservation = new TickerReservation({ ticker }, context);
const { owner } = await reservation.details();

return owner?.did === did;
} else if (isTokenOwnerRole(role)) {
const { ticker } = role;

const token = new SecurityToken({ ticker }, context);
const { owner } = await token.details();

return owner.did === did;
}

throw new PolymeshError({
code: ErrorCode.ValidationError,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
message: `Unrecognized role "${role!.type}"`,
});
}

/**
* Check whether this Identity possesses all specified roles
*/
public async hasRoles(roles: Role[]): Promise<boolean> {
const checkedRoles = await Promise.all(roles.map(this.hasRole.bind(this)));

return checkedRoles.every(hasRole => hasRole);
}
}
108 changes: 105 additions & 3 deletions src/api/entities/__tests__/Identity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import BigNumber from 'bignumber.js';

import { Entity } from '~/base';
import { polkadotMockUtils } from '~/testUtils/mocks';
import { Context } from '~/context';
import { entityMockUtils, polkadotMockUtils } from '~/testUtils/mocks';
import { RoleType } from '~/types';

import { Identity } from '../Identity';

jest.mock(
'~/api/entities/TickerReservation',
require('~/testUtils/mocks/entities').mockTickerReservationModule(
'~/api/entities/TickerReservation'
)
);
jest.mock(
'~/api/entities/SecurityToken',
require('~/testUtils/mocks/entities').mockSecurityTokenModule('~/api/entities/SecurityToken')
);

describe('Identity class', () => {
let context: Context;

beforeAll(() => {
polkadotMockUtils.initMocks();
});

beforeEach(() => {
context = polkadotMockUtils.getContextInstance();
});

afterEach(() => {
polkadotMockUtils.reset();
});
Expand All @@ -25,7 +44,6 @@ describe('Identity class', () => {
describe('constructor', () => {
test('should assign did to instance', () => {
const did = 'abc';
const context = polkadotMockUtils.getContextInstance();
const identity = new Identity({ did }, context);

expect(identity.did).toBe(did);
Expand All @@ -46,9 +64,93 @@ describe('Identity class', () => {
polkadotMockUtils
.createQueryStub('balances', 'identityBalance')
.resolves(fakeBalance.times(Math.pow(10, 6)));
const identity = new Identity({ did: 'abc' }, polkadotMockUtils.getContextInstance());
const identity = new Identity({ did: 'abc' }, context);
const result = await identity.getPolyXBalance();
expect(result).toEqual(fakeBalance);
});
});

describe('method: hasRole and hasRoles', () => {
beforeAll(() => {
entityMockUtils.initMocks();
});

afterEach(() => {
entityMockUtils.reset();
});

afterAll(() => {
entityMockUtils.cleanup();
});

test('hasRole should check whether the identity has the Ticker Owner role', async () => {
const identity = new Identity({ did: 'someDid' }, context);
const role = { type: RoleType.TickerOwner, ticker: 'someTicker' };

let hasRole = await identity.hasRole(role);

expect(hasRole).toBe(true);

identity.did = 'otherDid';

hasRole = await identity.hasRole(role);

expect(hasRole).toBe(false);
});

test('hasRole should check whether the identity has the Token Owner role', async () => {
const identity = new Identity({ did: 'someDid' }, context);
const role = { type: RoleType.TokenOwner, ticker: 'someTicker' };

let hasRole = await identity.hasRole(role);

expect(hasRole).toBe(true);

identity.did = 'otherDid';

hasRole = await identity.hasRole(role);

expect(hasRole).toBe(false);
});

test('hasRole should throw an error if the role is not recognized', () => {
const identity = new Identity({ did: 'someDid' }, context);
const type = 'Fake' as RoleType;
const role = { type, ticker: 'someTicker' };

const hasRole = identity.hasRole(role);

expect(hasRole).rejects.toThrow(`Unrecognized role "${type}"`);
});

test('hasRoles should return true if the identity possesses all roles', async () => {
const identity = new Identity({ did: 'someDid' }, context);
const roles = [
{ type: RoleType.TickerOwner, ticker: 'someTicker' },
{ type: RoleType.TickerOwner, ticker: 'otherTicker' },
];

const hasRole = await identity.hasRoles(roles);

expect(hasRole).toBe(true);
});

test("hasRoles should return false if at least one role isn't possessed by the identity", async () => {
const identity = new Identity({ did: 'someDid' }, context);
const roles = [
{ type: RoleType.TickerOwner, ticker: 'someTicker' },
{ type: RoleType.TickerOwner, ticker: 'otherTicker' },
];

const stub = entityMockUtils.getTickerReservationDetailsStub();

stub.onSecondCall().returns({
owner: null,
});

const hasRole = await identity.hasRoles(roles);

expect(hasRole).toBe(false);
});
});
});
35 changes: 17 additions & 18 deletions src/api/procedures/__tests__/createSecurityToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import {
import sinon from 'sinon';

import { SecurityToken } from '~/api/entities';
import { Params, prepareCreateSecurityToken } from '~/api/procedures/createSecurityToken';
import {
getRequiredRoles,
Params,
prepareCreateSecurityToken,
} from '~/api/procedures/createSecurityToken';
import { Context } from '~/context';
import { entityMockUtils, polkadotMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import {
KnownTokenIdentifierType,
KnownTokenType,
RoleType,
TickerReservationStatus,
TokenDocument,
TokenIdentifier,
Expand Down Expand Up @@ -235,23 +240,6 @@ describe('createSecurityToken procedure', () => {
);
});

test('should throw an error if the signing account is not the ticker owner', () => {
const expiryDate = new Date(new Date().getTime() + 1000);
entityMockUtils.getTickerReservationDetailsStub().resolves({
owner: entityMockUtils.getIdentityInstance({ did: 'someOtherDid' }),
expiryDate,
status: TickerReservationStatus.Reserved,
});
const proc = procedureMockUtils.getInstance<Params, SecurityToken>();
proc.context = mockContext;

return expect(
prepareCreateSecurityToken.call(proc, { ...args, extendPeriod: true })
).rejects.toThrow(
`You are not the owner of ticker "${ticker}", so you cannot create a Security Token with it`
);
});

test("should throw an error if the signing account doesn't have enough balance", () => {
polkadotMockUtils.createQueryStub('asset', 'assetCreationFee', {
returnValue: polkadotMockUtils.createMockBalance(600000000),
Expand Down Expand Up @@ -320,3 +308,14 @@ describe('createSecurityToken procedure', () => {
expect(result).toMatchObject(new SecurityToken({ ticker }, mockContext));
});
});

describe('getRequiredRoles', () => {
test('should return a ticker owner role', () => {
const ticker = 'someTicker';
const args = {
ticker,
} as Params;

expect(getRequiredRoles(args)).toEqual([{ type: RoleType.TickerOwner, ticker }]);
});
});
32 changes: 13 additions & 19 deletions src/api/procedures/__tests__/modifyToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Ticker, TokenName } from 'polymesh-types/types';
import sinon from 'sinon';

import { SecurityToken } from '~/api/entities';
import { Params, prepareModifyToken } from '~/api/procedures/modifyToken';
import { getRequiredRoles, Params, prepareModifyToken } from '~/api/procedures/modifyToken';
import { Context } from '~/context';
import { entityMockUtils, polkadotMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { RoleType } from '~/types';
import * as utilsModule from '~/utils';

jest.mock(
Expand Down Expand Up @@ -61,24 +62,6 @@ describe('modifyToken procedure', () => {
);
});

test('should throw an error if the user is not the owner of the token', () => {
entityMockUtils.getSecurityTokenDetailsStub({
owner: entityMockUtils.getIdentityInstance({ did: 'someOtherDid' }),
});

const proc = procedureMockUtils.getInstance<Params, SecurityToken>();
proc.context = mockContext;

return expect(
prepareModifyToken.call(proc, {
ticker,
makeDivisible: true,
})
).rejects.toThrow(
'You must be the owner of the Security Token to modify any of its properties'
);
});

test('should throw an error if makeDivisible is set to true and the security token is already divisible', () => {
entityMockUtils.getSecurityTokenDetailsStub({
isDivisible: true,
Expand Down Expand Up @@ -155,3 +138,14 @@ describe('modifyToken procedure', () => {
expect(result.ticker).toBe(procedureResult.ticker);
});
});

describe('getRequiredRoles', () => {
test('should return a token owner role', () => {
const ticker = 'someTicker';
const args = {
ticker,
} as Params;

expect(getRequiredRoles(args)).toEqual([{ type: RoleType.TokenOwner, ticker }]);
});
});
39 changes: 23 additions & 16 deletions src/api/procedures/__tests__/reserveTicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import sinon from 'sinon';
import { TickerReservation } from '~/api/entities';
import {
createTickerReservationResolver,
getRequiredRoles,
prepareReserveTicker,
ReserveTickerParams,
} from '~/api/procedures/reserveTicker';
import { PostTransactionValue } from '~/base';
import { Context } from '~/context';
import { entityMockUtils, polkadotMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { TickerReservationStatus } from '~/types';
import { RoleType, TickerReservationStatus } from '~/types';
import { PolymeshTx } from '~/types/internal';
import * as utilsModule from '~/utils';

Expand Down Expand Up @@ -194,21 +195,6 @@ describe('reserveTicker procedure', () => {
);
});

test('should throw an error if extendPeriod property is set to true and the signing account is not the ticker owner', () => {
const expiryDate = new Date(new Date().getTime() + 1000);
entityMockUtils.getTickerReservationDetailsStub().resolves({
owner: entityMockUtils.getIdentityInstance(),
expiryDate,
status: TickerReservationStatus.Reserved,
});
const proc = procedureMockUtils.getInstance<ReserveTickerParams, TickerReservation>();
proc.context = mockContext;

return expect(prepareReserveTicker.call(proc, { ...args, extendPeriod: true })).rejects.toThrow(
'You must be the owner of the ticker to extend its reservation period'
);
});

test('should add a register ticker transaction to the queue', async () => {
const proc = procedureMockUtils.getInstance<ReserveTickerParams, TickerReservation>();
proc.context = mockContext;
Expand Down Expand Up @@ -253,3 +239,24 @@ describe('tickerReservationResolver', () => {
expect(result.ticker).toBe(tickerString);
});
});

describe('getRequiredRoles', () => {
test('should return a ticker owner role if extending a reservation', () => {
const ticker = 'someTicker';
const args = {
ticker,
extendPeriod: true,
};

expect(getRequiredRoles(args)).toEqual([{ type: RoleType.TickerOwner, ticker }]);
});

test('should return an empty array if not extending a reservation', () => {
const ticker = 'someTicker';
const args = {
ticker,
};

expect(getRequiredRoles(args)).toEqual([]);
});
});
Loading

0 comments on commit 528474c

Please sign in to comment.