From 448e984995dfff6e6cb25ac38f1a133aceca3ce0 Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 24 Jun 2020 11:24:19 +0100 Subject: [PATCH] Fix some minor group-related bugs Incorrect column names when saving a group ban. Missing column in LeaveGroupTransactions. More stringent validity checks in group-kick, group-ban and remove-group-admin. Added loads more tests to cover group actions. --- .../hsqldb/HSQLDBDatabaseUpdates.java | 7 +- .../hsqldb/HSQLDBGroupRepository.java | 2 +- .../transaction/GroupBanTransaction.java | 6 +- .../transaction/GroupKickTransaction.java | 6 +- .../RemoveGroupAdminTransaction.java | 4 + .../org/qortal/test/AccountRefCacheTests.java | 4 +- .../org/qortal/test/TransferPrivsTests.java | 2 +- .../qortal/test/common/TransactionUtils.java | 33 +- .../org/qortal/test/group/AdminTests.java | 352 ++++++++++++++++++ .../java/org/qortal/test/group/MiscTests.java | 191 +++++++++- .../org/qortal/test/group/OwnerTests.java | 187 ++++++++++ .../test/minting/DisagreementTests.java | 2 +- 12 files changed, 769 insertions(+), 27 deletions(-) create mode 100644 src/test/java/org/qortal/test/group/AdminTests.java create mode 100644 src/test/java/org/qortal/test/group/OwnerTests.java diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index dfa0c066..0e265b2c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -530,7 +530,7 @@ public class HSQLDBDatabaseUpdates { // Leave group stmt.execute("CREATE TABLE LeaveGroupTransactions (signature Signature, leaver QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "member_reference Signature, admin_reference Signature, " + TRANSACTION_KEYS + ")"); + + "member_reference Signature, admin_reference Signature, previous_group_id GroupID, " + TRANSACTION_KEYS + ")"); // Kick from group stmt.execute("CREATE TABLE GroupKickTransactions (signature Signature, admin QortalPublicKey NOT NULL, " @@ -618,6 +618,11 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")"); break; + case 20: + // XXX Bug-fix, but remove when we build new genesis + stmt.execute("ALTER TABLE LeaveGroupTransactions ADD COLUMN IF NOT EXISTS previous_group_id GroupID"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java index 60984ee0..91db22f1 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java @@ -828,7 +828,7 @@ public class HSQLDBGroupRepository implements GroupRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupBans"); saveHelper.bind("group_id", groupBanData.getGroupId()).bind("offender", groupBanData.getOffender()).bind("admin", groupBanData.getAdmin()) - .bind("banned", groupBanData.getBanned()).bind("reason", groupBanData.getReason()).bind("expiry", groupBanData.getExpiry()) + .bind("banned_when", groupBanData.getBanned()).bind("reason", groupBanData.getReason()).bind("expires_when", groupBanData.getExpiry()) .bind("reference", groupBanData.getReference()); try { diff --git a/src/main/java/org/qortal/transaction/GroupBanTransaction.java b/src/main/java/org/qortal/transaction/GroupBanTransaction.java index 78e9c3d0..d3458ebe 100644 --- a/src/main/java/org/qortal/transaction/GroupBanTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupBanTransaction.java @@ -72,7 +72,11 @@ public class GroupBanTransaction extends Transaction { Account offender = getOffender(); - // Can't ban another admin unless the group owner + // Can't ban group owner + if (offender.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + + // Can't ban another admin unless admin is the group owner if (!admin.getAddress().equals(groupData.getOwner()) && this.repository.getGroupRepository().adminExists(groupId, offender.getAddress())) return ValidationResult.INVALID_GROUP_OWNER; diff --git a/src/main/java/org/qortal/transaction/GroupKickTransaction.java b/src/main/java/org/qortal/transaction/GroupKickTransaction.java index 86c9aaef..d9be8161 100644 --- a/src/main/java/org/qortal/transaction/GroupKickTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupKickTransaction.java @@ -74,7 +74,11 @@ public class GroupKickTransaction extends Transaction { if (!groupRepository.joinRequestExists(groupId, member.getAddress()) && !groupRepository.memberExists(groupId, member.getAddress())) return ValidationResult.NOT_GROUP_MEMBER; - // Can't kick another admin unless the group owner + // Can't kick group owner + if (member.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + + // Can't kick another admin unless kicker is the group owner if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupId, member.getAddress())) return ValidationResult.INVALID_GROUP_OWNER; diff --git a/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java b/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java index b8cb5b29..43f1fc8f 100644 --- a/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java +++ b/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java @@ -76,6 +76,10 @@ public class RemoveGroupAdminTransaction extends Transaction { if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress())) return ValidationResult.NOT_GROUP_ADMIN; + // Check admin is not group owner + if (admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + // Check creator has enough funds if (owner.getConfirmedBalance(Asset.QORT) < this.removeGroupAdminTransactionData.getFee()) return ValidationResult.NO_BALANCE; diff --git a/src/test/java/org/qortal/test/AccountRefCacheTests.java b/src/test/java/org/qortal/test/AccountRefCacheTests.java index 22355a90..c7305dd9 100644 --- a/src/test/java/org/qortal/test/AccountRefCacheTests.java +++ b/src/test/java/org/qortal/test/AccountRefCacheTests.java @@ -257,11 +257,11 @@ public class AccountRefCacheTests extends Common { // generate new payment from Alice to new account TransactionData paymentData1 = new PaymentTransactionData(TestTransaction.generateBase(alice), newbie.getAddress(), amount); - TransactionUtils.signAsUnconfirmed(repository, paymentData1, alice); // updates paymentData1's signature + TransactionUtils.signAndImportValid(repository, paymentData1, alice); // updates paymentData1's signature // generate another payment from Alice to new account TransactionData paymentData2 = new PaymentTransactionData(TestTransaction.generateBase(alice), newbie.getAddress(), amount); - TransactionUtils.signAsUnconfirmed(repository, paymentData2, alice); // updates paymentData2's signature + TransactionUtils.signAndImportValid(repository, paymentData2, alice); // updates paymentData2's signature // mint block containing payments (uses cache) BlockUtils.mintBlock(repository); diff --git a/src/test/java/org/qortal/test/TransferPrivsTests.java b/src/test/java/org/qortal/test/TransferPrivsTests.java index f2779caf..f95e0599 100644 --- a/src/test/java/org/qortal/test/TransferPrivsTests.java +++ b/src/test/java/org/qortal/test/TransferPrivsTests.java @@ -313,7 +313,7 @@ public class TransferPrivsTests extends Common { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderAccount.getPublicKey(), fee, null); TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, recipientAccount.getAddress()); - TransactionUtils.signAsUnconfirmed(repository, transactionData, senderAccount); + TransactionUtils.signAndImportValid(repository, transactionData, senderAccount); BlockMinter.mintTestingBlock(repository, mintingAccount); } diff --git a/src/test/java/org/qortal/test/common/TransactionUtils.java b/src/test/java/org/qortal/test/common/TransactionUtils.java index 3b795e4a..4779aa3b 100644 --- a/src/test/java/org/qortal/test/common/TransactionUtils.java +++ b/src/test/java/org/qortal/test/common/TransactionUtils.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertTrue; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.List; import org.qortal.account.PrivateKeyAccount; import org.qortal.data.transaction.TransactionData; @@ -17,8 +18,25 @@ import org.qortal.transaction.Transaction.ValidationResult; public class TransactionUtils { - /** Signs transaction using given account and imports into unconfirmed pile. */ - public static void signAsUnconfirmed(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { + /** Signs transaction using given account and attempts to import into unconfirmed pile, returning validation result. */ + public static ValidationResult signAndImport(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(signingAccount); + + // Add to unconfirmed + assertTrue("Transaction's signature should be valid", transaction.isSignatureValid()); + + // We might need to wait until transaction's timestamp is valid for the block we're about to mint + try { + Thread.sleep(1L); + } catch (InterruptedException e) { + } + + return transaction.importAsUnconfirmed(); + } + + /** Signs transaction using given account and imports into unconfirmed pile, checking transaction is valid. */ + public static void signAndImportValid(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { Transaction transaction = Transaction.fromData(repository, transactionData); transaction.sign(signingAccount); @@ -37,7 +55,7 @@ public class TransactionUtils { /** Signs transaction using given account and mints a new block.
See {@link BlockUtils#mintBlock(Repository)} */ public static void signAndMint(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { - signAsUnconfirmed(repository, transactionData, signingAccount); + signAndImportValid(repository, transactionData, signingAccount); // Mint block BlockUtils.mintBlock(repository); @@ -58,4 +76,13 @@ public class TransactionUtils { } } + public static void deleteUnconfirmedTransactions(Repository repository) throws DataException { + List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(); + + for (TransactionData transactionData : unconfirmedTransactions) + repository.getTransactionRepository().delete(transactionData); + + repository.saveChanges(); + } + } diff --git a/src/test/java/org/qortal/test/group/AdminTests.java b/src/test/java/org/qortal/test/group/AdminTests.java new file mode 100644 index 00000000..a39b23d7 --- /dev/null +++ b/src/test/java/org/qortal/test/group/AdminTests.java @@ -0,0 +1,352 @@ +package org.qortal.test.group; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.AddGroupAdminTransactionData; +import org.qortal.data.transaction.CancelGroupBanTransactionData; +import org.qortal.data.transaction.CreateGroupTransactionData; +import org.qortal.data.transaction.GroupBanTransactionData; +import org.qortal.data.transaction.GroupKickTransactionData; +import org.qortal.data.transaction.JoinGroupTransactionData; +import org.qortal.group.Group.ApprovalThreshold; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction.ValidationResult; + +public class AdminTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + @Test + public void testGroupKickMember() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Attempt to kick Bob + ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Attempt to kick Bob + result = groupKick(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + } + } + + @Test + public void testGroupKickAdmin() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Promote Bob to admin + addGroupAdmin(repository, alice, groupId, bob.getAddress()); + + // Confirm Bob is now admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Attempt to kick Bob + ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob now an admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Have Alice (owner) try to kick herself! + result = groupKick(repository, alice, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Have Bob try to kick Alice (owner) + result = groupKick(repository, bob, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + } + } + + @Test + public void testGroupBanMember() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Attempt to cancel non-existent Bob ban + ValidationResult result = cancelGroupBan(repository, alice, groupId, bob.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Attempt to ban Bob + result = groupBan(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Orphan last block (Bob ban) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed group-ban transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Bob to join + result = joinGroup(repository, bob, groupId); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Attempt to ban Bob + result = groupBan(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Cancel Bob's ban + result = cancelGroupBan(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Orphan last block (Bob join) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed join-group transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Orphan last block (Cancel Bob ban) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed cancel-ban transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Orphan last block (Bob ban) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed group-ban transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + } + } + + @Test + public void testGroupBanAdmin() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Bob to join + ValidationResult result = joinGroup(repository, bob, groupId); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Promote Bob to admin + addGroupAdmin(repository, alice, groupId, bob.getAddress()); + + // Confirm Bob is now admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Attempt to ban Bob + result = groupBan(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Cancel Bob's ban + result = cancelGroupBan(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Orphan last block (Bob join) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed join-group transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Orphan last block (Cancel Bob ban) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed cancel-ban transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Bob attempts to rejoin + result = joinGroup(repository, bob, groupId); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Orphan last block (Bob ban) + BlockUtils.orphanLastBlock(repository); + // Delete unconfirmed group-ban transaction + TransactionUtils.deleteUnconfirmedTransactions(repository); + + // Confirm Bob is now admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Have Alice (owner) try to ban herself! + result = groupBan(repository, alice, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Have Bob try to ban Alice (owner) + result = groupBan(repository, bob, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + } + } + + private Integer createGroup(Repository repository, PrivateKeyAccount owner, String groupName, boolean isOpen) throws DataException { + String description = groupName + " (description)"; + + ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE; + int minimumBlockDelay = 10; + int maximumBlockDelay = 1440; + + CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(owner), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + TransactionUtils.signAndMint(repository, transactionData, owner); + + return repository.getGroupRepository().fromGroupName(groupName).getGroupId(); + } + + private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException { + JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException { + GroupKickTransactionData transactionData = new GroupKickTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing"); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException { + GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException { + CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private void addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException { + AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member); + TransactionUtils.signAndMint(repository, transactionData, owner); + } + + private boolean isMember(Repository repository, String address, int groupId) throws DataException { + return repository.getGroupRepository().memberExists(groupId, address); + } + + private boolean isAdmin(Repository repository, String address, int groupId) throws DataException { + return repository.getGroupRepository().adminExists(groupId, address); + } + +} diff --git a/src/test/java/org/qortal/test/group/MiscTests.java b/src/test/java/org/qortal/test/group/MiscTests.java index 95808d14..481f0b6d 100644 --- a/src/test/java/org/qortal/test/group/MiscTests.java +++ b/src/test/java/org/qortal/test/group/MiscTests.java @@ -1,20 +1,23 @@ package org.qortal.test.group; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.data.transaction.CreateGroupTransactionData; +import org.qortal.data.transaction.GroupInviteTransactionData; +import org.qortal.data.transaction.JoinGroupTransactionData; +import org.qortal.data.transaction.LeaveGroupTransactionData; import org.qortal.group.Group.ApprovalThreshold; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.BlockUtils; import org.qortal.test.common.Common; import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.transaction.TestTransaction; -import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.ValidationResult; public class MiscTests extends Common { @@ -32,28 +35,184 @@ public class MiscTests extends Common { @Test public void testCreateGroupWithExistingName() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String groupName = "test-group"; - String description = "test group"; - final boolean isOpen = false; - ApprovalThreshold approvalThreshold = ApprovalThreshold.PCT40; - int minimumBlockDelay = 10; - int maximumBlockDelay = 1440; - - CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(alice), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); - TransactionUtils.signAndMint(repository, transactionData, alice); + // Create group + createGroup(repository, alice, "test-group", false); // duplicate String duplicateGroupName = "TEST-gr0up"; - transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(alice), duplicateGroupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); - Transaction transaction = Transaction.fromData(repository, transactionData); - transaction.sign(alice); + String description = duplicateGroupName + " (description)"; - ValidationResult result = transaction.importAsUnconfirmed(); + boolean isOpen = false; + ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE; + int minimumBlockDelay = 10; + int maximumBlockDelay = 1440; + + CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(alice), duplicateGroupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, alice); assertTrue("Transaction should be invalid", ValidationResult.OK != result); } } + @Test + public void testJoinOpenGroup() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + } + } + + @Test + public void testJoinClosedGroup() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "closed-group", false); + + // Confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Confirm Bob still not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Have Alice 'invite' Bob to confirm membership + groupInvite(repository, alice, groupId, bob.getAddress(), 0); // non-expiring invite + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + } + } + + @Test + public void testJoinGroupViaInvite() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "closed-group", false); + + // Confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Have Alice 'invite' Bob to join + groupInvite(repository, alice, groupId, bob.getAddress(), 0); // non-expiring invite + + // Confirm Bob still not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Bob uses invite to join + joinGroup(repository, bob, groupId); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + } + } + + @Test + public void testLeaveGroup() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Bob leaves + leaveGroup(repository, bob, groupId); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob now a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob no longer a member + assertFalse(isMember(repository, bob.getAddress(), groupId)); + } + } + + private Integer createGroup(Repository repository, PrivateKeyAccount owner, String groupName, boolean isOpen) throws DataException { + String description = groupName + " (description)"; + + ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE; + int minimumBlockDelay = 10; + int maximumBlockDelay = 1440; + + CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(owner), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + TransactionUtils.signAndMint(repository, transactionData, owner); + + return repository.getGroupRepository().fromGroupName(groupName).getGroupId(); + } + + private void joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException { + JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId); + TransactionUtils.signAndMint(repository, transactionData, joiner); + } + + private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { + GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive); + TransactionUtils.signAndMint(repository, transactionData, admin); + } + + private void leaveGroup(Repository repository, PrivateKeyAccount leaver, int groupId) throws DataException { + LeaveGroupTransactionData transactionData = new LeaveGroupTransactionData(TestTransaction.generateBase(leaver), groupId); + TransactionUtils.signAndMint(repository, transactionData, leaver); + } + + private boolean isMember(Repository repository, String address, int groupId) throws DataException { + return repository.getGroupRepository().memberExists(groupId, address); + } + } diff --git a/src/test/java/org/qortal/test/group/OwnerTests.java b/src/test/java/org/qortal/test/group/OwnerTests.java new file mode 100644 index 00000000..d4e7494c --- /dev/null +++ b/src/test/java/org/qortal/test/group/OwnerTests.java @@ -0,0 +1,187 @@ +package org.qortal.test.group; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.AddGroupAdminTransactionData; +import org.qortal.data.transaction.CreateGroupTransactionData; +import org.qortal.data.transaction.JoinGroupTransactionData; +import org.qortal.data.transaction.RemoveGroupAdminTransactionData; +import org.qortal.group.Group.ApprovalThreshold; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction.ValidationResult; + +public class OwnerTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + @Test + public void testAddAdmin() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Attempt to promote non-member + ValidationResult result = addGroupAdmin(repository, alice, groupId, bob.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Promote Bob to admin + addGroupAdmin(repository, alice, groupId, bob.getAddress()); + + // Confirm Bob is now admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Attempt to re-promote admin + result = addGroupAdmin(repository, alice, groupId, bob.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob no longer an admin + assertFalse(isAdmin(repository, bob.getAddress(), groupId)); + + // Confirm Bob is still a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Have Alice try to promote herself + result = addGroupAdmin(repository, alice, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + } + } + + @Test + public void testRemoveAdmin() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + // Create group + int groupId = createGroup(repository, alice, "open-group", true); + + // Attempt to demote non-member + ValidationResult result = removeGroupAdmin(repository, alice, groupId, bob.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Bob to join + joinGroup(repository, bob, groupId); + + // Attempt to demote non-admin member + result = removeGroupAdmin(repository, alice, groupId, bob.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Promote Bob to admin + addGroupAdmin(repository, alice, groupId, bob.getAddress()); + + // Confirm Bob is now admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Attempt to demote admin + result = removeGroupAdmin(repository, alice, groupId, bob.getAddress()); + // Should be OK + assertEquals(ValidationResult.OK, result); + + // Confirm Bob no longer an admin + assertFalse(isAdmin(repository, bob.getAddress(), groupId)); + + // Confirm Bob is still a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Confirm Bob is now admin + assertTrue(isAdmin(repository, bob.getAddress(), groupId)); + + // Have Alice (owner) try to demote herself + result = removeGroupAdmin(repository, alice, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + + // Have Bob try to demote Alice (owner) + result = removeGroupAdmin(repository, bob, groupId, alice.getAddress()); + // Should NOT be OK + assertNotSame(ValidationResult.OK, result); + } + } + + private Integer createGroup(Repository repository, PrivateKeyAccount owner, String groupName, boolean isOpen) throws DataException { + String description = groupName + " (description)"; + + ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE; + int minimumBlockDelay = 10; + int maximumBlockDelay = 1440; + + CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(owner), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + TransactionUtils.signAndMint(repository, transactionData, owner); + + return repository.getGroupRepository().fromGroupName(groupName).getGroupId(); + } + + private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException { + JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private ValidationResult addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException { + AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, owner); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private ValidationResult removeGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException { + RemoveGroupAdminTransactionData transactionData = new RemoveGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, owner); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private boolean isMember(Repository repository, String address, int groupId) throws DataException { + return repository.getGroupRepository().memberExists(groupId, address); + } + + private boolean isAdmin(Repository repository, String address, int groupId) throws DataException { + return repository.getGroupRepository().adminExists(groupId, address); + } + +} diff --git a/src/test/java/org/qortal/test/minting/DisagreementTests.java b/src/test/java/org/qortal/test/minting/DisagreementTests.java index d9d3429a..cd9724b8 100644 --- a/src/test/java/org/qortal/test/minting/DisagreementTests.java +++ b/src/test/java/org/qortal/test/minting/DisagreementTests.java @@ -86,7 +86,7 @@ public class DisagreementTests extends Common { // Cancel reward-share TransactionData cancelRewardShareTransactionData = AccountUtils.createRewardShare(repository, "alice", "bob", CANCEL_SHARE_PERCENT); - TransactionUtils.signAsUnconfirmed(repository, cancelRewardShareTransactionData, signingAccount); + TransactionUtils.signAndImportValid(repository, cancelRewardShareTransactionData, signingAccount); BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount); // Confirm reward-share no longer exists in repository