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
This commit is contained in:
Alexander Parvanov 2025-04-09 11:10:15 +03:00 committed by GitHub
parent 7df5aa3c92
commit c6cb671406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 231 additions and 91 deletions

View file

@ -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

View file

@ -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 (<obfustcated>/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/utils.ts:30:11)
at getHardhatProvider (<obfuscated>/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/utils.ts:41:9)
at async Object.latest (<obfuscated>/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/helpers/time/latest.ts:7:20)
at async Context.<anonymous> (<obfuscated>/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.

View file

@ -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]
);
});
});
});

127
hardhat/test/Lock.ts.backup Normal file
View file

@ -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]
);
});
});
});
});