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