const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("EdutHumanMembership", function () { async function expectRevertWithCustomError(promise, errorName) { try { await promise; expect.fail(`expected revert ${errorName}`); } catch (err) { expect(String(err)).to.contain(errorName); } } async function deployFixture() { const [deployer, treasury, sponsor, recipient, other] = await ethers.getSigners(); const factory = await ethers.getContractFactory("EdutHumanMembership", deployer); const price = ethers.utils.parseEther("0.002"); const contract = await factory.deploy(treasury.address, ethers.constants.AddressZero, price); await contract.deployed(); return { contract, deployer, treasury, sponsor, recipient, other, price }; } it("mints to recipient with sponsor payment", async function () { const { contract, treasury, sponsor, recipient, price } = await deployFixture(); const before = await ethers.provider.getBalance(treasury.address); const tx = await contract.connect(sponsor).mintMembership(recipient.address, { value: price }); await tx.wait(); const after = await ethers.provider.getBalance(treasury.address); expect(after.sub(before).eq(price)).to.equal(true); expect(await contract.ownerOf(1)).to.equal(recipient.address); const token = await contract.tokenOf(recipient.address); expect(token.tokenId.toNumber()).to.equal(1); expect(token.status).to.equal(1); expect(await contract.hasMembership(recipient.address)).to.equal(true); }); it("rejects duplicate mint for same human wallet", async function () { const { contract, sponsor, recipient, price } = await deployFixture(); await (await contract.connect(sponsor).mintMembership(recipient.address, { value: price })).wait(); await expectRevertWithCustomError( contract.connect(sponsor).mintMembership(recipient.address, { value: price }), "AlreadyMinted" ); }); it("supports owner-configurable price and owner-only admin", async function () { const { contract, deployer, other } = await deployFixture(); const nextPrice = ethers.utils.parseEther("0.003"); await expectRevertWithCustomError( contract.connect(other).updateMintPrice(ethers.constants.AddressZero, nextPrice), "NotOwner" ); await (await contract.connect(deployer).updateMintPrice(ethers.constants.AddressZero, nextPrice)).wait(); const price = await contract.currentMintPrice(); expect(price.amountAtomic.eq(nextPrice)).to.equal(true); }); it("blocks transfer paths (soulbound)", async function () { const { contract, sponsor, recipient, other, price } = await deployFixture(); await (await contract.connect(sponsor).mintMembership(recipient.address, { value: price })).wait(); await expectRevertWithCustomError( contract.connect(recipient).transferFrom(recipient.address, other.address, 1), "SoulboundNonTransferable" ); }); it("allows owner to update status", async function () { const { contract, deployer, sponsor, recipient, price } = await deployFixture(); await (await contract.connect(sponsor).mintMembership(recipient.address, { value: price })).wait(); await (await contract.connect(deployer).setMembershipStatus(recipient.address, 2)).wait(); expect(await contract.membershipStatus(recipient.address)).to.equal(2); }); it("supports ERC20 settlement path for USDC-style pricing", async function () { const [deployer, treasury, sponsor, recipient] = await ethers.getSigners(); const tokenFactory = await ethers.getContractFactory("MockERC20", deployer); const usdc = await tokenFactory.deploy("Mock USDC", "USDC", 6); await usdc.deployed(); const membershipFactory = await ethers.getContractFactory("EdutHumanMembership", deployer); const usdcPrice = ethers.BigNumber.from("100000000"); // 100 USDC with 6 decimals const contract = await membershipFactory.deploy(treasury.address, usdc.address, usdcPrice); await contract.deployed(); await (await usdc.mint(sponsor.address, usdcPrice)).wait(); await (await usdc.connect(sponsor).approve(contract.address, usdcPrice)).wait(); const before = await usdc.balanceOf(treasury.address); await (await contract.connect(sponsor).mintMembership(recipient.address)).wait(); const after = await usdc.balanceOf(treasury.address); expect(after.sub(before).eq(usdcPrice)).to.equal(true); expect(await contract.hasMembership(recipient.address)).to.equal(true); }); it("fails ERC20 mint without allowance", async function () { const [deployer, treasury, sponsor, recipient] = await ethers.getSigners(); const tokenFactory = await ethers.getContractFactory("MockERC20", deployer); const usdc = await tokenFactory.deploy("Mock USDC", "USDC", 6); await usdc.deployed(); const membershipFactory = await ethers.getContractFactory("EdutHumanMembership", deployer); const usdcPrice = ethers.BigNumber.from("100000000"); const contract = await membershipFactory.deploy(treasury.address, usdc.address, usdcPrice); await contract.deployed(); await (await usdc.mint(sponsor.address, usdcPrice)).wait(); await expectRevertWithCustomError( contract.connect(sponsor).mintMembership(recipient.address), "PaymentTransferFailed" ); }); });