feat: add multi-option reschedule voting
This commit is contained in:
+98
-11
@@ -5,28 +5,115 @@ namespace GmRelay.Bot.Tests.Features.Sessions.RescheduleSession;
|
||||
public sealed class HandleRescheduleTimeInputHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildVotingMessage_ShouldShowApprovedAndPendingParticipants()
|
||||
public void TryParseVotingInput_ShouldAcceptTwoOptionsAndDeadline()
|
||||
{
|
||||
var approvedId = Guid.NewGuid();
|
||||
var pendingId = Guid.NewGuid();
|
||||
var now = new DateTimeOffset(2026, 4, 24, 8, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var ok = RescheduleVotingInput.TryParse(
|
||||
"""
|
||||
25.04.2026 19:30
|
||||
26.04.2026 18:00
|
||||
Дедлайн: 25.04.2026 12:00
|
||||
""",
|
||||
now,
|
||||
out var input,
|
||||
out var error);
|
||||
|
||||
Assert.True(ok, error);
|
||||
Assert.Equal(2, input.Options.Count);
|
||||
Assert.Equal(new DateTimeOffset(2026, 4, 25, 16, 30, 0, TimeSpan.Zero), input.Options[0]);
|
||||
Assert.Equal(new DateTimeOffset(2026, 4, 26, 15, 0, 0, TimeSpan.Zero), input.Options[1]);
|
||||
Assert.Equal(new DateTimeOffset(2026, 4, 25, 9, 0, 0, TimeSpan.Zero), input.Deadline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseVotingInput_ShouldRejectSingleOption()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 4, 24, 8, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var ok = RescheduleVotingInput.TryParse(
|
||||
"""
|
||||
25.04.2026 19:30
|
||||
Дедлайн: 25.04.2026 12:00
|
||||
""",
|
||||
now,
|
||||
out _,
|
||||
out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Equal("Укажите от 2 до 3 вариантов времени.", error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildVotingMessage_ShouldShowOptionsDeadlineVotesAndPendingParticipants()
|
||||
{
|
||||
var firstOptionId = Guid.NewGuid();
|
||||
var secondOptionId = Guid.NewGuid();
|
||||
var aliceId = Guid.NewGuid();
|
||||
var bobId = Guid.NewGuid();
|
||||
var charlieId = Guid.NewGuid();
|
||||
var currentTime = new DateTime(2026, 4, 25, 16, 30, 0, DateTimeKind.Utc);
|
||||
var newTime = new DateTimeOffset(2026, 4, 26, 17, 0, 0, TimeSpan.Zero);
|
||||
var deadline = new DateTimeOffset(2026, 4, 25, 9, 0, 0, TimeSpan.Zero);
|
||||
var options = new List<RescheduleOptionDto>
|
||||
{
|
||||
new(firstOptionId, 1, new DateTimeOffset(2026, 4, 26, 16, 0, 0, TimeSpan.Zero)),
|
||||
new(secondOptionId, 2, new DateTimeOffset(2026, 4, 27, 17, 0, 0, TimeSpan.Zero))
|
||||
};
|
||||
var participants = new List<VoteParticipantDto>
|
||||
{
|
||||
new(approvedId, "Alice", "alice"),
|
||||
new(pendingId, "Bob", null)
|
||||
new(aliceId, "Alice", "alice"),
|
||||
new(bobId, "Bob", null),
|
||||
new(charlieId, "Charlie", null)
|
||||
};
|
||||
var votes = new List<RescheduleOptionVoteDto>
|
||||
{
|
||||
new(firstOptionId, aliceId, "Alice", "alice"),
|
||||
new(secondOptionId, bobId, "Bob", null)
|
||||
};
|
||||
|
||||
var text = HandleRescheduleTimeInputHandler.BuildVotingMessage(
|
||||
"Shadowrun",
|
||||
currentTime,
|
||||
newTime,
|
||||
deadline,
|
||||
options,
|
||||
participants,
|
||||
[approvedId]);
|
||||
votes);
|
||||
|
||||
Assert.Contains("Shadowrun", text);
|
||||
Assert.Contains("✅ @alice", text);
|
||||
Assert.Contains("⏳ Bob", text);
|
||||
Assert.Contains("Голоса: 1/2 ✅", text);
|
||||
Assert.Contains("Дедлайн: <b>25 апреля 2026, 12:00</b> (МСК)", text);
|
||||
Assert.Contains("1. <b>26 апреля 2026, 19:00</b> (МСК) — 1 голос", text);
|
||||
Assert.Contains("@alice", text);
|
||||
Assert.Contains("2. <b>27 апреля 2026, 20:00</b> (МСК) — 1 голос", text);
|
||||
Assert.Contains("Bob", text);
|
||||
Assert.Contains("Не проголосовали: Charlie", text);
|
||||
Assert.Contains("Голосов: 2/3", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildVotingKeyboard_ShouldCreateOneButtonPerOption()
|
||||
{
|
||||
var firstOptionId = Guid.NewGuid();
|
||||
var secondOptionId = Guid.NewGuid();
|
||||
var options = new List<RescheduleOptionDto>
|
||||
{
|
||||
new(firstOptionId, 1, new DateTimeOffset(2026, 4, 26, 16, 0, 0, TimeSpan.Zero)),
|
||||
new(secondOptionId, 2, new DateTimeOffset(2026, 4, 27, 17, 0, 0, TimeSpan.Zero))
|
||||
};
|
||||
|
||||
var keyboard = HandleRescheduleTimeInputHandler.BuildVotingKeyboard(options);
|
||||
var buttons = keyboard.InlineKeyboard.SelectMany(row => row).ToList();
|
||||
|
||||
Assert.Collection(
|
||||
buttons,
|
||||
button =>
|
||||
{
|
||||
Assert.Equal("1. 26.04 19:00", button.Text);
|
||||
Assert.Equal($"reschedule_vote:{firstOptionId}", button.CallbackData);
|
||||
},
|
||||
button =>
|
||||
{
|
||||
Assert.Equal("2. 27.04 20:00", button.Text);
|
||||
Assert.Equal($"reschedule_vote:{secondOptionId}", button.CallbackData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+36
-18
@@ -5,32 +5,50 @@ namespace GmRelay.Bot.Tests.Features.Sessions.RescheduleSession;
|
||||
public sealed class RescheduleVoteRulesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Evaluate_ShouldReject_WhenParticipantVotesNo()
|
||||
public void SelectWinner_ShouldApproveSingleTopOption()
|
||||
{
|
||||
var decision = RescheduleVoteRules.Evaluate("no", totalParticipants: 4, approvedParticipants: 3);
|
||||
var winningOptionId = Guid.NewGuid();
|
||||
var otherOptionId = Guid.NewGuid();
|
||||
|
||||
Assert.Equal(RescheduleVoteOutcome.Rejected, decision.Outcome);
|
||||
Assert.False(decision.ShouldRescheduleSession);
|
||||
Assert.Equal("Вы проголосовали против переноса.", decision.CallbackText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ShouldApprove_WhenEveryoneVotedYes()
|
||||
{
|
||||
var decision = RescheduleVoteRules.Evaluate("yes", totalParticipants: 3, approvedParticipants: 3);
|
||||
var decision = RescheduleVoteRules.SelectWinner(
|
||||
[
|
||||
new RescheduleOptionVoteCount(winningOptionId, 3),
|
||||
new RescheduleOptionVoteCount(otherOptionId, 1)
|
||||
]);
|
||||
|
||||
Assert.Equal(RescheduleVoteOutcome.Approved, decision.Outcome);
|
||||
Assert.True(decision.ShouldRescheduleSession);
|
||||
Assert.True(decision.ShouldResetParticipantRsvps);
|
||||
Assert.Equal(winningOptionId, decision.SelectedOptionId);
|
||||
Assert.Equal("Победил вариант с большинством голосов.", decision.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ShouldStayPending_WhileVotesOutstanding()
|
||||
public void SelectWinner_ShouldRejectTie()
|
||||
{
|
||||
var decision = RescheduleVoteRules.Evaluate("yes", totalParticipants: 5, approvedParticipants: 2);
|
||||
var firstOptionId = Guid.NewGuid();
|
||||
var secondOptionId = Guid.NewGuid();
|
||||
|
||||
Assert.Equal(RescheduleVoteOutcome.Pending, decision.Outcome);
|
||||
Assert.False(decision.ShouldRescheduleSession);
|
||||
Assert.False(decision.ShouldResetParticipantRsvps);
|
||||
var decision = RescheduleVoteRules.SelectWinner(
|
||||
[
|
||||
new RescheduleOptionVoteCount(firstOptionId, 2),
|
||||
new RescheduleOptionVoteCount(secondOptionId, 2)
|
||||
]);
|
||||
|
||||
Assert.Equal(RescheduleVoteOutcome.Rejected, decision.Outcome);
|
||||
Assert.Null(decision.SelectedOptionId);
|
||||
Assert.Equal("Голоса разделились поровну, перенос не применяется.", decision.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectWinner_ShouldRejectWhenNobodyVoted()
|
||||
{
|
||||
var decision = RescheduleVoteRules.SelectWinner(
|
||||
[
|
||||
new RescheduleOptionVoteCount(Guid.NewGuid(), 0),
|
||||
new RescheduleOptionVoteCount(Guid.NewGuid(), 0)
|
||||
]);
|
||||
|
||||
Assert.Equal(RescheduleVoteOutcome.Rejected, decision.Outcome);
|
||||
Assert.Null(decision.SelectedOptionId);
|
||||
Assert.Equal("Никто не проголосовал до дедлайна, перенос не применяется.", decision.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user