[Ethereum] テストプロジェクト fundraiser

$ mkdir fundraiser
$ cd fundraiser
$ truffle unbox react
$ tree
$ rm contracts/SimpleStorage.sol \
migrations/2_deploy_contracts.js \
test/*
$ touch contracts/Fundraiser.sol test/fundraiser_test.js

受取人の名前、Webサイト、受取人のアドレス、トップページのカードに使う画像URL、受取人に関する説明、管理人またはownerのアドレス

fundraiser_test.js

const FundraiserContract = artifacts.require("Fundraiser");

contract("Fundraiser", accounts => {
	let fundraiser;
	const name = "Beneficiary Name";
	const url = "beneficiaryname.org";
	const imageURL = "https://placekitten.com/600/350";
	const description = "Beneficiary description";
	const beneficiary = accounts[1];
	const owner = accounts[0];

	beforeEach(async() => {
		fundraiser = await FundraiserContract.new(
			name,
			url,
			imageURL,
			description,
			beneficiary,
			owner
		);
	});

	describe("initialization", () => {
		

		it("gets the beneficiary name", async() => {
			const actual = await fundraiser.name();
			assert.equal(actual, name, "names should match");
		});

		it("gets the beneficiary url", async() => {
			const actual = await fundraiser.url();
			assert.equal(actual, url, "url should match");
		});

		it("gets the beneficiary image url", async() => {
			const actual = await fundraiser.imageURL();
			assert.equal(actual, imageURL, "imageURL should match");
		});

		it("gets the beneficiary description", async() => {
			const actual = await fundraiser.description();
			assert.equal(actual, description, "description should match");
		});

		it("gets the beneficiary", async() => {
			const actual = await fundraiser.beneficiary();
			assert.equal(actual, beneficiary, "beneficiary addresses should match");
		});

		it("gets the owner", async() => {
			const actual = await fundraiser.owner();
			assert.equal(actual, owner, "bios should match");
		})

	});

	describe("setBeneficiary", () => {
		const newBeneficiary = accounts[2];

		it("updated beneficiary when called by owner account", async() => {
			await fundraiser.setBeneficiary(newBeneficiary, {from: owner});
			const actualBeneficiary = await fundraiser.beneficiary();
			assert.equal(actualBeneficiary, newBeneficiary,
				"beneficiaries should match");
		})

		it("throws an error when called from a non-owner account", async() => {
			try {
				await fundraiser.setBeneficiary(newBeneficiary, {from: accounts[3]});
				assert.fail("withdraw was not restricted to owners")
			} catch(err) {
				const expectedError = "Ownable: caller is not the owner"
				const actualError = err.reason;
				assert.equal(actualError, expectedError, "should not be permitted")
			}
		})
	});

	describe("making donations", () => {
		const value = web3.utils.toWei('0.0289');
		const donor = accounts[2];

		it("increases myDonationsCount", async() => {
			const currentDonationsCount = await fundraiser.myDonationsCount(
				{from: donor}
			);

			await fundraiser.donate({from: donor, value});

			const newDonationsCount = await fundraiser.myDonationsCount(
				{from: donor}
			);

			assert.equal(
				1,
				newDonationsCount - currentDonationsCount,
				"myDonationsCount should increment by 1"
			);
		});
		it("includes donation in myDonations", async() => {
			await fundraiser.donate({from: donor, value});
			const {values, dates} = await fundraiser.myDonations(
					{from: donor}
				);
			assert.equal(
					value,
					values[0],
					"values should match"
				);
			assert(dates[0], "date should be present");
		});
		it("increases the totalDonations amaount", async() => {
			const currentTotalDonations = await fundraiser.totalDonations();
			await fundraiser.donate({from: donor, value});
			const newTotalDonations = await fundraiser.totalDonations();

			const diff = newTotalDonations - currentTotalDonations;

			assert.equal(
				diff,
				value,
				"difference should match the donation value"
			);
		});
		it("increases donationsCount", async() => {
			const currentDonationsCount = await fundraiser.donationsCount();
			await fundraiser.donate({from: donor, value});
			const newDonationsCount = await fundraiser.donationsCount();

			assert.equal(
				1,
				newDonationsCount - currentDonationsCount,
				"donationsCount should increment by 1"
			)
		})
		it("emits the DonationReceived event", async() => {
			const tx = await fundraiser.donate({from: donor, value});
			const expectedEvent = "DonationReceived";
			const actualEvent = tx.logs[0].event;

			assert.equal(actualEvent, expectedEvent, "events should match");
		})
	});
	describe("withdrawing funds", ()=> {
		beforeEach(async() => {
			await fundraiser.donate(
				{from: accounts[2], value: web3.utils.toWei('0.1')}
			);
		});

		describe("access controls", () => {
			it("throws an error when called from a non-owner account", async() => {
				try {
					await fundraiser.withdraw({from: accounts[3]});
					assert.fail("withdraw was not restricted to owners");
				} catch(err) {
					const expectedError = "Ownable: caller is not the owner";
					const actualError = err.reason;
					assert.equal(actualError, expectedError, "should not be permitted");
				}
			});

			it("permits the owner to call the function", async() => {
				try {
					await fundraiser.withdraw({from: owner});
					assert(true, "no errors were thrown");
				} catch(err) {
					assert.fail("should not have thrown an error");
				}
			})
		});

		it("transfers balance to beneficiary", async() => {
			const currentContractBalance = await web3.eth.getBalance(fundraiser.address);
			const currentBeneficiaryBalance = await web3.eth.getBalance(beneficiary);

			await fundraiser.withdraw({from: owner});

			const newContractBalance = await web3.eth.getBalance(fundraiser.address);
			const newBeneficiaryBalance = await web3.eth.getBalance(beneficiary);
			const beneficiaryDifference = newBeneficiaryBalance - currentBeneficiaryBalance;

			assert.equal(
				newContractBalance,
				0,
				"contract should have a 0 balance"
			);
			assert.equal(
				beneficiaryDifference,
				currentContractBalance,
				"beneficiary should receive all the funds"
			);
		});

		it("emit Withdraw event", async()=>{
			const tx = await fundraiser.withdraw({from: owner});
			const expectedEvent = "Withdraw";
			const actualEvent = tx.logs[0].event;

			assert.equal(
				actualEvent,
				expectedEvent,
				"events should match"
			);
		});
	});
	describe("fallback function", () => {
		const value = web3.utils.toWei('0.0289');

		it("increases the totalDonation amount", async() => {
			const currentTotalDonations = await fundraiser.totalDonations();
			await web3.eth.sendTransaction(
				{to: fundraiser.address, from: accounts[9], value}
			);
			const newTotalDonations = await fundraiser.totalDonations();

			const diff = newTotalDonations - currentTotalDonations;

			assert.equal(
				diff,
				value,
				"difference should match the donation value"
			);
		});

		it("increase donationsCount", async() => {
			const currentDonationsCount = await fundraiser.donationsCount();
			await web3.eth.sendTransaction(
				{to: fundraiser.address, from: accounts[9], value}
			);
			const newDonationsCount = await fundraiser.donationsCount();

			assert.equal(
				1,
				newDonationsCount - currentDonationsCount,
				"donationsCount should increment by 1"
			);
		});
	});
});

Fundraiser.sol

pragma solidity >0.4.23 <=0.8.0;

import "openzeppelin-solidity/contracts/access/Ownable.sol";
import "openzeppelin-solidity/contracts/utils/math/SafeMath.sol";

contract Fundraiser is Ownable{
	using SafeMath for uint256;

	uint256 public totalDonations;
	uint256 public donationsCount;
	event DonationReceived(address indexed donor, uint256 value);
	event Withdraw(uint256 amount);

	struct Donation {
		uint256 value;
		uint256 date;
	}
	mapping(address => Donation[]) private _donations;

	string public name;
	string public url;
	string public imageURL;
	string public description;

	address payable public beneficiary;

	constructor(
		string memory _name,
		string memory _url,
		string memory _imageURL,
		string memory _description,
		address payable _beneficiary,
		address _custodian
	)

	public {
		name = _name;
		url = _url;
		imageURL = _imageURL;
		description = _description;
		beneficiary = _beneficiary;
		transferOwnership(_custodian);
	}

	function setBeneficiary(address payable _beneficiary) public onlyOwner {
		beneficiary = _beneficiary;
	}

	function myDonationsCount() public view returns(uint256){
		return _donations[msg.sender].length;
	}

	function donate() public payable {
		Donation memory donation = Donation({
			value: msg.value,
			date: block.timestamp
		});
		_donations[msg.sender].push(donation);
		totalDonations = totalDonations.add(msg.value);
		donationsCount++;

		emit DonationReceived(msg.sender, msg.value);
	}

	function myDonations() public view returns(
		uint256[] memory values,
		uint256[] memory dates
	){
		uint256 count = myDonationsCount();
		values = new uint256[](count);
		dates = new uint256[](count);

		for (uint256 i = 0; i < count; i++){
			Donation storage donation = _donations[msg.sender][i];
			values[i] = donation.value;
			dates[i] = donation.date;
		}

		return (values, dates);
	}

	function withdraw() public onlyOwner {
		uint256 balance = address(this).balance;
		beneficiary.transfer(balance);
		emit Withdraw(balance);
	}

	fallback () external payable {
		totalDonations = totalDonations.add(msg.value);
		donationsCount++;
	}
}

address型とaddress payable型の二つのアドレスがある
少しずつ理解してきました