Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/envio-tests/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ node_modules/
lib
*.res.js
*.res.mjs

# Internal test indexer temp handler files
test/.tmp/
35 changes: 20 additions & 15 deletions packages/envio-tests/test/HandlerRegister_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ contracts:
- event: Transfer(address indexed from, address indexed to, uint256 value)
chains:
- id: 1
rpc:
url: https://eth.com
for: sync
start_block: 0
contracts:
- name: ERC20
Expand All @@ -39,9 +36,6 @@ contracts:
- event: Approval(address indexed owner, address indexed spender, uint256 value)
chains:
- id: 137
rpc:
url: https://polygon.com
for: sync
start_block: 0
contracts:
- name: ERC20
Expand All @@ -60,17 +54,11 @@ contracts:
- event: Approval(address indexed owner, address indexed spender, uint256 value)
chains:
- id: 1
rpc:
url: https://eth.com
for: sync
start_block: 0
contracts:
- name: ERC20
address: "0x1111111111111111111111111111111111111111"
- id: 137
rpc:
url: https://polygon.com
for: sync
start_block: 0
contracts:
- name: ERC20
Expand All @@ -90,9 +78,6 @@ contracts:
- event: Approval(address indexed owner, address indexed spender, uint256 value)
chains:
- id: 1
rpc:
url: https://eth.com
for: sync
start_block: 0
contracts:
- name: ERC20
Expand Down Expand Up @@ -333,4 +318,24 @@ describe("HandlerRegister multiple registrations", () => {
registrations->describeRegistrations(~eventName="Approval", ~labels=[], ~crLabels=[]),
)).toEqual(([], [(None, None, 0)]))
})

// A registration matching no configured chain can never be dispatched, so it
// is reported at the call site instead of silently doing nothing.
it("throws when the event is not configured on the contract", t => {
HandlerRegister.resetOnEventRegistrations()
HandlerRegister.startRegistration(~config)
t->toThrowErrorEqual(
() => setHandler(~eventName="Nonexistent", makeHandler()),
`Event "Nonexistent" is not configured on contract "ERC20", so its handler would never run. Add it to your config, or remove the registration. Configured events on "ERC20": Approval, Transfer.`,
)
})

it("throws when the contract is not configured on any chain", t => {
HandlerRegister.resetOnEventRegistrations()
HandlerRegister.startRegistration(~config)
t->toThrowErrorEqual(
() => setContractRegister(~contractName="Missing", makeContractRegister()),
`Contract "Missing" is not configured on any chain, so its handler for "Transfer" would never run. Add it to your config, or remove the registration. Configured contracts: ERC20, ERC721.`,
)
})
})
2 changes: 1 addition & 1 deletion packages/envio-tests/test/MockIndexerHandlers_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ indexer.onEvent({ contract: "Token", event: "Nonexistent" }, async () => {});
~configYaml=yaml,
)->ignore,

