// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/Coupon.sol"; import "../src/interfaces/ICoupon.sol"; contract CouponTest is Test { Coupon coupon; address admin = address(1); address factory = address(2); address settler = address(3); address user1 = address(4); address user2 = address(5); function setUp() public { vm.startPrank(admin); coupon = new Coupon(); coupon.initialize("Genex Coupon", "GXC", admin); coupon.grantRole(keccak256("FACTORY_ROLE"), factory); coupon.grantRole(keccak256("SETTLER_ROLE"), settler); vm.stopPrank(); } function test_MintSetsConfigCorrectly() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); ICoupon.CouponConfig memory config = coupon.getConfig(tokenId); assertEq(uint8(config.couponType), uint8(ICoupon.CouponType.Utility)); assertTrue(config.transferable); assertEq(config.maxResaleCount, 3); assertEq(coupon.ownerOf(tokenId), user1); assertEq(coupon.getFaceValue(tokenId), 100e6); } function test_BurnByOwner() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); vm.prank(user1); coupon.burn(tokenId); vm.expectRevert(); coupon.ownerOf(tokenId); } function test_BurnBySettler() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); vm.prank(settler); coupon.burn(tokenId); } function test_NonTransferableCouponReverts() public { uint256 tokenId = _mintNonTransferableCoupon(user1, 50e6); vm.prank(user1); vm.expectRevert("Coupon: non-transferable"); coupon.safeTransferFrom(user1, user2, tokenId); } function test_TransferableTransferSucceeds() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); vm.prank(user1); coupon.safeTransferFrom(user1, user2, tokenId); assertEq(coupon.ownerOf(tokenId), user2); } function test_MaxResaleCountEnforced() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); // 使用 settler 增加到上限 (3) vm.startPrank(settler); coupon.incrementResaleCount(tokenId); coupon.incrementResaleCount(tokenId); coupon.incrementResaleCount(tokenId); vm.stopPrank(); assertEq(coupon.getResaleCount(tokenId), 3); // 转移应被拒绝 vm.prank(user1); vm.expectRevert("Coupon: max resale count exceeded"); coupon.safeTransferFrom(user1, user2, tokenId); } function test_IncrementResaleCountOnlySettler() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); vm.prank(user1); vm.expectRevert(); coupon.incrementResaleCount(tokenId); } function test_BatchTransfer() public { uint256[] memory tokenIds = new uint256[](3); for (uint256 i = 0; i < 3; i++) { tokenIds[i] = _mintUtilityCoupon(user1, 100e6); } vm.prank(user1); coupon.batchTransfer(user1, user2, tokenIds); for (uint256 i = 0; i < 3; i++) { assertEq(coupon.ownerOf(tokenIds[i]), user2); } } function test_ResaleCountInitiallyZero() public { uint256 tokenId = _mintUtilityCoupon(user1, 100e6); assertEq(coupon.getResaleCount(tokenId), 0); } function test_MintIncrementsTokenId() public { uint256 id1 = _mintUtilityCoupon(user1, 100e6); uint256 id2 = _mintUtilityCoupon(user1, 200e6); assertEq(id2, id1 + 1); } // --- Helpers --- function _mintUtilityCoupon(address to, uint256 faceValue) internal returns (uint256) { bytes32[] memory stores = new bytes32[](0); ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: true, maxResaleCount: 3, maxPrice: faceValue, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: admin, faceValue: faceValue }); vm.prank(factory); return coupon.mint(to, faceValue, config); } function _mintNonTransferableCoupon(address to, uint256 faceValue) internal returns (uint256) { bytes32[] memory stores = new bytes32[](0); ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: false, maxResaleCount: 0, maxPrice: faceValue, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: admin, faceValue: faceValue }); vm.prank(factory); return coupon.mint(to, faceValue, config); } }