Skip to content

Commit

Permalink
Test multiple behaviors each round
Browse files Browse the repository at this point in the history
  • Loading branch information
bdemann committed Apr 15, 2024
1 parent 575cc5c commit 9835e13
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 101 deletions.
65 changes: 63 additions & 2 deletions dfx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function getAgentHost(): string {
: `http://127.0.0.1:${getWebServerPort()}`;
}

export async function createAnonymousAgent() {
export async function getAnonymousAgent() {
const agent = new HttpAgent({
host: getAgentHost()
});
Expand All @@ -38,7 +38,13 @@ export async function createAnonymousAgent() {
}
}

export async function createAuthenticatedAgent(
/**
* Returns an agent authenticated with the identity with the given name.
*
* @param identityName
* @returns
*/
export async function getAuthenticatedAgent(
identityName: string
): Promise<HttpAgent> {
const agent = new HttpAgent({
Expand All @@ -53,6 +59,61 @@ export async function createAuthenticatedAgent(
return agent;
}

/**
* Returns an agent authenticated with the identity with the given name.
*
* IMPORTANT: In order to be synchronous this call will not fetch the root key.
* If you are not on mainnet you will need to fetch the root key separately.
*
* @param identityName
* @returns
*/
export function getAuthenticatedAgentSync(identityName: string): HttpAgent {
const agent = new HttpAgent({
host: getAgentHost(),
identity: getSecp256k1KeyIdentity(identityName)
});

return agent;
}

/**
* Generates a new identity with the given name if there doesn't already exist
* an identity with that name, and returns an agent authenticated with that
* identity.
*
* IMPORTANT: In order to be synchronous this call will not fetch the root key.
* If you are not on mainnet you will need to fetch the root key separately.
*
* @param identityName
* @returns
*/
export function generateAuthenticatedAgentSync(
identityName: string
): HttpAgent {
if (!identityExists(identityName)) {
generateIdentity(identityName);
}
return getAuthenticatedAgentSync(identityName);
}

/**
* Generates a new identity with the given name if there doesn't already exist
* an identity with that name, and returns an agent authenticated with that
* identity.
*
* @param identityName
* @returns
*/
export async function generateAuthenticatedAgent(
identityName: string
): Promise<HttpAgent> {
if (!identityExists(identityName)) {
generateIdentity(identityName);
}
return await getAuthenticatedAgent(identityName);
}

export function whoami(): string {
return execSync(`dfx identity whoami`).toString().trim();
}
Expand Down
22 changes: 15 additions & 7 deletions property_tests/get_actor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Agent } from '@dfinity/agent';

import { getCanisterId } from '../dfx';

export function getActor(parentDir: string) {
export function getActor(parentDir: string, agent?: Agent) {
const resolvedPathIndex = require.resolve(
`${parentDir}/dfx_generated/canister/index.js`
);
Expand All @@ -13,10 +15,16 @@ export function getActor(parentDir: string) {

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createActor } = require(`${parentDir}/dfx_generated/canister`);
return createActor(getCanisterId('canister'), {
agentOptions: {
host: 'http://127.0.0.1:8000',
verifyQuerySignatures: false // TODO Major issue: https://forum.dfinity.org/t/agent-js-0-20-0-is-released-replica-signed-query-edition/24743/16?u=lastmjs
}
});

const options =
agent !== undefined
? { agent }
: {
agentOptions: {
host: 'http://127.0.0.1:8000',
verifyQuerySignatures: false // TODO Major issue: https://forum.dfinity.org/t/agent-js-0-20-0-is-released-replica-signed-query-edition/24743/16?u=lastmjs
}
};

return createActor(getCanisterId('canister'), options);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Agent } from '@dfinity/agent';
import { deepEqual, getActor, Named } from 'azle/property_tests';
import { CandidReturnType } from 'azle/property_tests/arbitraries/candid/candid_return_type_arb';
import { CandidValueAndMeta } from 'azle/property_tests/arbitraries/candid/candid_value_and_meta_arb';
Expand All @@ -7,11 +8,10 @@ import { Test } from 'azle/test';
import { InspectMessageBehavior } from './test';

export function generateTests(
mode: 'query' | 'update',
functionName: string,
params: Named<CandidValueAndMeta<CorrespondingJSType>>[],
returnType: CandidValueAndMeta<CandidReturnType>,
behavior: InspectMessageBehavior
agents: [Agent, InspectMessageBehavior][]
): Test[][] {
const paramValues = params.map(
(param) => param.value.value.agentArgumentValue
Expand All @@ -20,32 +20,56 @@ export function generateTests(
const expectedResult = returnType.value.agentResponseValue;

return [
[
{
name: `${mode} method "${functionName}"`,
test: async () => {
const actor = getActor(__dirname);
try {
const result = await actor[functionName](
...paramValues
);

if (mode === 'update' && behavior !== 'ACCEPT') {
return {
Err: 'Expected canister method to throw but it did not'
};
}

return { Ok: deepEqual(result, expectedResult) };
} catch (error) {
if (mode === 'update' && behavior !== 'ACCEPT') {
return { Ok: true };
}

throw error;
}
agents.map(([agent, behavior]) => {
return generateTest(
agent,
functionName,
paramValues,
expectedResult,
behavior
);
})
];
}

function generateTest(
agent: Agent,
functionName: string,
paramValues: CorrespondingJSType[],
expectedResult: CorrespondingJSType,
behavior: InspectMessageBehavior
): Test {
return {
name: `method "${functionName}" expected ${behavior}`,
test: async () => {
await agent.fetchRootKey();
const actor = getActor(__dirname, agent);
try {
const result = await actor[functionName](...paramValues);

if (behavior === 'ACCEPT') {
return { Ok: deepEqual(result, expectedResult) };
}

return {
Err: 'Expected canister method to throw but it did not'
};
} catch (error: any) {
if (behavior === 'RETURN') {
return {
Ok: error.message.includes('rejected the message')
};
}

if (behavior === 'THROW') {
const expectedError = `Method \\"${functionName}\\" not allowed`;
return {
Ok: error.message.includes(expectedError)
};
}

throw error;
}
]
];
}
};
}
119 changes: 59 additions & 60 deletions property_tests/tests/canister_methods/inspect_message/test/test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Agent } from '@dfinity/agent';
import { generateAuthenticatedAgentSync, getPrincipal } from 'azle/dfx';
import { runPropTests } from 'azle/property_tests';
import { CandidReturnTypeArb } from 'azle/property_tests/arbitraries/candid/candid_return_type_arb';
import { CandidValueAndMetaArb } from 'azle/property_tests/arbitraries/candid/candid_value_and_meta_arb';
Expand All @@ -6,81 +8,78 @@ import {
CanisterConfig
} from 'azle/property_tests/arbitraries/canister_arb';
import { InspectMessageMethodArb } from 'azle/property_tests/arbitraries/canister_methods/inspect_message_method_arb';
import { QueryMethodArb } from 'azle/property_tests/arbitraries/canister_methods/query_method_arb';
import { UpdateMethodArb } from 'azle/property_tests/arbitraries/canister_methods/update_method_arb';
import fc from 'fast-check';
import { v4 } from 'uuid';

import { CorrespondingJSType } from '../../../../arbitraries/candid/corresponding_js_type';
import { generateTests } from './generate_tests';

export type InspectMessageBehavior = 'ACCEPT' | 'RETURN' | 'THROW';

const CanisterConfigArb = fc
.constantFrom<InspectMessageBehavior>('ACCEPT', 'RETURN', 'THROW')
.chain((behavior) => {
const InspectMessageArb = InspectMessageMethodArb({
generateBody: () => generateInspectMessageMethodBody(behavior),
generateTests: () => []
});
const AZLE_ACCEPT_IDENTITY_NAME = `_prop_test_azle_accept_identity_${v4()}`;
const AZLE_RETURN_IDENTITY_NAME = `_prop_test_azle_return_identity_${v4()}`;
const AZLE_THROW_IDENTITY_NAME = `_prop_test_azle_throw_identity_${v4()}`;

const HeterogeneousQueryMethodArb = QueryMethodArb(
fc.array(CandidValueAndMetaArb()),
CandidReturnTypeArb(),
{
generateBody: (_, returnType) =>
`return ${returnType.src.valueLiteral}`,
generateTests: (...args) =>
generateTests('query', ...args, behavior)
}
);

const HeterogeneousUpdateMethodArb = UpdateMethodArb(
fc.array(CandidValueAndMetaArb()),
CandidReturnTypeArb(),
{
generateBody: (_, returnType) =>
`return ${returnType.src.valueLiteral}`,
generateTests: (...args) =>
generateTests('update', ...args, behavior)
}
);
function CanisterConfigArb() {
const agents: [Agent, InspectMessageBehavior][] = [
[generateAuthenticatedAgentSync(AZLE_ACCEPT_IDENTITY_NAME), 'ACCEPT'],
[generateAuthenticatedAgentSync(AZLE_RETURN_IDENTITY_NAME), 'RETURN'],
[generateAuthenticatedAgentSync(AZLE_THROW_IDENTITY_NAME), 'THROW']
];

const small = {
minLength: 0,
maxLength: 20
};
const InspectMessageArb = InspectMessageMethodArb({
generateBody: () => generateInspectMessageMethodBody(),
generateTests: () => []
});

return fc.tuple(
InspectMessageArb,
fc.array(HeterogeneousQueryMethodArb, small),
fc.array(HeterogeneousUpdateMethodArb, small)
);
})
.map(
([inspectMessageMethod, queryMethods, updateMethods]): CanisterConfig<
CorrespondingJSType,
CorrespondingJSType
> => {
return {
inspectMessageMethod,
queryMethods,
updateMethods
};
const HeterogeneousUpdateMethodArb = UpdateMethodArb(
fc.array(CandidValueAndMetaArb()),
CandidReturnTypeArb(),
{
generateBody: (_, returnType) =>
`return ${returnType.src.valueLiteral}`,
generateTests: (...args) => generateTests(...args, agents)
}
);

runPropTests(CanisterArb(CanisterConfigArb));
const small = {
minLength: 0,
maxLength: 20
};

function generateInspectMessageMethodBody(
behavior: InspectMessageBehavior
): string {
if (behavior === 'RETURN') {
return /*TS*/ '';
}
return fc
.tuple(InspectMessageArb, fc.array(HeterogeneousUpdateMethodArb, small))
.map(
([inspectMessageMethod, updateMethods]): CanisterConfig<
CorrespondingJSType,
CorrespondingJSType
> => {
return {
inspectMessageMethod,
updateMethods
};
}
);
}

if (behavior === 'THROW') {
return /*TS*/ `throw \`Method "$\{ic.methodName()}" not allowed\``;
}
runPropTests(CanisterArb(CanisterConfigArb()));

return /*TS*/ `ic.acceptMessage();`;
function generateInspectMessageMethodBody(): string {
const acceptPrincipal = getPrincipal(AZLE_ACCEPT_IDENTITY_NAME);
const returnPrincipal = getPrincipal(AZLE_RETURN_IDENTITY_NAME);
const throwPrincipal = getPrincipal(AZLE_THROW_IDENTITY_NAME);
return `
if (ic.caller().toText() === "${acceptPrincipal}") {
ic.acceptMessage()
return;
}
if (ic.caller().toText() === "${returnPrincipal}") {
return
}
if (ic.caller().toText() === "${throwPrincipal}") {
throw new Error(\`Method "$\{ic.methodName()}" not allowed\`)
}
throw new Error("Unexpected caller")
`;
}
4 changes: 2 additions & 2 deletions src/compiler/file_uploader/uploader_actor.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Actor, ActorMethod, ActorSubclass } from '@dfinity/agent';

import { createAuthenticatedAgent } from '../../../dfx';
import { getAuthenticatedAgent } from '../../../dfx';

export type UploaderActor = ActorSubclass<_SERVICE>;

export async function createActor(
canisterId: string,
identityName: string
): Promise<UploaderActor> {
const agent = await createAuthenticatedAgent(identityName);
const agent = await getAuthenticatedAgent(identityName);

return Actor.createActor<_SERVICE>(
({ IDL }) => {
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/file_watcher/file_watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Actor } from '@dfinity/agent';
import { watch } from 'chokidar';
import { readFileSync, writeFileSync } from 'fs';

import { createAuthenticatedAgent, whoami } from '../../../dfx';
import { getAuthenticatedAgent, whoami } from '../../../dfx';
import { getCanisterJavaScript } from '../get_canister_javascript';
import { ok } from '../utils/result';

Expand Down Expand Up @@ -57,7 +57,7 @@ async function reloadJs(

writeFileSync(reloadedJsPath, canisterJavaScriptResult.ok);

const agent = await createAuthenticatedAgent(whoami());
const agent = await getAuthenticatedAgent(whoami());

const actor = Actor.createActor(
({ IDL }) => {
Expand Down

0 comments on commit 9835e13

Please sign in to comment.