diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de1b61..9c1cff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.5.5 (unreleased) * Added function `advanceBlockTo`. ([#94](https://github.com/OpenZeppelin/openzeppelin-test-helpers/pull/94)) + * Added `expectEvent.not` support to test negative cases. ([#104](https://github.com/OpenZeppelin/openzeppelin-test-helpers/pull/104)) ## 0.5.4 (2019-11-13) * Fixed some RPC calls not having an `id` field. ([#92](https://github.com/OpenZeppelin/openzeppelin-test-helpers/pull/92)) diff --git a/contracts/EventEmitter.sol b/contracts/EventEmitter.sol index a34fd72..a0b743d 100644 --- a/contracts/EventEmitter.sol +++ b/contracts/EventEmitter.sol @@ -4,6 +4,7 @@ import "./IndirectEventEmitter.sol"; contract EventEmitter { event Argumentless(); + event WillNeverBeEmitted(); event ShortUint(uint8 value); event ShortInt(int8 value); event LongUint(uint256 value); diff --git a/docs/modules/ROOT/pages/api.adoc b/docs/modules/ROOT/pages/api.adoc index 4878da5..f0d156d 100644 --- a/docs/modules/ROOT/pages/api.adoc +++ b/docs/modules/ROOT/pages/api.adoc @@ -161,6 +161,43 @@ async function expectEvent.inConstruction(emitter, eventName, eventArgs = {}) Same as `inTransaction`, but for events emitted during the construction of `emitter`. Note that this is currently only supported for truffle contracts. +=== `not.inTransaction` + +```javascript +async function notInTransaction (txHash, emitter, eventName) +``` + +Asserts that the event with name `eventName` as declared in the `emitter` contract was not emitted in the transaction with hash `txHash`. Note that the assertion will also fail if the transaction was emitted indirectly (in a nested external function call). + +```javascript +// With web3 contracts +const contract = await MyContract.deploy().send(); +const { transactionHash } = await contract.methods.foo().send(); +await expectEvent.not.inTransaction(transactionHash, contract, 'NotEmittedByFoo'); + +// With web3 contracts +const contract = await MyContract.deploy().send(); +const { transactionHash } = await contract.methods.foo().send(); +await expectEvent.not.inTransaction(transactionHash, contract, 'EmittedByFoo'); // Will fail + +// With truffle contracts +const contract = await MyContract.new(); +const { txHash } = await contract.foo(); +await expectEvent.not.inTransaction(txHash, contract, 'NotEmittedByFoo'); + +const contract = await MyContract.new(); +const { txHash } = await contract.foo(); +await expectEvent.not.inTransaction(txHash, contract, 'EmittedByFoo'); // Will fail +``` + +=== `not.inConstruction` + +```javascript +async function notInConstruction (contract, eventName) +``` + +Same as `not.inTransaction`, but for events emitted during the construction of emitter. Note that this is currently only supported for truffle contracts. + [[expect-revert]] == `expectRevert` diff --git a/src/expectEvent.js b/src/expectEvent.js index a8f8594..25fda56 100644 --- a/src/expectEvent.js +++ b/src/expectEvent.js @@ -64,6 +64,13 @@ async function inConstruction (contract, eventName, eventArgs = {}) { return inTransaction(contract.transactionHash, contract.constructor, eventName, eventArgs); } +async function notInConstruction (contract, eventName) { + if (!isTruffleContract(contract)) { + throw new Error('expectEvent.inConstruction is only supported for truffle-contract objects'); + } + return notInTransaction(contract.transactionHash, contract.constructor, eventName); +} + async function inTransaction (txHash, emitter, eventName, eventArgs = {}) { const receipt = await web3.eth.getTransactionReceipt(txHash); @@ -71,6 +78,16 @@ async function inTransaction (txHash, emitter, eventName, eventArgs = {}) { return inLogs(logs, eventName, eventArgs); } +async function notInTransaction (txHash, emitter, eventName) { + const receipt = await web3.eth.getTransactionReceipt(txHash); + + const logs = decodeLogs(receipt.logs, emitter, eventName); + + const events = logs.filter(e => e.event === eventName); + + expect(events.length === 0).to.equal(true, `Event ${eventName} was found`); +} + // This decodes longs for a single event type, and returns a decoded object in // the same form truffle-contract uses on its receipts function decodeLogs (logs, emitter, eventName) { @@ -138,4 +155,8 @@ function isTruffleContract (contract) { expectEvent.inLogs = deprecate(inLogs, 'expectEvent.inLogs() is deprecated. Use expectEvent() instead.'); expectEvent.inConstruction = inConstruction; expectEvent.inTransaction = inTransaction; + +expectEvent.not = {}; +expectEvent.not.inConstruction = notInConstruction; +expectEvent.not.inTransaction = notInTransaction; module.exports = expectEvent; diff --git a/test/src/expectEvent.truffle.test.js b/test/src/expectEvent.truffle.test.js index 184fc00..97b5285 100644 --- a/test/src/expectEvent.truffle.test.js +++ b/test/src/expectEvent.truffle.test.js @@ -458,4 +458,63 @@ contract('expectEvent (truffle contracts)', function ([deployer]) { }); }); }); + + describe('not', function () { + describe('inTransaction', function () { + context('with no arguments', function () { + beforeEach(async function () { + const { receipt } = await this.emitter.emitArgumentless(); + this.txHash = receipt.transactionHash; + }); + it('accepts not emitted events', async function () { + await expectEvent.not.inTransaction(this.txHash, EventEmitter, 'WillNeverBeEmitted'); + }); + it('throws when event does not exist in ABI', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, EventEmitter, 'Nonexistant')); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, EventEmitter, 'Argumentless')); + }); + }); + context('with arguments', function () { + beforeEach(async function () { + this.value = 42; + const { receipt } = await this.emitter.emitShortUint(this.value); + this.txHash = receipt.transactionHash; + }); + it('accepts not emitted events', async function () { + await expectEvent.not.inTransaction(this.txHash, EventEmitter, 'WillNeverBeEmitted'); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, EventEmitter, 'ShortUint')); + }); + }); + context('with events emitted by an indirectly called contract', function () { + beforeEach(async function () { + this.value = 'OpenZeppelin'; + const { receipt } = await this.emitter.emitStringAndEmitIndirectly(this.value, this.secondEmitter.address); + this.txHash = receipt.transactionHash; + }); + it('accepts not emitted events', async function () { + await expectEvent.not.inTransaction(this.txHash, EventEmitter, 'WillNeverBeEmitted'); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, IndirectEventEmitter, 'IndirectString')); + }); + }); + }); + describe('inConstructor', function () { + it('accepts not emitted events', async function () { + await expectEvent.not.inConstruction(this.emitter, 'WillNeverBeEmitted'); + }); + it('throws when event does not exist in ABI', async function () { + await assertFailure(expectEvent.not.inConstruction(this.emitter, 'Nonexistant')); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inConstruction(this.emitter, 'ShortUint')); + await assertFailure(expectEvent.not.inConstruction(this.emitter, 'Boolean')); + await assertFailure(expectEvent.not.inConstruction(this.emitter, 'String')); + }); + }); + }); }); diff --git a/test/src/expectEvent.web3.test.js b/test/src/expectEvent.web3.test.js index 6e1b7ae..2ea8304 100644 --- a/test/src/expectEvent.web3.test.js +++ b/test/src/expectEvent.web3.test.js @@ -430,4 +430,54 @@ contract('expectEvent (web3 contracts) ', function ([deployer]) { await assertFailure(expectEvent.inConstruction(this.emitter, 'ShortUint')); }); }); + + describe('not', function () { + describe('inTransaction', function () { + context('with no arguments', function () { + beforeEach(async function () { + ({ transactionHash: this.txHash } = await this.emitter.methods.emitArgumentless().send()); + }); + it('accepts not emitted events', async function () { + await expectEvent.not.inTransaction(this.txHash, EventEmitter, 'WillNeverBeEmitted'); + }); + it('throws when event does not exist in ABI', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, EventEmitter, 'Nonexistant')); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, EventEmitter, 'Argumentless')); + }); + }); + context('with arguments', function () { + beforeEach(async function () { + this.value = 42; + ({ transactionHash: this.txHash } = await this.emitter.methods.emitShortUint(this.value).send()); + }); + it('accepts not emitted events', async function () { + await expectEvent.not.inTransaction(this.txHash, EventEmitter, 'WillNeverBeEmitted'); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, EventEmitter, 'ShortUint')); + }); + }); + context('with events emitted by an indirectly called contract', function () { + beforeEach(async function () { + this.value = 'OpenZeppelin'; + ({ transactionHash: this.txHash } = await this.emitter.methods.emitStringAndEmitIndirectly( + this.value, this.secondEmitter.options.address + ).send()); + }); + it('accepts not emitted events', async function () { + await expectEvent.not.inTransaction(this.txHash, EventEmitter, 'WillNeverBeEmitted'); + }); + it('throws when event its emitted', async function () { + await assertFailure(expectEvent.not.inTransaction(this.txHash, IndirectEventEmitter, 'IndirectString')); + }); + }); + }); + describe('inConstruction', function () { + it('is unsupported', async function () { + await assertFailure(expectEvent.not.inConstruction(this.emitter, 'ShortUint')); + }); + }); + }); });