From c6cb6714066ea07713d5b796dec7b6b64cc87c81 Mon Sep 17 00:00:00 2001 From: Alexander Parvanov <47386361+YYx00xZZ@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:10:15 +0300 Subject: [PATCH] Add tests (#6) * Rework tests to remove dependency on hardhat network * Add Docs for testing * update readme * temporary allow workflow to run on push and pr * Add tests * update container name for tests run * Update workflow * Cleanup * rename workflow --- .../{deploy.yml => deploy-contract.yml} | 32 +++- README.md | 19 +++ hardhat/test/Lock.ts | 144 ++++++++---------- hardhat/test/Lock.ts.backup | 127 +++++++++++++++ 4 files changed, 231 insertions(+), 91 deletions(-) rename .github/workflows/{deploy.yml => deploy-contract.yml} (66%) create mode 100644 hardhat/test/Lock.ts.backup diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-contract.yml similarity index 66% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-contract.yml index 3fb4ca75c6..f33a51549c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-contract.yml @@ -1,14 +1,14 @@ -name: deploy +name: deploy contract and run test -on: - pull_request: - types: [closed] - branches: [master] - paths: ["**"] +on: [push, pull_request] + # pull_request: + # types: [closed] + # branches: [master] + # paths: ["**"] jobs: deploy: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CI:Deploy') + # if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CI:Deploy') runs-on: ubuntu-latest steps: @@ -72,9 +72,27 @@ jobs: docker build -t $IMAGE_NAME:with-contract-latest -t $IMAGE_NAME:with-contract-$COMMIT_SHA -f Dockerfile.with-contract . rm Dockerfile.with-contract + rm -rf ./geth_data docker images -a - name: push image run: | docker push $IMAGE_NAME:with-contract-latest docker push $IMAGE_NAME:with-contract-$COMMIT_SHA + + - name: start geth with contract + run: | + docker run -d --name geth-service-with-contract -p 8545:8545 -p 30303:30303 yyx00xzz/geth-dev:with-contract-latest --dev --http --http.addr 0.0.0.0 --http.port 8545 --http.api admin,debug,web3,eth,txpool,miner,net,dev --miner.gasprice "1" --http.corsdomain "*" + echo "Waiting for Geth to be ready..." + # Poll the Geth RPC endpoint until it's responsive + while ! curl -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' http://localhost:8545 > /dev/null; do + echo "Geth not ready yet, waiting..." + docker logs geth-service + sleep 5 + done + echo "Geth is ready." + + - name: run tests + run: | + cd ./hardhat + npx hardhat test --network geth diff --git a/README.md b/README.md index 36ec52df93..1b57de55b7 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,25 @@ Using mounted dir for the data can be tricky because of permission errs tho. Currently, the new image (with contract) is built from the latest base image. This should be extended in the future to support building from a given tag. +--- + +### Tests +The default tests that Hardhat Sample Project provides depends on using the Hardhat network. +This is an example of an error they throw when ran against the Geth: +``` + 4) Lock + Deployment + Should fail if the unlockTime is not in the future: + OnlyHardhatNetworkError: This helper can only be used with Hardhat Network. You are connected to 'geth'. + at checkIfDevelopmentNetwork (/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/utils.ts:30:11) + at getHardhatProvider (/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/utils.ts:41:9) + at async Object.latest (/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/helpers/time/latest.ts:7:20) + at async Context. (/hardhat/test/Lock.ts:54:26) +``` +I've provided the same tests reworked to be compatible with Geth. + +When running the docker container with Geth, I do not use bind mount and I am making sure that the geth_data dir from previous steps is cleaned so I can be sure that I am working with the contract previously deployed in the container. + ## Go Ethereum Golang execution layer implementation of the Ethereum protocol. diff --git a/hardhat/test/Lock.ts b/hardhat/test/Lock.ts index 160dbfa163..e6326dc0cb 100644 --- a/hardhat/test/Lock.ts +++ b/hardhat/test/Lock.ts @@ -1,127 +1,103 @@ -import { - time, - loadFixture, -} from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { expect } from "chai"; import hre from "hardhat"; describe("Lock", function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployOneYearLockFixture() { - const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; - const ONE_GWEI = 1_000_000_000; - - const lockedAmount = ONE_GWEI; - const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; - - // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await hre.ethers.getSigners(); + const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; + const ONE_GWEI = 1_000_000_000; + const lockedAmount = ONE_GWEI; + async function deployLock(unlockTime: number) { const Lock = await hre.ethers.getContractFactory("Lock"); const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); - - return { lock, unlockTime, lockedAmount, owner, otherAccount }; + return lock; } describe("Deployment", function () { it("Should set the right unlockTime", async function () { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); - + const unlockTime = Math.floor(Date.now() / 1000) + ONE_YEAR_IN_SECS; + const lock = await deployLock(unlockTime); expect(await lock.unlockTime()).to.equal(unlockTime); }); it("Should set the right owner", async function () { - const { lock, owner } = await loadFixture(deployOneYearLockFixture); - + const unlockTime = Math.floor(Date.now() / 1000) + ONE_YEAR_IN_SECS; + const [owner] = await hre.ethers.getSigners(); + const lock = await deployLock(unlockTime); expect(await lock.owner()).to.equal(owner.address); }); it("Should receive and store the funds to lock", async function () { - const { lock, lockedAmount } = await loadFixture( - deployOneYearLockFixture - ); - + const unlockTime = Math.floor(Date.now() / 1000) + ONE_YEAR_IN_SECS; + const lock = await deployLock(unlockTime); expect(await hre.ethers.provider.getBalance(lock.target)).to.equal( lockedAmount ); }); it("Should fail if the unlockTime is not in the future", async function () { - // We don't use the fixture here because we want a different deployment - const latestTime = await time.latest(); - const Lock = await hre.ethers.getContractFactory("Lock"); - await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( + const latestTime = Math.floor(Date.now() / 1000); + await expect(deployLock(latestTime)).to.be.revertedWith( "Unlock time should be in the future" ); }); }); describe("Withdrawals", function () { - describe("Validations", function () { - it("Should revert with the right error if called too soon", async function () { - const { lock } = await loadFixture(deployOneYearLockFixture); - - await expect(lock.withdraw()).to.be.revertedWith( - "You can't withdraw yet" - ); - }); - - it("Should revert with the right error if called from another account", async function () { - const { lock, unlockTime, otherAccount } = await loadFixture( - deployOneYearLockFixture - ); - - // We can increase the time in Hardhat Network - await time.increaseTo(unlockTime); - - // We use lock.connect() to send a transaction from another account - await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( - "You aren't the owner" - ); - }); - - it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { - const { lock, unlockTime } = await loadFixture( - deployOneYearLockFixture - ); - - // Transactions are sent using the first signer by default - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).not.to.be.reverted; - }); + it("Should revert with the right error if called too soon", async function () { + const unlockTime = Math.floor(Date.now() / 1000) + ONE_YEAR_IN_SECS; + const lock = await deployLock(unlockTime); + await expect(lock.withdraw()).to.be.revertedWith("You can't withdraw yet"); }); - describe("Events", function () { - it("Should emit an event on withdrawals", async function () { - const { lock, unlockTime, lockedAmount } = await loadFixture( - deployOneYearLockFixture - ); + it("Should revert with the right error if called from another account", async function () { + const unlockTime = Math.floor(Date.now() / 1000) + 10; // Unlock in 10 seconds + const lock = await deployLock(unlockTime); + const [, otherAccount] = await hre.ethers.getSigners(); - await time.increaseTo(unlockTime); + // Wait for unlock time + await new Promise((resolve) => setTimeout(resolve, 11000)); - await expect(lock.withdraw()) - .to.emit(lock, "Withdrawal") - .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg - }); + await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( + "You aren't the owner" + ); }); - describe("Transfers", function () { - it("Should transfer the funds to the owner", async function () { - const { lock, unlockTime, lockedAmount, owner } = await loadFixture( - deployOneYearLockFixture - ); + it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { + const unlockTime = Math.floor(Date.now() / 1000) + 10; // Unlock in 10 seconds + const lock = await deployLock(unlockTime); - await time.increaseTo(unlockTime); + // Wait for unlock time + await new Promise((resolve) => setTimeout(resolve, 11000)); - await expect(lock.withdraw()).to.changeEtherBalances( - [owner, lock], - [lockedAmount, -lockedAmount] - ); - }); + await expect(lock.withdraw()).not.to.be.reverted; + }); + + it("Should emit an event on withdrawals", async function () { + const unlockTime = Math.floor(Date.now() / 1000) + 10; // Unlock in 10 seconds + const lock = await deployLock(unlockTime); + const [owner] = await hre.ethers.getSigners(); + + // Wait for unlock time + await new Promise((resolve) => setTimeout(resolve, 11000)); + + await expect(lock.withdraw()) + .to.emit(lock, "Withdrawal") + .withArgs(lockedAmount, anyValue); + }); + + it("Should transfer the funds to the owner", async function () { + const unlockTime = Math.floor(Date.now() / 1000) + 10; // Unlock in 10 seconds + const lock = await deployLock(unlockTime); + const [owner] = await hre.ethers.getSigners(); + + // Wait for unlock time + await new Promise((resolve) => setTimeout(resolve, 11000)); + + await expect(lock.withdraw()).to.changeEtherBalances( + [owner, lock], + [lockedAmount, -lockedAmount] + ); }); }); }); diff --git a/hardhat/test/Lock.ts.backup b/hardhat/test/Lock.ts.backup new file mode 100644 index 0000000000..160dbfa163 --- /dev/null +++ b/hardhat/test/Lock.ts.backup @@ -0,0 +1,127 @@ +import { + time, + loadFixture, +} from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { expect } from "chai"; +import hre from "hardhat"; + +describe("Lock", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function deployOneYearLockFixture() { + const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; + const ONE_GWEI = 1_000_000_000; + + const lockedAmount = ONE_GWEI; + const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; + + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await hre.ethers.getSigners(); + + const Lock = await hre.ethers.getContractFactory("Lock"); + const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); + + return { lock, unlockTime, lockedAmount, owner, otherAccount }; + } + + describe("Deployment", function () { + it("Should set the right unlockTime", async function () { + const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); + + expect(await lock.unlockTime()).to.equal(unlockTime); + }); + + it("Should set the right owner", async function () { + const { lock, owner } = await loadFixture(deployOneYearLockFixture); + + expect(await lock.owner()).to.equal(owner.address); + }); + + it("Should receive and store the funds to lock", async function () { + const { lock, lockedAmount } = await loadFixture( + deployOneYearLockFixture + ); + + expect(await hre.ethers.provider.getBalance(lock.target)).to.equal( + lockedAmount + ); + }); + + it("Should fail if the unlockTime is not in the future", async function () { + // We don't use the fixture here because we want a different deployment + const latestTime = await time.latest(); + const Lock = await hre.ethers.getContractFactory("Lock"); + await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( + "Unlock time should be in the future" + ); + }); + }); + + describe("Withdrawals", function () { + describe("Validations", function () { + it("Should revert with the right error if called too soon", async function () { + const { lock } = await loadFixture(deployOneYearLockFixture); + + await expect(lock.withdraw()).to.be.revertedWith( + "You can't withdraw yet" + ); + }); + + it("Should revert with the right error if called from another account", async function () { + const { lock, unlockTime, otherAccount } = await loadFixture( + deployOneYearLockFixture + ); + + // We can increase the time in Hardhat Network + await time.increaseTo(unlockTime); + + // We use lock.connect() to send a transaction from another account + await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( + "You aren't the owner" + ); + }); + + it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { + const { lock, unlockTime } = await loadFixture( + deployOneYearLockFixture + ); + + // Transactions are sent using the first signer by default + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()).not.to.be.reverted; + }); + }); + + describe("Events", function () { + it("Should emit an event on withdrawals", async function () { + const { lock, unlockTime, lockedAmount } = await loadFixture( + deployOneYearLockFixture + ); + + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()) + .to.emit(lock, "Withdrawal") + .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg + }); + }); + + describe("Transfers", function () { + it("Should transfer the funds to the owner", async function () { + const { lock, unlockTime, lockedAmount, owner } = await loadFixture( + deployOneYearLockFixture + ); + + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()).to.changeEtherBalances( + [owner, lock], + [lockedAmount, -lockedAmount] + ); + }); + }); + }); +});