feat: allow players to leave sessions
This commit is contained in:
@@ -6,7 +6,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VERSION: 1.2.0
|
VERSION: 1.3.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.2.0</Version>
|
<Version>1.3.0</Version>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
|
|
||||||
Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire.
|
Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire.
|
||||||
|
|
||||||
|
**Текущая версия:** `v1.3.0`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Ключевые возможности
|
## ✨ Ключевые возможности
|
||||||
|
|
||||||
### 🤖 Telegram Бот
|
### 🤖 Telegram Бот
|
||||||
- **📅 Создание расписаний (Batch Sessions)**: Создавайте сразу несколько игр одним сообщением (на неделю или месяц вперед).
|
- **📅 Создание расписаний (Batch Sessions)**: Создавайте сразу несколько игр одним сообщением (на неделю или месяц вперед).
|
||||||
- **✋ Интерактивная запись**: Игроки записываются на конкретные даты нажатием одной кнопки.
|
- **✋ Интерактивная запись и выход**: Игроки записываются на конкретные даты и самостоятельно снимают запись нажатием одной кнопки.
|
||||||
- **👥 Лимит мест и лист ожидания**: ГМ задаёт максимальный состав, бот не переполняет сессию и автоматически ведёт очередь ожидания.
|
- **👥 Лимит мест и лист ожидания**: ГМ задаёт максимальный состав, бот не переполняет сессию, автоматически ведёт очередь ожидания и освобождённое место отдаёт первому ожидающему.
|
||||||
- **📁 Поддержка Форумов (Telegram Topics)**: Бот автоматически создает тему во вложенных чатах Telegram под каждую новую пачку игр.
|
- **📁 Поддержка Форумов (Telegram Topics)**: Бот автоматически создает тему во вложенных чатах Telegram под каждую новую пачку игр.
|
||||||
- **❌ Управление сессиями**: Мастер может отменять отдельные игры прямо в общем сообщении расписания.
|
- **❌ Управление сессиями**: Мастер может отменять отдельные игры прямо в общем сообщении расписания.
|
||||||
- **🗓 Экспорт в Календарь**: Генерация файла `.ics` для добавления всех игр в Google, Apple или Яндекс Календарь одной командой.
|
- **🗓 Экспорт в Календарь**: Генерация файла `.ics` для добавления всех игр в Google, Apple или Яндекс Календарь одной командой.
|
||||||
@@ -118,6 +120,8 @@ docker compose up -d
|
|||||||
|
|
||||||
Строка `Мест:` необязательна. Если она указана, игроки сверх лимита попадут в лист ожидания, а ГМ сможет повысить первого ожидающего через кнопку в Telegram или Web Dashboard.
|
Строка `Мест:` необязательна. Если она указана, игроки сверх лимита попадут в лист ожидания, а ГМ сможет повысить первого ожидающего через кнопку в Telegram или Web Dashboard.
|
||||||
|
|
||||||
|
Игрок может самостоятельно снять запись кнопкой `🚪 Выйти` в сообщении расписания. Если он был в основном составе и в листе ожидания есть игроки, бот автоматически переводит первого ожидающего в основной состав и обновляет сообщение пачки.
|
||||||
|
|
||||||
### Другие команды
|
### Другие команды
|
||||||
- `/listsessions` — Показать список всех актуальных игр в этой группе.
|
- `/listsessions` — Показать список всех актуальных игр в этой группе.
|
||||||
- `/reschedulesession` — Перенести сессию на другое время с голосованием игроков.
|
- `/reschedulesession` — Перенести сессию на другое время с голосованием игроков.
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.2.0
|
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.3.0
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -29,7 +29,7 @@ services:
|
|||||||
- gmrelay
|
- gmrelay
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.2.0
|
image: git.codeanddice.ru/toutsu/gmrelay-web:1.3.0
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
using Dapper;
|
||||||
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Rendering;
|
||||||
|
using Npgsql;
|
||||||
|
using Telegram.Bot;
|
||||||
|
|
||||||
|
namespace GmRelay.Bot.Features.Sessions.CreateSession;
|
||||||
|
|
||||||
|
public sealed record LeaveSessionCommand(
|
||||||
|
Guid SessionId,
|
||||||
|
long TelegramUserId,
|
||||||
|
string CallbackQueryId,
|
||||||
|
long ChatId,
|
||||||
|
int MessageId);
|
||||||
|
|
||||||
|
internal sealed record LeaveSessionInfoDto(string Title, Guid BatchId, string Status, int? MaxPlayers);
|
||||||
|
internal sealed record LeaveSessionParticipantDto(Guid ParticipantRowId, string DisplayName, string RegistrationStatus);
|
||||||
|
internal sealed record LeaveSessionPromotionDto(Guid ParticipantRowId, string DisplayName);
|
||||||
|
|
||||||
|
public sealed class LeaveSessionHandler(
|
||||||
|
NpgsqlDataSource dataSource,
|
||||||
|
ITelegramBotClient bot,
|
||||||
|
ILogger<LeaveSessionHandler> logger)
|
||||||
|
{
|
||||||
|
public async Task HandleAsync(LeaveSessionCommand command, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
||||||
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||||||
|
var transactionCommitted = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = await connection.QuerySingleOrDefaultAsync<LeaveSessionInfoDto>(
|
||||||
|
"""
|
||||||
|
SELECT title AS Title,
|
||||||
|
batch_id AS BatchId,
|
||||||
|
status AS Status,
|
||||||
|
max_players AS MaxPlayers
|
||||||
|
FROM sessions
|
||||||
|
WHERE id = @SessionId
|
||||||
|
FOR UPDATE
|
||||||
|
""",
|
||||||
|
new { command.SessionId },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
if (session is null)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(ct);
|
||||||
|
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SessionStatus.IsCancelled(session.Status))
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(ct);
|
||||||
|
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия уже отменена.", cancellationToken: ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var participant = await connection.QuerySingleOrDefaultAsync<LeaveSessionParticipantDto>(
|
||||||
|
"""
|
||||||
|
SELECT sp.id AS ParticipantRowId,
|
||||||
|
p.display_name AS DisplayName,
|
||||||
|
sp.registration_status AS RegistrationStatus
|
||||||
|
FROM session_participants sp
|
||||||
|
JOIN players p ON p.id = sp.player_id
|
||||||
|
WHERE sp.session_id = @SessionId
|
||||||
|
AND p.telegram_id = @TelegramUserId
|
||||||
|
AND sp.is_gm = false
|
||||||
|
FOR UPDATE OF sp
|
||||||
|
""",
|
||||||
|
new { command.SessionId, command.TelegramUserId },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
if (participant is null)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(ct);
|
||||||
|
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Вы не записаны на эту сессию.", cancellationToken: ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"""
|
||||||
|
DELETE FROM session_participants
|
||||||
|
WHERE id = @ParticipantRowId
|
||||||
|
""",
|
||||||
|
new { participant.ParticipantRowId },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
var activeParticipantsAfterLeave = await connection.ExecuteScalarAsync<int>(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM session_participants
|
||||||
|
WHERE session_id = @SessionId
|
||||||
|
AND is_gm = false
|
||||||
|
AND registration_status = @Active
|
||||||
|
""",
|
||||||
|
new { command.SessionId, Active = ParticipantRegistrationStatus.Active },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
var waitlistedParticipants = await connection.ExecuteScalarAsync<int>(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM session_participants
|
||||||
|
WHERE session_id = @SessionId
|
||||||
|
AND is_gm = false
|
||||||
|
AND registration_status = @Waitlisted
|
||||||
|
""",
|
||||||
|
new { command.SessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
string? promotedDisplayName = null;
|
||||||
|
if (SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
|
||||||
|
participant.RegistrationStatus,
|
||||||
|
session.MaxPlayers,
|
||||||
|
activeParticipantsAfterLeave,
|
||||||
|
waitlistedParticipants))
|
||||||
|
{
|
||||||
|
var promoted = await connection.QuerySingleAsync<LeaveSessionPromotionDto>(
|
||||||
|
"""
|
||||||
|
SELECT sp.id AS ParticipantRowId,
|
||||||
|
p.display_name AS DisplayName
|
||||||
|
FROM session_participants sp
|
||||||
|
JOIN players p ON p.id = sp.player_id
|
||||||
|
WHERE sp.session_id = @SessionId
|
||||||
|
AND sp.is_gm = false
|
||||||
|
AND sp.registration_status = @Waitlisted
|
||||||
|
ORDER BY sp.created_at ASC, sp.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE OF sp
|
||||||
|
""",
|
||||||
|
new { command.SessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"""
|
||||||
|
UPDATE session_participants
|
||||||
|
SET registration_status = @Active,
|
||||||
|
rsvp_status = @Pending,
|
||||||
|
responded_at = NULL
|
||||||
|
WHERE id = @ParticipantRowId
|
||||||
|
""",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
promoted.ParticipantRowId,
|
||||||
|
Active = ParticipantRegistrationStatus.Active,
|
||||||
|
Pending = RsvpStatus.Pending
|
||||||
|
},
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
promotedDisplayName = promoted.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchSessions = (await connection.QueryAsync<SessionBatchDto>(
|
||||||
|
"""
|
||||||
|
SELECT id AS SessionId,
|
||||||
|
scheduled_at AS ScheduledAt,
|
||||||
|
status AS Status,
|
||||||
|
max_players AS MaxPlayers
|
||||||
|
FROM sessions
|
||||||
|
WHERE batch_id = @BatchId
|
||||||
|
ORDER BY scheduled_at
|
||||||
|
""",
|
||||||
|
new { session.BatchId },
|
||||||
|
transaction)).ToList();
|
||||||
|
|
||||||
|
var batchParticipants = (await connection.QueryAsync<ParticipantBatchDto>(
|
||||||
|
"""
|
||||||
|
SELECT sp.session_id AS SessionId,
|
||||||
|
p.display_name AS DisplayName,
|
||||||
|
p.telegram_username AS TelegramUsername,
|
||||||
|
sp.registration_status AS RegistrationStatus
|
||||||
|
FROM session_participants sp
|
||||||
|
JOIN players p ON sp.player_id = p.id
|
||||||
|
JOIN sessions s ON sp.session_id = s.id
|
||||||
|
WHERE s.batch_id = @BatchId AND sp.is_gm = false
|
||||||
|
ORDER BY sp.registration_status ASC, sp.created_at ASC, sp.responded_at ASC, p.created_at ASC
|
||||||
|
""",
|
||||||
|
new { session.BatchId },
|
||||||
|
transaction)).ToList();
|
||||||
|
|
||||||
|
await transaction.CommitAsync(ct);
|
||||||
|
transactionCommitted = true;
|
||||||
|
|
||||||
|
var renderResult = SessionBatchRenderer.Render(session.Title, batchSessions, batchParticipants);
|
||||||
|
|
||||||
|
await bot.EditMessageText(
|
||||||
|
chatId: command.ChatId,
|
||||||
|
messageId: command.MessageId,
|
||||||
|
text: renderResult.Text,
|
||||||
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||||
|
replyMarkup: renderResult.Markup,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
var callbackText = participant.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||||||
|
? "Вы удалены из листа ожидания."
|
||||||
|
: promotedDisplayName is null
|
||||||
|
? "Вы отписались от сессии."
|
||||||
|
: $"Вы отписались от сессии. Место получил(а) {promotedDisplayName}.";
|
||||||
|
|
||||||
|
await bot.AnswerCallbackQuery(command.CallbackQueryId, callbackText, cancellationToken: ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Ошибка при самостоятельной отмене записи на сессию {SessionId}", command.SessionId);
|
||||||
|
if (!transactionCommitted)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorText = transactionCommitted
|
||||||
|
? "Запись снята, но не удалось обновить сообщение расписания."
|
||||||
|
: "Произошла ошибка при отмене записи.";
|
||||||
|
await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ public sealed class UpdateRouter(
|
|||||||
HandleRsvpHandler rsvpHandler,
|
HandleRsvpHandler rsvpHandler,
|
||||||
CreateSessionHandler createSessionHandler,
|
CreateSessionHandler createSessionHandler,
|
||||||
JoinSessionHandler joinSessionHandler,
|
JoinSessionHandler joinSessionHandler,
|
||||||
|
LeaveSessionHandler leaveSessionHandler,
|
||||||
PromoteWaitlistedPlayerHandler promoteWaitlistedPlayerHandler,
|
PromoteWaitlistedPlayerHandler promoteWaitlistedPlayerHandler,
|
||||||
CancelSessionHandler cancelSessionHandler,
|
CancelSessionHandler cancelSessionHandler,
|
||||||
DeleteSessionHandler deleteSessionHandler,
|
DeleteSessionHandler deleteSessionHandler,
|
||||||
@@ -73,6 +74,19 @@ public sealed class UpdateRouter(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action == "leave_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var leaveSessionId))
|
||||||
|
{
|
||||||
|
var command = new LeaveSessionCommand(
|
||||||
|
SessionId: leaveSessionId,
|
||||||
|
TelegramUserId: query.From.Id,
|
||||||
|
CallbackQueryId: query.Id,
|
||||||
|
ChatId: message.Chat.Id,
|
||||||
|
MessageId: message.MessageId);
|
||||||
|
|
||||||
|
await leaveSessionHandler.HandleAsync(command, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (action == "cancel_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var cancelSessionId))
|
if (action == "cancel_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var cancelSessionId))
|
||||||
{
|
{
|
||||||
var command = new CancelSessionCommand(
|
var command = new CancelSessionCommand(
|
||||||
@@ -210,6 +224,7 @@ public sealed class UpdateRouter(
|
|||||||
Ссылка: https://link
|
Ссылка: https://link
|
||||||
|
|
||||||
/listsessions — список предстоящих сессий
|
/listsessions — список предстоящих сессий
|
||||||
|
Игроки могут записаться кнопкой «На дату» и сняться кнопкой «Выйти».
|
||||||
/help — эта справка
|
/help — эта справка
|
||||||
""",
|
""",
|
||||||
cancellationToken: ct);
|
cancellationToken: ct);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ builder.Services.AddSingleton<HandleRsvpHandler>();
|
|||||||
builder.Services.AddSingleton<SendJoinLinkHandler>();
|
builder.Services.AddSingleton<SendJoinLinkHandler>();
|
||||||
builder.Services.AddSingleton<CreateSessionHandler>();
|
builder.Services.AddSingleton<CreateSessionHandler>();
|
||||||
builder.Services.AddSingleton<JoinSessionHandler>();
|
builder.Services.AddSingleton<JoinSessionHandler>();
|
||||||
|
builder.Services.AddSingleton<LeaveSessionHandler>();
|
||||||
builder.Services.AddSingleton<PromoteWaitlistedPlayerHandler>();
|
builder.Services.AddSingleton<PromoteWaitlistedPlayerHandler>();
|
||||||
builder.Services.AddSingleton<CancelSessionHandler>();
|
builder.Services.AddSingleton<CancelSessionHandler>();
|
||||||
builder.Services.AddSingleton<GmRelay.Bot.Features.Sessions.ListSessions.DeleteSessionHandler>();
|
builder.Services.AddSingleton<GmRelay.Bot.Features.Sessions.ListSessions.DeleteSessionHandler>();
|
||||||
|
|||||||
@@ -23,4 +23,14 @@ public static class SessionCapacityRules
|
|||||||
|
|
||||||
return !maxPlayers.HasValue || activeParticipants < maxPlayers.Value;
|
return !maxPlayers.HasValue || activeParticipants < maxPlayers.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool ShouldPromoteAfterParticipantLeaves(
|
||||||
|
string removedRegistrationStatus,
|
||||||
|
int? maxPlayers,
|
||||||
|
int activeParticipantsAfterLeave,
|
||||||
|
int waitlistedParticipants)
|
||||||
|
{
|
||||||
|
return removedRegistrationStatus == ParticipantRegistrationStatus.Active
|
||||||
|
&& CanPromoteWaitlistedPlayer(maxPlayers, activeParticipantsAfterLeave, waitlistedParticipants);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ public static class SessionBatchRenderer
|
|||||||
buttons.Add(new[]
|
buttons.Add(new[]
|
||||||
{
|
{
|
||||||
InlineKeyboardButton.WithCallbackData(GetJoinButtonText(session, sessionPlayers.Count, dateTitle), $"join_session:{session.SessionId}"),
|
InlineKeyboardButton.WithCallbackData(GetJoinButtonText(session, sessionPlayers.Count, dateTitle), $"join_session:{session.SessionId}"),
|
||||||
|
InlineKeyboardButton.WithCallbackData($"🚪 Выйти {dateTitle}", $"leave_session:{session.SessionId}")
|
||||||
|
});
|
||||||
|
|
||||||
|
buttons.Add(new[]
|
||||||
|
{
|
||||||
InlineKeyboardButton.WithCallbackData($"❌ Отменить {dateTitle} (ГМ)", $"cancel_session:{session.SessionId}"),
|
InlineKeyboardButton.WithCallbackData($"❌ Отменить {dateTitle} (ГМ)", $"cancel_session:{session.SessionId}"),
|
||||||
InlineKeyboardButton.WithCallbackData($"⏰ (ГМ)", $"reschedule_session:{session.SessionId}")
|
InlineKeyboardButton.WithCallbackData($"⏰ (ГМ)", $"reschedule_session:{session.SessionId}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="nav-version">v1.2.0</div>
|
<div class="nav-version">v1.3.0</div>
|
||||||
</div>
|
</div>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
GM-Relay Design System v1.2.0
|
GM-Relay Design System v1.3.0
|
||||||
Dark RPG Dashboard Theme
|
Dark RPG Dashboard Theme
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,26 @@ public sealed class SessionCapacityRulesTests
|
|||||||
Assert.False(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 3, waitlistedParticipants: 1));
|
Assert.False(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 3, waitlistedParticipants: 1));
|
||||||
Assert.False(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 2, waitlistedParticipants: 0));
|
Assert.False(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 2, waitlistedParticipants: 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldPromoteAfterParticipantLeaves_ShouldOnlyPromoteAfterActiveParticipantLeaves()
|
||||||
|
{
|
||||||
|
Assert.True(SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
|
||||||
|
removedRegistrationStatus: ParticipantRegistrationStatus.Active,
|
||||||
|
maxPlayers: 2,
|
||||||
|
activeParticipantsAfterLeave: 1,
|
||||||
|
waitlistedParticipants: 1));
|
||||||
|
|
||||||
|
Assert.False(SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
|
||||||
|
removedRegistrationStatus: ParticipantRegistrationStatus.Waitlisted,
|
||||||
|
maxPlayers: 2,
|
||||||
|
activeParticipantsAfterLeave: 1,
|
||||||
|
waitlistedParticipants: 1));
|
||||||
|
|
||||||
|
Assert.False(SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
|
||||||
|
removedRegistrationStatus: ParticipantRegistrationStatus.Active,
|
||||||
|
maxPlayers: 2,
|
||||||
|
activeParticipantsAfterLeave: 1,
|
||||||
|
waitlistedParticipants: 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,13 +42,15 @@ public sealed class SessionBatchRendererTests
|
|||||||
Assert.Contains("Лист ожидания (1)", text);
|
Assert.Contains("Лист ожидания (1)", text);
|
||||||
Assert.Contains("Charlie", text);
|
Assert.Contains("Charlie", text);
|
||||||
Assert.Contains("Bob", text);
|
Assert.Contains("Bob", text);
|
||||||
Assert.Equal(2, result.Markup.InlineKeyboard.Count());
|
Assert.Equal(4, result.Markup.InlineKeyboard.Count());
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
buttons.Select(button => button.CallbackData),
|
buttons.Select(button => button.CallbackData),
|
||||||
callbackData => Assert.Equal($"join_session:{firstSessionId}", callbackData),
|
callbackData => Assert.Equal($"join_session:{firstSessionId}", callbackData),
|
||||||
|
callbackData => Assert.Equal($"leave_session:{firstSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"cancel_session:{firstSessionId}", callbackData),
|
callbackData => Assert.Equal($"cancel_session:{firstSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"reschedule_session:{firstSessionId}", callbackData),
|
callbackData => Assert.Equal($"reschedule_session:{firstSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"join_session:{secondSessionId}", callbackData),
|
callbackData => Assert.Equal($"join_session:{secondSessionId}", callbackData),
|
||||||
|
callbackData => Assert.Equal($"leave_session:{secondSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData),
|
callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData),
|
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"promote_waitlist:{secondSessionId}", callbackData));
|
callbackData => Assert.Equal($"promote_waitlist:{secondSessionId}", callbackData));
|
||||||
|
|||||||
Reference in New Issue
Block a user