diff --git a/sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc b/sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc
new file mode 100644
index 0000000..6dd7016
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc
@@ -0,0 +1,351 @@
+/**
+ * Author: dkanus
+ * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
+ * License: GPL
+ * Copyright 2023 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class TEST_Voting extends TestCase
+ abstract
+ dependsOn(VotingModel);
+
+enum ExpectedOutcome {
+ TEST_EO_Continue,
+ TEST_EO_End,
+};
+
+protected static function VotingModel MakeVotingModel(bool drawMeansWin) {
+ local VotingModel model;
+
+ model = VotingModel(__().memory.Allocate(class'VotingModel'));
+ model.Start(drawMeansWin);
+ return model;
+}
+
+protected static function SetVoters(
+ VotingModel model,
+ optional string voterID0,
+ optional string voterID1,
+ optional string voterID2,
+ optional string voterID3,
+ optional string voterID4,
+ optional string voterID5,
+ optional string voterID6,
+ optional string voterID7,
+ optional string voterID8,
+ optional string voterID9
+) {
+ local UserID nextID;
+ local array voterIDs;
+
+ if (voterID0 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID0));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID1 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID1));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID2 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID2));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID3 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID3));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID4 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID4));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID5 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID5));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID6 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID6));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID7 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID7));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID8 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID8));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID9 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID9));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ model.UpdatePotentialVoters(voterIDs);
+}
+
+protected static function MakeFaultyYesVote(
+ VotingModel model,
+ string voterID,
+ VotingModel.VotingResult expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Illegal vote had unexpected result.");
+ TEST_ExpectTrue(model.CastVote(id, true) == expected);
+}
+
+protected static function MakeFaultyNoVote(
+ VotingModel model,
+ string voterID,
+ VotingModel.VotingResult expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Illegal vote had unexpected result.");
+ TEST_ExpectTrue(model.CastVote(id, false) == expected);
+}
+
+protected static function VoteYes(VotingModel model, string voterID, ExpectedOutcome expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Failed to add legitimate vote.");
+ TEST_ExpectTrue(model.CastVote(id, true) == VFR_Success);
+ if (expected == TEST_EO_Continue) {
+ Issue("Vote, that shouldn't have ended voting, ended it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ } else if (expected == TEST_EO_End) {
+ Issue("Vote, that should've ended voting with one side's victory, didn't do it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Success);
+ }
+}
+
+protected static function VoteNo(VotingModel model, string voterID, ExpectedOutcome expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Failed to add legitimate vote.");
+ TEST_ExpectTrue(model.CastVote(id, false) == VFR_Success);
+ if (expected == TEST_EO_Continue) {
+ Issue("Vote, that shouldn't have ended voting, ended it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ } else if (expected == TEST_EO_End) {
+ Issue("Vote, that should've ended voting with one side's victory, didn't do it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Failure);
+ }
+}
+
+protected static function TESTS() {
+ Test_VotingModel();
+}
+
+protected static function Test_VotingModel() {
+ SubTest_YesVoting();
+ SubTest_NoVoting();
+ SubTest_FaultyVoting();
+ SubTest_DisconnectVoting_DrawMeansWin();
+ SubTest_DisconnectVoting_DrawMeansLoss();
+ SubTest_ReconnectVoting_DrawMeansWin();
+ SubTest_ReconnectVoting_DrawMeansLoss();
+}
+
+protected static function SubTest_YesVoting() {
+ local VotingModel model;
+
+ Context("Testing \"yes\" voting.");
+ model = MakeVotingModel(true);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_End);
+
+ model = MakeVotingModel(false);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_End);
+}
+
+protected static function SubTest_NoVoting() {
+ local VotingModel model;
+
+ Context("Testing \"no\" voting.");
+ model = MakeVotingModel(true);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_End);
+
+ model = MakeVotingModel(false);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_End);
+}
+
+protected static function SubTest_FaultyVoting() {
+ local VotingModel model;
+
+ Context("Testing \"faulty\" voting.");
+ model = MakeVotingModel(false);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_End);
+ MakeFaultyYesVote(model, "4", VFR_VotingEnded); // voting already ended, so no more votes here
+
+ model = MakeVotingModel(true);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_End);
+ MakeFaultyYesVote(model, "4", VFR_VotingEnded); // voting already ended, so no more votes here
+}
+
+protected static function SubTest_DisconnectVoting_DrawMeansWin() {
+ local VotingModel model;
+
+ Context("Testing \"disconnect\" voting when draw means victory.");
+ model = MakeVotingModel(true);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 2 "yes" votes
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "9", TEST_EO_Continue);
+ // Here we're at 3 "no" votes, 3 "yes" votes out of 7 total;
+ // disconnect "6" and "9" for "yes" to win
+ SetVoters(model, "2", "4", "5", "8", "10");
+ Issue("Unexpected result after voting users disconnected.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Success);
+}
+
+protected static function SubTest_DisconnectVoting_DrawMeansLoss() {
+ local VotingModel model;
+
+ Context("Testing \"disconnect\" voting when draw means loss.");
+ model = MakeVotingModel(false);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteYes(model, "7", TEST_EO_Continue);
+ SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 3 "yes" votes
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "9", TEST_EO_Continue);
+ // Here we're at 3 "no" votes, 3 "yes" votes out of 7 total;
+ // disconnect "6" and "9" for "yes" to win
+ SetVoters(model, "2", "4", "5", "8", "10");
+ Issue("Unexpected result after voting users disconnected.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Success);
+}
+
+protected static function SubTest_ReconnectVoting_DrawMeansWin() {
+ local VotingModel model;
+
+ Context("Testing \"reconnect\" voting when draw means victory.");
+ model = MakeVotingModel(true);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ // Disconnect 1 3 "yes" voters
+ SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "7", TEST_EO_Continue);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "9", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "3", VFR_NotAllowed);
+ // Restore 3 "yes" voter
+ SetVoters(model, "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteNo(model, "3", TEST_EO_End);
+}
+
+protected static function SubTest_ReconnectVoting_DrawMeansLoss() {
+ local VotingModel model;
+
+ Context("Testing \"reconnect\" voting when draw means loss.");
+ model = MakeVotingModel(false);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ // Disconnect 1 3 "yes" voters
+ SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "7", TEST_EO_Continue);
+ VoteYes(model, "9", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "3", VFR_NotAllowed);
+ // Restore 3 "yes" voter
+ SetVoters(model, "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteNo(model, "3", TEST_EO_End);
+}
+
+defaultproperties {
+ caseGroup = "Commands"
+ caseName = "Voting model"
+}
\ No newline at end of file