Skip to content
Open
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
95 changes: 95 additions & 0 deletions blockchain/test/lifecycle/Election.lifecycle.negative.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Election — Negative & Boundary Lifecycle Tests", function () {
async function deployElectionFixture(startOffset, endOffset) {
const [owner, voter, other] = await ethers.getSigners();

const now = (await ethers.provider.getBlock("latest")).timestamp;

const Election = await ethers.getContractFactory("Election");
const election = await Election.deploy();
await election.waitForDeployment();

const electionInfo = {
startTime: now + startOffset,
endTime: now + endOffset,
name: "Test Election",
description: "Test Description",
};

const candidates = [
{ candidateID: 0, name: "A", description: "A" },
{ candidateID: 1, name: "B", description: "B" },
];

await election.initialize(
electionInfo,
candidates,
0, // resultType
0, // electionId
ethers.ZeroAddress, // ballot (unused in negative tests)
owner.address,
ethers.ZeroAddress // resultCalculator (unused)
);

return { election, owner, voter, other, now };
}
it("reverts userVote before election start", async function () {
const { election, voter } = await deployElectionFixture(1000, 2000);

await expect(
election.connect(voter).userVote([0])
).to.be.revertedWithCustomError(election, "ElectionInactive");
});
it("reverts userVote after election end", async function () {
const { election, voter, now } = await deployElectionFixture(10, 20);

await ethers.provider.send("evm_setNextBlockTimestamp", [now + 30]);
await ethers.provider.send("evm_mine");

await expect(
election.connect(voter).userVote([0])
).to.be.revertedWithCustomError(election, "ElectionInactive");
});
it("reverts when user votes twice", async function () {
const { election, voter } = await deployElectionFixture(0, 1000);

const latest = await ethers.provider.getBlock("latest");
await ethers.provider.send("evm_setNextBlockTimestamp", [latest.timestamp + 1]);
await ethers.provider.send("evm_mine");

await expect(
election.connect(voter).userVote([0])
).to.be.reverted;

await expect(
election.connect(voter).userVote([0])
).to.be.reverted;
});
Comment on lines +55 to +69
Copy link

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the Election contract file
fd -t f "Election" --type f | grep -E "\.(sol|test\.js)$" | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 330


🏁 Script executed:

# Search for custom error definitions related to voting in solidity files
rg -n "error\s+\w*(Already|Duplicate|Voted)" --type solidity -A 1

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 103


🏁 Script executed:

# Also search for all custom errors in Election contract
rg -n "^\s*error\s+" -g "*.sol" | head -30

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1908


🏁 Script executed:

# Find the deployElectionFixture function definition
rg -n "deployElectionFixture" -B 2 -A 10 | head -50

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 4922


🏁 Script executed:

# Look at the userVote function in Election contract
rg -n "function userVote" -B 2 -A 15 --type solidity

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 103


🏁 Script executed:

# Read the full deployElectionFixture function
cat -n blockchain/test/lifecycle/Election.lifecycle.negative.test.js | sed -n '5,37p'

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1247


🏁 Script executed:

# Search for other tests that test voting (to see the pattern)
rg -n "userVote" blockchain/test/ -B 3 -A 3 | head -80

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 7459


🏁 Script executed:

# Look at the userVote function implementation
rg -n "function userVote" blockchain/contracts/Election.sol -B 2 -A 20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 891


🏁 Script executed:

# Check if there's a positive voting test to understand expected behavior
rg -n "should.*vote\|successfully.*vote" blockchain/test/ -i -B 2 -A 10 | head -60

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 53


Test logic error: first vote should succeed, not revert.

This test is named "reverts when user votes twice" but expects both votes to revert. For a valid double-voting test, the first vote should succeed, and only the second vote should revert with a specific error (AlreadyVoted).

Additionally, use .to.be.revertedWithCustomError(election, "AlreadyVoted") instead of generic .to.be.reverted, consistent with other tests in this file.

