refactor(#22): разделить SessionBatchRenderer на neutral view и Telegram renderer
- SessionBatchViewBuilder в Shared собирает нейтральную view model - TelegramSessionBatchRenderer в Bot/Web рендерит HTML + InlineKeyboardMarkup - DiscordSessionBatchRenderer заглушка подготовлена - BatchMessageEditor перенесён из Shared в Bot/Web - Удалён SessionBatchRenderer, убран Telegram.Bot из Shared.csproj - Обновлены все вызовы (7 handler-ов + Web SessionService + smoke tests) - Новые тесты на builder и Telegram renderer
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# Issue #19: Выровнять /newsession с batch-сценарием лендинга
|
||||
|
||||
> **Goal:** Сделать `/newsession` атомарным: либо весь batch создаётся целиком, либо ничего — с понятным сообщением об ошибках. Убрать создание частичных сессий при наличии любых ошибок ввода.
|
||||
|
||||
**Architecture:** Изменить `NewSessionParseResult.IsValid` так, чтобы он учитывал ЛЮБЫЕ parse-ошибки (некорректные даты, лимиты, повторы, прошедшие даты). Обновить `CreateSessionHandler`, чтобы при `!IsValid` отправлялось одно детальное сообщение с перечислением ошибок и help-шаблоном, и creation прерывался до транзакции БД.
|
||||
|
||||
**Tech Stack:** C#, .NET, Dapper, Telegram.Bot, xUnit
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Добавить `HasErrors` и ужесточить `IsValid`
|
||||
|
||||
**Objective:** Запретить частичное создание: `IsValid == false`, если есть хоть одна ошибка парсинга.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/GmRelay.Bot/Features/Sessions/CreateSession/NewSessionCommandParser.cs`
|
||||
- Test: `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs`
|
||||
|
||||
**Step 1: RED — написать failing test**
|
||||
|
||||
Добавить в конец `NewSessionCommandParserTests.cs`:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Parse_ShouldBeInvalid_WhenAnyErrorsPresent()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
|
||||
var text = """
|
||||
Название: Delta Green
|
||||
Время: 25.04.2026 19:30
|
||||
Время: 31.04.2026 19:30
|
||||
Ссылка: https://example.test/dg
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, nowUtc);
|
||||
|
||||
Assert.True(result.HasErrors);
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test and verify RED**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~NewSessionCommandParserTests" -v n
|
||||
```
|
||||
Expected: `HasErrors` property not found → compile error or FAIL.
|
||||
|
||||
**Step 3: GREEN — minimal implementation**
|
||||
|
||||
В `NewSessionCommandParser.cs`, в `NewSessionParseResult`, заменить `IsValid` на:
|
||||
|
||||
```csharp
|
||||
public bool HasErrors =>
|
||||
PastTimeInputs.Count > 0 ||
|
||||
InvalidTimeInputs.Count > 0 ||
|
||||
InvalidSeatLimitInputs.Count > 0 ||
|
||||
InvalidRecurringInputs.Count > 0;
|
||||
|
||||
public bool IsValid =>
|
||||
!string.IsNullOrWhiteSpace(Title) &&
|
||||
!string.IsNullOrWhiteSpace(Link) &&
|
||||
ScheduledTimes.Count > 0 &&
|
||||
!HasErrors;
|
||||
```
|
||||
|
||||
**Step 4: Run test and verify GREEN**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~NewSessionCommandParserTests" -v n
|
||||
```
|
||||
Expected: 7 passed (включая новый).
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/GmRelay.Bot/Features/Sessions/CreateSession/NewSessionCommandParser.cs
|
||||
git add tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs
|
||||
git commit -m "feat(#19): add HasErrors to prevent partial batch creation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Обновить существующий тест на Past/Invalid times
|
||||
|
||||
**Objective:** Старый тест ожидал `IsValid == true` при partial invalid — теперь это баг, нужно обновить assertion.
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs:114`
|
||||
|
||||
**Step 1: Write failing expectation first**
|
||||
|
||||
Заменить в методе `Parse_ShouldCollectPastAndInvalidTimes` (примерно строка 114):
|
||||
|
||||
```csharp
|
||||
// Было:
|
||||
Assert.True(result.IsValid);
|
||||
// Стало:
|
||||
Assert.False(result.IsValid);
|
||||
```
|
||||
|
||||
**Step 2: Run test and verify RED**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "Parse_ShouldCollectPastAndInvalidTimes" -v n
|
||||
```
|
||||
Expected: FAIL — expected False, actual True.
|
||||
|
||||
**Step 3: Fix already done in Task 1**
|
||||
|
||||
Task 1 уже изменил `IsValid`. Перезапустить тест.
|
||||
|
||||
**Step 4: Run test and verify GREEN**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "Parse_ShouldCollectPastAndInvalidTimes" -v n
|
||||
```
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs
|
||||
git commit -m "test(#19): update assertion - partial invalid means invalid"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Обновить `CreateSessionHandler` для атомарной валидации и единого сообщения об ошибках
|
||||
|
||||
**Objective:** Убрать разбросанные warning-сообщения. При любых ошибках — одно сообщение с перечнем ошибок + help-шаблон. Не создавать сессии.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs`
|
||||
|
||||
**Step 1: RED — напишем интеграционный поведенческий тест (опционально, landing test уже покрывает happy path)**
|
||||
|
||||
Просто запустим landing promise smoke test и убедимся, что он ещё green (happy path не сломан).
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "TelegramLandingPromisesSmokeTests" -v n
|
||||
```
|
||||
Expected: PASS.
|
||||
|
||||
**Step 2: GREEN — minimal change in handler**
|
||||
|
||||
В `CreateSessionHandler.cs`, метод `HandleAsync`, заменить начало (строки 19–59) на:
|
||||
|
||||
```csharp
|
||||
var parseResult = NewSessionCommandParser.Parse(message.Text ?? message.Caption, DateTimeOffset.UtcNow);
|
||||
|
||||
var errorMessages = new List<string>();
|
||||
|
||||
foreach (var timeInput in parseResult.PastTimeInputs)
|
||||
errorMessages.Add($"⚠️ Дата {timeInput} находится в прошлом и будет пропущена.");
|
||||
|
||||
foreach (var timeInput in parseResult.InvalidTimeInputs)
|
||||
errorMessages.Add($"⚠️ Некорректный формат времени '{timeInput}'. Пропущено.");
|
||||
|
||||
foreach (var seatLimitInput in parseResult.InvalidSeatLimitInputs)
|
||||
errorMessages.Add($"⚠️ Некорректный лимит мест '{seatLimitInput}'. Укажите целое число больше 0.");
|
||||
|
||||
foreach (var recurringInput in parseResult.InvalidRecurringInputs)
|
||||
errorMessages.Add($"⚠️ Некорректный повтор расписания '{recurringInput}'. Укажите число игр 1–52 и шаг 1–365 дней.");
|
||||
|
||||
if (!parseResult.IsValid)
|
||||
{
|
||||
var helpText = """
|
||||
❌ Не удалось распознать формат. Пожалуйста, используйте шаблон:
|
||||
|
||||
/newsession
|
||||
Название: My Game
|
||||
Время: 15.05.2026 19:30
|
||||
Время: 22.05.2026 19:30
|
||||
Мест: 4
|
||||
Ссылка: https://link
|
||||
Картинка: https://cover
|
||||
|
||||
Для повтора можно указать одну дату и строки:
|
||||
Игр: 4
|
||||
Интервал: 7
|
||||
""";
|
||||
|
||||
if (errorMessages.Count > 0)
|
||||
{
|
||||
helpText = string.Join('\n', errorMessages) + "\n\n" + helpText;
|
||||
}
|
||||
|
||||
await botClient.SendMessage(
|
||||
chatId: message.Chat.Id,
|
||||
text: helpText,
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Build and verify**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet build src/GmRelay.Bot
|
||||
```
|
||||
Expected: Build succeeded.
|
||||
|
||||
**Step 4: Run all relevant tests**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~CreateSession|FullyQualifiedName~Landing" -v n
|
||||
```
|
||||
Expected: All passed.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs
|
||||
git commit -m "feat(#19): atomic validation with detailed error message in /newsession"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Проверить полный test suite на регрессии
|
||||
|
||||
**Objective:** Убедиться, что изменения не сломали существующие тесты.
|
||||
|
||||
**Files:**
|
||||
- Test: все тесты проекта `GmRelay.Bot.Tests`
|
||||
|
||||
**Step 1: Run full test suite**
|
||||
|
||||
```bash
|
||||
cd /repo && dotnet test tests/GmRelay.Bot.Tests -v n
|
||||
```
|
||||
Expected: All passed.
|
||||
|
||||
**Step 2: Commit (if any test baseline updated)**
|
||||
|
||||
Если всё зелёное — commit message:
|
||||
```bash
|
||||
git commit --allow-empty -m "test(#19): verify full suite green after atomic validation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] `NewSessionCommandParser.Parse` возвращает `IsValid = false` при любых ошибках (past/invalid times, seat limits, recurring).
|
||||
- [ ] `HasErrors` true ↔ есть хотя бы одна ошибка.
|
||||
- [ ] `CreateSessionHandler` не открывает БД-транзакцию, если `!IsValid`.
|
||||
- [ ] Пользователь получает одно сообщение с перечислением ошибок + help.
|
||||
- [ ] Landing promise smoke test проходит (happy path не сломан).
|
||||
- [ ] Полный test suite зелёный.
|
||||
Reference in New Issue
Block a user