// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/CouponFactory.sol"; import "../src/Coupon.sol"; import "../src/Compliance.sol"; import "../src/interfaces/ICoupon.sol"; contract CouponFactoryTest is Test { CouponFactory factory; Coupon coupon; Compliance compliance; address admin = address(1); address issuer = address(2); address minter = address(3); function setUp() public { vm.startPrank(admin); // 部署 Compliance compliance = new Compliance(); compliance.initialize(admin); // 部署 Coupon coupon = new Coupon(); coupon.initialize("Genex Coupon", "GXC", admin); // 部署 CouponFactory factory = new CouponFactory(); factory.initialize(address(coupon), address(compliance), admin); // 授权 coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory)); factory.grantRole(keccak256("MINTER_ROLE"), minter); // 设置发行方 KYC compliance.setKycLevel(issuer, 3); vm.stopPrank(); } function test_MintBatchUtility() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 5, config); assertEq(tokenIds.length, 5); for (uint256 i = 0; i < 5; i++) { assertEq(coupon.ownerOf(tokenIds[i]), issuer); assertEq(coupon.getFaceValue(tokenIds[i]), 100e6); } } function test_MintBatchSecurity() public { vm.prank(minter); ICoupon.CouponConfig memory config = _securityConfig(); uint256[] memory tokenIds = factory.mintBatch(issuer, 1000e6, 3, config); assertEq(tokenIds.length, 3); } function test_UtilityMaxPriceExceedsFaceValue() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); config.maxPrice = 200e6; // 超过面值 100e6 vm.expectRevert("Utility: maxPrice <= faceValue"); factory.mintBatch(issuer, 100e6, 1, config); } function test_UtilityMaxExpiry12Months() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); config.expiryDate = block.timestamp + 400 days; vm.expectRevert("Utility: max 12 months"); factory.mintBatch(issuer, 100e6, 1, config); } function test_SecurityRequiresExpiry() public { vm.prank(minter); ICoupon.CouponConfig memory config = _securityConfig(); config.expiryDate = 0; vm.expectRevert("Security: expiry required"); factory.mintBatch(issuer, 1000e6, 1, config); } function test_SecurityRequiresKycL3() public { address lowKycIssuer = address(10); vm.prank(admin); compliance.setKycLevel(lowKycIssuer, 1); vm.prank(minter); ICoupon.CouponConfig memory config = _securityConfig(); vm.expectRevert("Compliance: insufficient KYC level"); factory.mintBatch(lowKycIssuer, 1000e6, 1, config); } function test_OnlyMinterCanMint() public { vm.prank(issuer); ICoupon.CouponConfig memory config = _utilityConfig(); vm.expectRevert(); factory.mintBatch(issuer, 100e6, 1, config); } function test_ZeroQuantityReverts() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); vm.expectRevert("CouponFactory: invalid quantity"); factory.mintBatch(issuer, 100e6, 0, config); } function test_ExcessiveQuantityReverts() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); vm.expectRevert("CouponFactory: invalid quantity"); factory.mintBatch(issuer, 100e6, 10001, config); } function test_BatchInfoRecorded() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); factory.mintBatch(issuer, 100e6, 3, config); CouponFactory.BatchInfo memory batch = factory.getBatchInfo(0); assertEq(batch.issuer, issuer); assertEq(batch.faceValue, 100e6); assertEq(batch.quantity, 3); assertEq(batch.tokenIds.length, 3); } function testFuzz_MintBatchQuantity(uint256 quantity) public { quantity = bound(quantity, 1, 100); vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, quantity, config); assertEq(tokenIds.length, quantity); } // --- Helpers --- function _utilityConfig() internal view returns (ICoupon.CouponConfig memory) { bytes32[] memory stores = new bytes32[](0); return ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: true, maxResaleCount: 3, maxPrice: 100e6, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: issuer, faceValue: 100e6 }); } function _securityConfig() internal view returns (ICoupon.CouponConfig memory) { bytes32[] memory stores = new bytes32[](0); return ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Security, transferable: true, maxResaleCount: 10, maxPrice: 0, expiryDate: block.timestamp + 365 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: issuer, faceValue: 1000e6 }); } }