🐛 Proposed fix
 it("reverts when user votes twice", async function () {
-  const { election, voter } = await deployElectionFixture(0, 1000);
+  const { election, voter } = await deployElectionFixture(10, 2000);

   const latest = await ethers.provider.getBlock("latest");
-  await ethers.provider.send("evm_setNextBlockTimestamp", [latest.timestamp + 1]);
+  await ethers.provider.send("evm_setNextBlockTimestamp", [latest.timestamp + 15]);
   await ethers.provider.send("evm_mine");

-  await expect(
-    election.connect(voter).userVote([0])
-  ).to.be.reverted;
+  // First vote should succeed
+  await election.connect(voter).userVote([0]);

+  // Second vote should revert with specific error
   await expect(
     election.connect(voter).userVote([0])
-  ).to.be.reverted;
+  ).to.be.revertedWithCustomError(election, "AlreadyVoted");
 });
🤖 Prompt for AI Agents
In `@blockchain/test/lifecycle/Election.lifecycle.negative.test.js` around lines
55 - 69, The test currently asserts both votes revert but should allow the first
vote and only revert the second; change the first call to succeed (remove
expect) by invoking election.connect(voter).userVote([0]) normally, then assert
the second call reverts with the custom error using await
expect(election.connect(voter).userVote([0])).to.be.revertedWithCustomError(election,
"AlreadyVoted"); ensure you keep the existing timestamp advance and use the same
fixtures (deployElectionFixture, election, voter) and test title "reverts when
user votes twice".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, the election is intentionally initialized with a zero address ballot to keep the PR scoped to lifecycle boundary checks only. Because of this, the first userVote reverts before reaching the AlreadyVoted check.

I avoided introducing a mock ballot here to keep the PR minimal. If maintainers prefer strict AlreadyVoted validation, I am happy to add that in a follow-up PR with proper mocks.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harshkumarrai, thank you for the clarification! Your scoping decision makes sense—keeping the PR focused on lifecycle boundary checks without introducing mock ballot infrastructure is reasonable.

The test is valid as-is for verifying that voting fails with an invalid ballot configuration. To make the test's actual behavior clearer to future readers, consider adding a brief comment above the test explaining that the zero-address ballot causes the revert, so it's testing invalid configuration rather than pure double-voting logic:

// NOTE: Uses zero-address ballot; both votes revert due to invalid config.
// Dedicated AlreadyVoted validation requires ballot mock (deferred).
it("reverts when user votes twice", async function () {

A follow-up PR with proper ballot mocks to validate the AlreadyVoted error specifically would be valuable for comprehensive coverage, but it's not blocking for this PR's objectives.

٩(◕‿◕)۶


✏️ Learnings added
Learnt from: harshkumarrai
Repo: AOSSIE-Org/Agora-Blockchain PR: 227
File: blockchain/test/lifecycle/Election.lifecycle.negative.test.js:55-69
Timestamp: 2026-01-27T13:40:36.893Z
Learning: In blockchain/test/lifecycle/Election.lifecycle.negative.test.js, the double-voting test intentionally uses a zero-address ballot to keep the PR scoped to lifecycle checks only. Testing the AlreadyVoted error specifically requires introducing ballot mocks, which is deferred to a follow-up PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


it("reverts addCandidate after election start", async function () {
const { election, owner, now } = await deployElectionFixture(5, 1000);

await ethers.provider.send("evm_setNextBlockTimestamp", [now + 10]);
await ethers.provider.send("evm_mine");

await expect(
election.connect(owner).addCandidate("X", "X")
).to.be.revertedWithCustomError(election, "ElectionInactive");
});
it("reverts removeCandidate with invalid candidate id", async function () {
const { election, owner } = await deployElectionFixture(1000, 2000);

await expect(
election.connect(owner).removeCandidate(99)
).to.be.revertedWithCustomError(election, "InvalidCandidateID");
});
it("reverts getResult before election end", async function () {
const { election } = await deployElectionFixture(0, 1000);

await expect(
election.getResult()
).to.be.revertedWithCustomError(election, "ElectionIncomplete");
});
});