"Handler type errors:\n__mock_indexer_handlers.ts(3,38): error TS2322: Type '\"Nonexistent\"' is not assignable to type '\"Transfer\"'.",
"Type errors:\n__mock_indexer_handlers.ts(3,38): error TS2322: Type '\"Nonexistent\"' is not assignable to type '\"Transfer\"'.",
)
})
})
148 changes: 148 additions & 0 deletions packages/envio-tests/test/OptionalBlockParams_test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Block-range resolution for `process()`: which defaults apply when startBlock
// or endBlock are omitted, how progress carries into a second call, and what
// invalid ranges report. Migrated from scenarios/test_codegen.
let _ = InternalTestIndexer.fromUserApi(
~configYaml=`
name: block-params
contracts:
- name: Gravatar
events:
- event: EmptyEvent()
chains:
- id: 1
start_block: 1
contracts:
- name: Gravatar
address: "0x1111111111111111111111111111111111111111"
`,
~schema=`
type SimulateTestEvent {
id: ID!
blockNumber: Int!
logIndex: Int!
timestamp: Int!
}
`,
~handlers=`
import { indexer } from "envio";

indexer.onEvent({ contract: "Gravatar", event: "EmptyEvent" }, async ({ event, context }) => {
context.SimulateTestEvent.set({
id: \`\${event.block.number}_\${event.logIndex}\`,
blockNumber: event.block.number,
logIndex: event.logIndex,
timestamp: event.block.timestamp,
});
});
`,
~test=`
import { describe, it } from "vitest";
import { createTestIndexer, type SimulateTestEvent } from "envio";

const emptyEvent = { contract: "Gravatar", event: "EmptyEvent" } as const;

const at = (blockNumber: number): SimulateTestEvent => ({
id: \`\${blockNumber}_0\`,
blockNumber,
logIndex: 0,
timestamp: 0,
});

const messageOf = async (run: () => Promise<unknown>): Promise<string | undefined> => {
try {
await run();
return undefined;
} catch (error) {
return (error as Error).message;
}
};

describe("optional block params", () => {
it("defaults startBlock from config and endBlock to startBlock", async (t) => {
const indexer = createTestIndexer();
await indexer.process({ chains: { 1: { simulate: [emptyEvent] } } });

t.expect(await indexer.SimulateTestEvent.getAll()).toEqual([at(1)]);
});

it("defaults endBlock to startBlock when only startBlock is given", async (t) => {
const indexer = createTestIndexer();
await indexer.process({ chains: { 1: { startBlock: 5, simulate: [emptyEvent] } } });

t.expect(await indexer.SimulateTestEvent.getAll()).toEqual([at(5)]);
});

it("uses explicit startBlock and endBlock when provided", async (t) => {
const indexer = createTestIndexer();
await indexer.process({ chains: { 1: { startBlock: 1, endBlock: 100, simulate: [emptyEvent] } } });

t.expect(await indexer.SimulateTestEvent.getAll()).toEqual([at(1)]);
});

it("defaults startBlock to progressBlock+1 on a second process call", async (t) => {
const indexer = createTestIndexer();
await indexer.process({ chains: { 1: { startBlock: 1, endBlock: 100, simulate: [emptyEvent] } } });
// Omitting startBlock resumes at 101.
await indexer.process({ chains: { 1: { simulate: [{ ...emptyEvent, block: { number: 101 } }] } } });

const entities = await indexer.SimulateTestEvent.getAll();
const byBlock = [...entities].sort((a, b) => a.blockNumber - b.blockNumber);
t.expect(byBlock).toEqual([at(1), at(101)]);
});

it("defaults endBlock to the highest simulate block number", async (t) => {
const indexer = createTestIndexer();
await indexer.process({ chains: { 1: { simulate: [{ ...emptyEvent, block: { number: 50 } }] } } });

t.expect(await indexer.SimulateTestEvent.getAll()).toEqual([at(50)]);
});

// The generated types reject the next three inputs outright, so each casts
// through \`any\` to reach the runtime validation behind them.
it("rejects a non-numeric chain ID", async (t) => {
const indexer = createTestIndexer();
const message = await messageOf(() =>
indexer.process({ chains: { abc: { startBlock: 1, endBlock: 100 } } } as any),
);

t.expect(message).toBe('Invalid chain ID "abc": expected a numeric chain ID');
});

it("rejects a chain ID absent from the config", async (t) => {
const indexer = createTestIndexer();
const message = await messageOf(() =>
indexer.process({ chains: { 9999: { startBlock: 1, endBlock: 100 } } } as any),
);

t.expect(message).toBe("Chain 9999 is not configured in config.yaml");
});

it("rejects a startBlock of the wrong type", async (t) => {
const indexer = createTestIndexer();
const message = await messageOf(() =>
indexer.process({ chains: { 1: { startBlock: "not_a_number" } } } as any),
);

t.expect(message).toBe(
'Invalid processConfig: RescriptSchemaError: Failed parsing at ["chains"]["1"]["startBlock"]. ' +
'Reason: Expected int32 | undefined, received "not_a_number"',
);
});

// Without simulate items and without an endBlock the run enters auto-exit
// mode. It has no real source to read from, so race a timeout and assert only
// that block-range validation didn't reject it.
it("enters auto-exit mode when endBlock is omitted without simulate", async (t) => {
const indexer = createTestIndexer();
const message = await messageOf(() =>
Promise.race([
indexer.process({ chains: { 1: { startBlock: 1 } } }),
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000)),
]),
);

t.expect(message === undefined || !message.includes("endBlock is required")).toBe(true);
});
});
`,
)
Loading