diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index 101b251..7fb4054 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -22,6 +22,11 @@ contract IdentityToken is ERC721, IIdentityToken { // tokenId => attribute keyHash => attribute value mapping(uint256 => mapping(bytes32 => bytes)) public attributes; + // required attribute key hashes + bytes32 private constant NAME_KEY = keccak256(abi.encodePacked("name")); + bytes32 private constant EMAIL_KEY = keccak256(abi.encodePacked("email")); + bytes32 private constant PHONE_KEY = keccak256(abi.encodePacked("phone")); + // tokenId => array of Endorsements mapping(uint256 => DataTypes.Endorsement[]) public endorsements; @@ -76,6 +81,25 @@ contract IdentityToken is ERC721, IIdentityToken { return tokenId; } + /** + * @dev Adds validation to ensure required identity fields are present. + * Name is mandatory and at least one contact method (email or phone) + * must be provided. + */ + function _validateRequiredFields(uint256 tokenId) internal view { + bytes storage name = attributes[tokenId][NAME_KEY]; + bytes storage email = attributes[tokenId][EMAIL_KEY]; + bytes storage phone = attributes[tokenId][PHONE_KEY]; + + // Name is mandatory + if (name.length == 0) revert Errors.MissingName(); + + // At least one contact method required + if (email.length == 0 && phone.length == 0) { + revert Errors.MissingContact(); + } + } + /** * @dev Sets a metadata attribute (e.g., name, social link) for an identity. */ @@ -84,7 +108,14 @@ contract IdentityToken is ERC721, IIdentityToken { string calldata key, bytes calldata value ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { + bytes32 keyHash = keccak256(abi.encodePacked(key)); + _setAttribute(tokenId, key, value); + + // skip validation if the updated attribute is one of the required fields, since + if (keyHash != NAME_KEY && keyHash != EMAIL_KEY && keyHash != PHONE_KEY) { + _validateRequiredFields(tokenId); + } } /** @@ -108,9 +139,13 @@ contract IdentityToken is ERC721, IIdentityToken { ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { if (keys.length != values.length) revert Errors.ArrayLengthMismatch(); + uint8 shouldValidate = 0; + for (uint256 i = 0; i < keys.length; i++) { _setAttribute(tokenId, keys[i], values[i]); } + + _validateRequiredFields(tokenId); } /** @@ -120,6 +155,27 @@ contract IdentityToken is ERC721, IIdentityToken { _setAttribute(tokenId, "name", bytes(name)); } + /** + * @dev Convenience setter for the "email" / "phone" attribute. + */ + function setContact( + uint256 tokenId, + string calldata email, + string calldata phone + ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { + if (bytes(email).length == 0 && bytes(phone).length == 0) { + revert Errors.MissingContact(); + } + + if (bytes(email).length != 0) { + _setAttribute(tokenId, "email", bytes(email)); + } + + if (bytes(phone).length != 0) { + _setAttribute(tokenId, "phone", bytes(phone)); + } + } + /** * @dev Convenience setter for the "github" attribute. */ @@ -128,6 +184,18 @@ contract IdentityToken is ERC721, IIdentityToken { string calldata github ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { _setAttribute(tokenId, "github", bytes(github)); + _validateRequiredFields(tokenId); + } + + function deleteAttribute( + uint256 tokenId, + string calldata key + ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { + bytes32 keyHash = keccak256(abi.encodePacked(key)); + + delete attributes[tokenId][keyHash]; + + emit Events.AttributeDeleted(tokenId, keyHash); } /** @@ -184,4 +252,56 @@ contract IdentityToken is ERC721, IIdentityToken { emit Events.AttributeSet(tokenId, keyHash, key, value); } + + // Identity helpers + + /// @notice Returns true if the address owns any identity token. + function hasIdentity(address owner) external view returns (bool) { + return balanceOf(owner) > 0; + } + + /// @notice Returns full metadata for a given token. + function getIdentity(uint256 tokenId) external view returns (DataTypes.Identity memory) { + address owner = _requireOwned(tokenId); + DataTypes.IdentityState storage state = identityStates[tokenId]; + return + DataTypes.Identity({ + tokenId: tokenId, + owner: owner, + isCompromised: state.isCompromised, + backupWallet: state.backupWallet, + pendingBackupWallet: state.pendingBackupWallet, + backupUnlockTime: state.backupUnlockTime, + validUntil: state.validUntil, + endorsementCount: endorsements[tokenId].length + }); + } + + /// @notice Returns all token IDs owned by an address (0 or 1 given soulbound constraint). + function getIdentityByOwner(address owner) external view returns (uint256[] memory) { + uint256 tokenId = ownerToTokenId[owner]; + if (tokenId == 0) { + return new uint256[](0); + } + uint256[] memory result = new uint256[](1); + result[0] = tokenId; + return result; + } + + /// @notice Returns true if the token has at least one active (non-revoked, non-expired) endorsement. + function isVerified(uint256 tokenId) external view returns (bool) { + DataTypes.Endorsement[] storage list = endorsements[tokenId]; + for (uint256 i = 0; i < list.length; i++) { + DataTypes.Endorsement storage e = list[i]; + bool active = e.revokedAt == 0 && (e.validUntil == 0 || e.validUntil >= block.timestamp); + if (active) return true; + } + return false; + } + + /// @notice Returns true if the token's validUntil is set and has passed. + function isExpired(uint256 tokenId) external view returns (bool) { + uint256 validUntil = identityStates[tokenId].validUntil; + return validUntil != 0 && block.timestamp > validUntil; + } } diff --git a/src/interfaces/IIdentityToken.sol b/src/interfaces/IIdentityToken.sol index 6ca163a..33e0ca8 100644 --- a/src/interfaces/IIdentityToken.sol +++ b/src/interfaces/IIdentityToken.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { DataTypes } from "../libraries/DataTypes.sol"; interface IIdentityToken is IERC721, IERC721Metadata { // ------------------------------------------------------------------------- @@ -10,6 +11,7 @@ interface IIdentityToken is IERC721, IERC721Metadata { // ------------------------------------------------------------------------- function setAttribute(uint256 tokenId, string calldata key, bytes calldata value) external; + function deleteAttribute(uint256 tokenId, string calldata key) external; function getAttribute(uint256 tokenId, string calldata key) external view returns (bytes memory); @@ -36,7 +38,13 @@ interface IIdentityToken is IERC721, IERC721Metadata { ) external view - returns (bool isCompromised, address backupWallet, address pendingBackupWallet, uint256 backupUnlockTime); + returns ( + bool isCompromised, + address backupWallet, + address pendingBackupWallet, + uint256 backupUnlockTime, + uint256 validUntil + ); function attributes(uint256 tokenId, bytes32 keyHash) external view returns (bytes memory); @@ -53,4 +61,16 @@ interface IIdentityToken is IERC721, IERC721Metadata { uint256 validUntil, uint256 revokedAt ); + + // Identity helpers + + function hasIdentity(address owner) external view returns (bool); + + function getIdentity(uint256 tokenId) external view returns (DataTypes.Identity memory); + + function getIdentityByOwner(address owner) external view returns (uint256[] memory); + + function isVerified(uint256 tokenId) external view returns (bool); + + function isExpired(uint256 tokenId) external view returns (bool); } diff --git a/src/libraries/DataTypes.sol b/src/libraries/DataTypes.sol index c228658..6211cde 100644 --- a/src/libraries/DataTypes.sol +++ b/src/libraries/DataTypes.sol @@ -15,5 +15,17 @@ library DataTypes { address backupWallet; address pendingBackupWallet; uint256 backupUnlockTime; + uint256 validUntil; + } + + struct Identity { + uint256 tokenId; + address owner; + bool isCompromised; + address backupWallet; + address pendingBackupWallet; + uint256 backupUnlockTime; + uint256 validUntil; + uint256 endorsementCount; } } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 210c8cd..99e95d2 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -16,4 +16,6 @@ library Errors { error AlreadyHasIdentity(); error DuplicateEndorsement(); error ArrayLengthMismatch(); + error MissingName(); + error MissingContact(); } diff --git a/src/libraries/Events.sol b/src/libraries/Events.sol index a975a96..e07e6e0 100644 --- a/src/libraries/Events.sol +++ b/src/libraries/Events.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; library Events { + event AttributeDeleted(uint256 indexed tokenId, bytes32 indexed keyHash); event AttributeSet(uint256 indexed tokenId, bytes32 indexed keyHash, string key, bytes value); event EndorsementGiven(uint256 indexed fromId, uint256 indexed toId, bytes32 typeHash, uint256 expiry); event EndorsementRevoked(uint256 indexed fromId, uint256 indexed toId, uint256 index); diff --git a/src/libraries/Schema.sol b/src/libraries/Schema.sol index e17a33a..05f44b5 100644 --- a/src/libraries/Schema.sol +++ b/src/libraries/Schema.sol @@ -18,4 +18,6 @@ library Schema { bytes32 internal constant GITHUB = keccak256(abi.encodePacked("github")); bytes32 internal constant LINKEDIN = keccak256(abi.encodePacked("linkedin")); bytes32 internal constant TWITTER = keccak256(abi.encodePacked("twitter")); + bytes32 internal constant PHONE = keccak256(abi.encodePacked("phone")); + bytes32 internal constant EMAIL = keccak256(abi.encodePacked("email")); } diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 8c27803..adc0b44 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -5,9 +5,12 @@ import { Test } from "forge-std/Test.sol"; import { IdentityToken } from "../src/IdentityToken.sol"; import { DataTypes } from "../src/libraries/DataTypes.sol"; import { Errors } from "../src/libraries/Errors.sol"; +import { Events } from "../src/libraries/Events.sol"; +import { StdStorage, stdStorage } from "forge-std/Test.sol"; import { Schema } from "../src/libraries/Schema.sol"; contract IdentityTokenTest is Test { + using stdStorage for StdStorage; IdentityToken public identityToken; address public alice = address(0x1); address public bob = address(0x2); @@ -21,7 +24,7 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_Mint() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); assertEq(tokenId, 1); @@ -34,34 +37,41 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_SetAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); + // Set name first, then email to satisfy validation identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); bytes32 keyHash = keccak256(abi.encodePacked("name")); bytes memory retrievedValue = identityToken.attributes(tokenId, keyHash); + assertEq(string(identityToken.getAttribute(tokenId, "github")), "https://github.com/alice"); assertEq(string(retrievedValue), "Alice Nakamoto"); } function test_GetAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } function test_GetAttribute_MatchesRawMapping() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); + // Set required fields first + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); + + identityToken.setGithub(tokenId, "https://github.com/alice"); assertEq( string(identityToken.getAttribute(tokenId, "github")), @@ -70,14 +80,14 @@ contract IdentityTokenTest is Test { } function test_SetAttribute_SocialLinks() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); + identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); - vm.prank(alice); identityToken.setAttribute(tokenId, "linkedin", bytes("https://linkedin.com/in/alice")); - vm.prank(alice); identityToken.setAttribute(tokenId, "twitter", bytes("https://twitter.com/alice")); assertEq(string(identityToken.getAttribute(tokenId, "github")), "https://github.com/alice"); @@ -86,33 +96,35 @@ contract IdentityTokenTest is Test { } function test_OverwriteAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice")); - vm.prank(alice); + identityToken.setName(tokenId, "Alice"); + identityToken.setContact(tokenId, "alice@example.com", ""); identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } function test_SetAttribute_EmptyValue() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("")); - - assertEq(identityToken.getAttribute(tokenId, "name").length, 0); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); + identityToken.setAttribute(tokenId, "github", bytes("")); + assertEq(identityToken.getAttribute(tokenId, "github").length, 0); } function test_SetAttribute_LongURL() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); + // Required fields first + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); + string memory url = "https://www.linkedin.com/in/alice-nakamoto-very-long-profile-url-example-1234567890"; - vm.prank(alice); identityToken.setAttribute(tokenId, "linkedin", bytes(url)); assertEq(string(identityToken.getAttribute(tokenId, "linkedin")), url); @@ -123,20 +135,20 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_SetName() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); identityToken.setName(tokenId, "Alice Nakamoto"); assertEq(string(identityToken.attributes(tokenId, Schema.NAME)), "Alice Nakamoto"); } function test_SetGithub() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); identityToken.setGithub(tokenId, "https://github.com/alice"); assertEq(string(identityToken.attributes(tokenId, Schema.GITHUB)), "https://github.com/alice"); @@ -147,44 +159,47 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_SetAttributesBatch() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - string[] memory keys = new string[](4); + string[] memory keys = new string[](5); keys[0] = "name"; keys[1] = "github"; keys[2] = "nationality"; keys[3] = "residence"; + keys[4] = "email"; - bytes[] memory values = new bytes[](4); + bytes[] memory values = new bytes[](5); values[0] = bytes("Alice Nakamoto"); values[1] = bytes("https://github.com/alice"); values[2] = bytes("Japanese"); values[3] = bytes("Tokyo"); + values[4] = bytes("alice@example.com"); - vm.prank(alice); identityToken.setAttributesBatch(tokenId, keys, values); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); assertEq(string(identityToken.getAttribute(tokenId, "github")), "https://github.com/alice"); assertEq(string(identityToken.getAttribute(tokenId, "nationality")), "Japanese"); assertEq(string(identityToken.getAttribute(tokenId, "residence")), "Tokyo"); + assertEq(string(identityToken.getAttribute(tokenId, "email")), "alice@example.com"); } function test_SetAttributesBatch_SingleEntry() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); + identityToken.setContact(tokenId, "alice@example.com", ""); + string[] memory keys = new string[](1); - keys[0] = "age"; + keys[0] = "name"; bytes[] memory values = new bytes[](1); - values[0] = bytes("30"); + values[0] = bytes("Alice Nakamoto"); - vm.prank(alice); identityToken.setAttributesBatch(tokenId, keys, values); - assertEq(string(identityToken.getAttribute(tokenId, "age")), "30"); + assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } // ------------------------------------------------------------------------- @@ -224,7 +239,7 @@ contract IdentityTokenTest is Test { } function test_RevertIf_BatchLengthMismatch() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); string[] memory keys = new string[](2); @@ -233,7 +248,6 @@ contract IdentityTokenTest is Test { bytes[] memory values = new bytes[](1); values[0] = bytes("Alice Nakamoto"); - vm.prank(alice); vm.expectRevert(Errors.ArrayLengthMismatch.selector); identityToken.setAttributesBatch(tokenId, keys, values); } @@ -250,6 +264,8 @@ contract IdentityTokenTest is Test { assertEq(Schema.GITHUB, keccak256(abi.encodePacked("github"))); assertEq(Schema.LINKEDIN, keccak256(abi.encodePacked("linkedin"))); assertEq(Schema.TWITTER, keccak256(abi.encodePacked("twitter"))); + assertEq(Schema.PHONE, keccak256(abi.encodePacked("phone"))); + assertEq(Schema.EMAIL, keccak256(abi.encodePacked("email"))); } // ------------------------------------------------------------------------- @@ -282,4 +298,235 @@ contract IdentityTokenTest is Test { assertEq(storedValidUntil, validUntil); assertEq(revokedAt, 0); } + + // --- deleteAttribute --- + + function test_DeleteAttribute() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + identityToken.setContact(tokenId, "alice@example.com", ""); + + identityToken.deleteAttribute(tokenId, "email"); + + bytes32 keyHash = keccak256(abi.encodePacked("email")); + bytes memory value = identityToken.attributes(tokenId, keyHash); + + assertEq(value.length, 0); + } + + function test_DeleteAttribute_EmitsEvent() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + identityToken.setContact(tokenId, "alice@example.com", ""); + + bytes32 keyHash = keccak256(abi.encodePacked("email")); + + vm.expectEmit(true, true, false, false); + emit Events.AttributeDeleted(tokenId, keyHash); + identityToken.deleteAttribute(tokenId, "email"); + } + + function test_RevertIf_NotOwnerDeletesAttribute() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + vm.startPrank(alice); + identityToken.setContact(tokenId, "alice@example.com", ""); + vm.stopPrank(); + + vm.startPrank(bob); + vm.expectRevert(Errors.NotTokenOwner.selector); + identityToken.deleteAttribute(tokenId, "email"); + } + + function test_DeleteAttribute_NeverSet_DoesNotRevert() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + identityToken.deleteAttribute(tokenId, "nonexistent"); + + bytes32 keyHash = keccak256(abi.encodePacked("nonexistent")); + bytes memory value = identityToken.attributes(tokenId, keyHash); + + assertEq(value.length, 0); + } + + function test_DeleteAttribute_Twice_DoesNotRevert() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + identityToken.setContact(tokenId, "alice@example.com", ""); + + identityToken.deleteAttribute(tokenId, "email"); + + identityToken.deleteAttribute(tokenId, "email"); + } + + function test_DeleteAttribute_ThenReSet() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + identityToken.setContact(tokenId, "alice@example.com", ""); + + identityToken.deleteAttribute(tokenId, "email"); + + identityToken.setContact(tokenId, "new@example.com", ""); + + bytes32 keyHash = keccak256(abi.encodePacked("email")); + bytes memory value = identityToken.attributes(tokenId, keyHash); + + assertEq(string(value), "new@example.com"); + } + + function test_RevertIf_CompromisedIdentityDeletesAttribute() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + // Use stdStorage to set isCompromised without hardcoding a slot + stdstore.target(address(identityToken)).sig("identityStates(uint256)").with_key(tokenId).depth(0).checked_write( + true + ); // isCompromised is the first field in IdentityState + + vm.expectRevert(Errors.IdentityCompromised.selector); + identityToken.deleteAttribute(tokenId, "email"); + } + // --- hasIdentity --- + + function test_HasIdentity_True() public { + vm.startPrank(alice); + identityToken.mint(); + assertTrue(identityToken.hasIdentity(alice)); + } + + function test_HasIdentity_False() public view { + assertFalse(identityToken.hasIdentity(alice)); + } + + // --- getIdentity --- + + function test_GetIdentity_ReturnsCorrectFields() public { + vm.startPrank(alice); + uint256 tokenId = identityToken.mint(); + + DataTypes.Identity memory identity = identityToken.getIdentity(tokenId); + + assertEq(identity.tokenId, tokenId); + assertEq(identity.owner, alice); + assertFalse(identity.isCompromised); + assertEq(identity.validUntil, 0); + assertEq(identity.endorsementCount, 0); + } + + function test_GetIdentity_EndorsementCountUpdates() public { + vm.prank(alice); + uint256 aliceId = identityToken.mint(); + + vm.prank(bob); + uint256 bobId = identityToken.mint(); + + bytes32 connectionType = keccak256(abi.encodePacked("Colleague")); + vm.prank(alice); + identityToken.endorse(aliceId, bobId, connectionType, 0); + + DataTypes.Identity memory identity = identityToken.getIdentity(bobId); + assertEq(identity.endorsementCount, 1); + } + + function test_GetIdentity_RevertsForNonexistentToken() public { + vm.expectRevert(); + identityToken.getIdentity(999); + } + + // --- getIdentityByOwner --- + + function test_GetIdentityByOwner_ReturnsTokenId() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + uint256[] memory result = identityToken.getIdentityByOwner(alice); + + assertEq(result.length, 1); + assertEq(result[0], tokenId); + } + + function test_GetIdentityByOwner_ReturnsEmptyIfNoToken() public view { + uint256[] memory result = identityToken.getIdentityByOwner(alice); + assertEq(result.length, 0); + } + + // --- isVerified --- + + function test_IsVerified_FalseWithNoEndorsements() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + assertFalse(identityToken.isVerified(tokenId)); + } + + function test_IsVerified_TrueWithActiveEndorsement() public { + vm.prank(alice); + uint256 aliceId = identityToken.mint(); + + vm.prank(bob); + uint256 bobId = identityToken.mint(); + + bytes32 connectionType = keccak256(abi.encodePacked("Colleague")); + vm.prank(alice); + identityToken.endorse(aliceId, bobId, connectionType, 0); + + assertTrue(identityToken.isVerified(bobId)); + } + + function test_IsVerified_FalseWithExpiredEndorsement() public { + vm.prank(alice); + uint256 aliceId = identityToken.mint(); + + vm.prank(bob); + uint256 bobId = identityToken.mint(); + + bytes32 connectionType = keccak256(abi.encodePacked("Colleague")); + uint256 validUntil = block.timestamp + 1 days; + + vm.prank(alice); + identityToken.endorse(aliceId, bobId, connectionType, validUntil); + + vm.warp(block.timestamp + 2 days); + + assertFalse(identityToken.isVerified(bobId)); + } + + // --- isExpired --- + + function test_IsExpired_FalseWhenNoValidUntil() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + assertFalse(identityToken.isExpired(tokenId)); + } + + function test_IsExpired_FalseBeforeExpiry() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + stdstore.target(address(identityToken)).sig("identityStates(uint256)").with_key(tokenId).depth(4).checked_write( + block.timestamp + 1 days + ); + + assertFalse(identityToken.isExpired(tokenId)); + } + + function test_IsExpired_TrueAfterExpiry() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + uint256 expiry = block.timestamp + 1 days; + + stdstore.target(address(identityToken)).sig("identityStates(uint256)").with_key(tokenId).depth(4).checked_write( + expiry + ); + + vm.warp(expiry + 1); + + assertTrue(identityToken.isExpired(tokenId)); + } }