Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6fae2907d | |||
| f65c18f0d3 | |||
| 836d74f43b | |||
| 747bd76c3e | |||
| 95a34840b7 |
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 3.11.3
|
||||
VERSION: 3.11.4
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ services:
|
||||
crond -f
|
||||
|
||||
bot:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:3.11.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:3.11.4
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
discord:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.11.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.11.4
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -86,7 +86,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
web:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:3.11.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:3.11.4
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nav-version">v3.11.3</div>
|
||||
<div class="nav-version">v3.11.4</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<EditForm Model="@model" OnValidSubmit="HandleSubmit">
|
||||
<div class="gm-form-group">
|
||||
<label class="gm-form-label">Название игры</label>
|
||||
<InputText @bind-Value="model.Title" class="gm-form-control" placeholder="например, D&D 5e: Dragon's Hoard" />
|
||||
<InputText @bind-Value="model.Title" @bind-Value:event="oninput" class="gm-form-control" placeholder="например, D&D 5e: Dragon's Hoard" />
|
||||
<div class="gm-form-hint">Изменение этого поля обновит все сессии в одной группе.</div>
|
||||
</div>
|
||||
|
||||
@@ -48,12 +48,12 @@
|
||||
|
||||
<div class="gm-form-group">
|
||||
<label class="gm-form-label">Ссылка для подключения</label>
|
||||
<InputText @bind-Value="model.JoinLink" class="gm-form-control" placeholder="Ссылка на Discord или VTT" />
|
||||
<InputText @bind-Value="model.JoinLink" @bind-Value:event="oninput" class="gm-form-control" placeholder="Ссылка на Discord или VTT" />
|
||||
</div>
|
||||
|
||||
<div class="gm-form-group">
|
||||
<label class="gm-form-label">Лимит мест</label>
|
||||
<InputNumber @bind-Value="model.MaxPlayers" class="gm-form-control" min="1" placeholder="Без лимита" />
|
||||
<InputNumber @bind-Value="model.MaxPlayers" @bind-Value:event="oninput" class="gm-form-control" min="1" placeholder="Без лимита" />
|
||||
<div class="gm-form-hint">Пустое значение означает запись без лимита. Если лимит заполнен, новые игроки попадут в лист ожидания.</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1075,11 +1075,18 @@ public sealed class SessionService(
|
||||
"\n" +
|
||||
$"👥 Мест: <b>{(maxPlayers.HasValue ? maxPlayers.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : "без лимита")}</b>";
|
||||
|
||||
try
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: oldSession.TelegramChatId,
|
||||
messageThreadId: oldSession.ThreadId,
|
||||
text: notification,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to send session update notification to chat {ChatId} for session {SessionId}", oldSession.TelegramChatId, sessionId);
|
||||
}
|
||||
|
||||
var mode = SessionNotificationModeExtensions.FromDatabaseValue(oldSession.NotificationMode);
|
||||
if (mode.ShouldSendDirectMessages())
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ The runner logs in to a real Telegram user account, creates a supergroup, invite
|
||||
- `test_dashboard_authenticates_and_shows_groups`
|
||||
Builds a valid Mini App initData payload, posts it to `/auth/telegram-webapp`, and verifies that the Blazor home page renders the authenticated greeting.
|
||||
- `test_dashboard_session_edit_flow`
|
||||
Seeds a player, group, and session directly in PostgreSQL, opens the group details page, clicks through to the session editor, changes the title, join link, max players and publication mode, asserts the updated values appear on the page, and verifies the persisted database state.
|
||||
Seeds a player, group, and session directly in PostgreSQL, opens the group details page, navigates to the session editor, changes the title, join link and max players, asserts the updated values appear on the page, and verifies the persisted database state.
|
||||
- `test_dashboard_session_delete_flow`
|
||||
Seeds a session, opens the group details page, confirms the deletion dialog, asserts the session disappears from the dashboard, and verifies the session was removed from PostgreSQL.
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def _seed_group_in_database(
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO gmrelay.players (platform, external_user_id, display_name, created_at)
|
||||
INSERT INTO public.players (platform, external_user_id, display_name, created_at)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (platform, external_user_id)
|
||||
WHERE platform IS NOT NULL AND external_user_id IS NOT NULL
|
||||
@@ -96,7 +96,7 @@ def _seed_group_in_database(
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO gmrelay.game_groups (id, platform, external_group_id, telegram_chat_id, name, created_at)
|
||||
INSERT INTO public.game_groups (id, platform, external_group_id, telegram_chat_id, name, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id
|
||||
@@ -106,7 +106,7 @@ def _seed_group_in_database(
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO gmrelay.group_managers (group_id, player_id, role, created_at)
|
||||
INSERT INTO public.group_managers (group_id, player_id, role, created_at)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (group_id, player_id) DO UPDATE SET role = EXCLUDED.role
|
||||
""",
|
||||
@@ -115,7 +115,7 @@ def _seed_group_in_database(
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO gmrelay.sessions (
|
||||
INSERT INTO public.sessions (
|
||||
id, group_id, batch_id, title, scheduled_at, join_link, status, publication_mode,
|
||||
notification_mode, max_players, format, location_address, system, created_at
|
||||
)
|
||||
@@ -150,7 +150,7 @@ def _seed_group_in_database(
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO gmrelay.session_participants (session_id, player_id, registration_status, rsvp_status, is_gm, created_at)
|
||||
INSERT INTO public.session_participants (session_id, player_id, registration_status, rsvp_status, is_gm, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (session_id, player_id) DO NOTHING
|
||||
""",
|
||||
@@ -172,7 +172,7 @@ def _get_session_from_db(session_id: str):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT title, join_link, max_players, publication_mode, format, location_address, system, status
|
||||
FROM gmrelay.sessions
|
||||
FROM public.sessions
|
||||
WHERE id = %s
|
||||
""",
|
||||
(session_id,),
|
||||
@@ -201,7 +201,7 @@ def _assert_session_deleted(session_id: str) -> None:
|
||||
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT COUNT(*) FROM gmrelay.sessions WHERE id = %s", (session_id,))
|
||||
cur.execute("SELECT COUNT(*) FROM public.sessions WHERE id = %s", (session_id,))
|
||||
assert cur.fetchone()[0] == 0, f"Session {session_id} was not deleted"
|
||||
|
||||
|
||||
@@ -212,15 +212,41 @@ def _delete_test_data(group_id: str, session_id: str) -> None:
|
||||
if not dsn:
|
||||
return
|
||||
|
||||
telegram_id = str(_telegram_id())
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM gmrelay.session_participants WHERE session_id = %s", (session_id,))
|
||||
cur.execute("DELETE FROM gmrelay.sessions WHERE id = %s", (session_id,))
|
||||
cur.execute("DELETE FROM gmrelay.group_managers WHERE group_id = %s", (group_id,))
|
||||
cur.execute("DELETE FROM gmrelay.game_groups WHERE id = %s", (group_id,))
|
||||
cur.execute("DELETE FROM public.session_participants WHERE session_id = %s", (session_id,))
|
||||
cur.execute("DELETE FROM public.sessions WHERE id = %s", (session_id,))
|
||||
cur.execute("DELETE FROM public.group_managers WHERE group_id = %s", (group_id,))
|
||||
cur.execute("DELETE FROM public.game_groups WHERE id = %s", (group_id,))
|
||||
|
||||
# Only remove the player if the test created it. If the Telegram ID
|
||||
# belongs to a real production user (e.g. the Toutsu account), leave
|
||||
# the row intact so we do not break that account.
|
||||
cur.execute(
|
||||
"DELETE FROM gmrelay.players WHERE external_user_id = %s AND platform = 'Telegram'",
|
||||
(str(_telegram_id()),),
|
||||
"SELECT id FROM public.players WHERE platform = 'Telegram' AND external_user_id = %s",
|
||||
(telegram_id,),
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
# Determine whether this player has any other groups or sessions.
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM public.group_managers m "
|
||||
"JOIN public.players p ON p.id = m.player_id "
|
||||
"WHERE p.platform = 'Telegram' AND p.external_user_id = %s",
|
||||
(telegram_id,),
|
||||
)
|
||||
manager_count = cur.fetchone()[0]
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM public.session_participants sp "
|
||||
"JOIN public.players p ON p.id = sp.player_id "
|
||||
"WHERE p.platform = 'Telegram' AND p.external_user_id = %s",
|
||||
(telegram_id,),
|
||||
)
|
||||
participant_count = cur.fetchone()[0]
|
||||
if manager_count == 0 and participant_count == 0:
|
||||
cur.execute(
|
||||
"DELETE FROM public.players WHERE platform = 'Telegram' AND external_user_id = %s",
|
||||
(telegram_id,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -260,6 +286,7 @@ def test_dashboard_authenticates_and_shows_groups() -> None:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
page.set_viewport_size({"width": 1920, "height": 1080})
|
||||
|
||||
_authenticate_page(page, base_url, bot_token, telegram_id)
|
||||
_wait_for_blazor(page)
|
||||
@@ -296,6 +323,7 @@ def test_dashboard_session_edit_flow() -> None:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
page.set_viewport_size({"width": 1920, "height": 1080})
|
||||
|
||||
_authenticate_page(page, base_url, bot_token, telegram_id)
|
||||
_wait_for_blazor(page)
|
||||
@@ -303,24 +331,47 @@ def test_dashboard_session_edit_flow() -> None:
|
||||
page.goto(f"{base_url}/group/{group_id}")
|
||||
page.wait_for_selector(f"text={original_title}", timeout=15000)
|
||||
|
||||
page.locator(f"text={original_title}").first.click()
|
||||
page.goto(f"{base_url}/session/edit/{session_id}")
|
||||
page.wait_for_selector("text=Редактирование сессии", timeout=15000)
|
||||
|
||||
title_input = page.get_by_label("Название игры")
|
||||
title_input.fill(updated_title)
|
||||
def _fill_blazor_input(locator, value: str) -> None:
|
||||
"""Fill a Blazor-bound input, move focus out, and wait for the value to stick."""
|
||||
locator.fill(value)
|
||||
locator.press("Tab")
|
||||
# Blazor Server round-trips on change; give it time to update the model
|
||||
# and re-render before the next field is touched.
|
||||
page.wait_for_timeout(500)
|
||||
expect(locator).to_have_value(value)
|
||||
|
||||
join_input = page.get_by_label("Ссылка для подключения")
|
||||
join_input.fill(updated_join_link)
|
||||
title_input = page.locator(".gm-form-group").filter(has_text="Название игры").locator("input").first
|
||||
_fill_blazor_input(title_input, updated_title)
|
||||
|
||||
max_players_input = page.get_by_label("Лимит мест")
|
||||
max_players_input.fill("3")
|
||||
join_input = page.locator(".gm-form-group").filter(has_text="Ссылка для подключения").locator("input").first
|
||||
_fill_blazor_input(join_input, updated_join_link)
|
||||
|
||||
page.get_by_label("Режим публикации").select_option("Catalog")
|
||||
max_players_input = page.locator(".gm-form-group").filter(has_text="Лимит мест").locator("input").first
|
||||
_fill_blazor_input(max_players_input, "3")
|
||||
|
||||
# Verify the form actually received the new values before Blazor submits.
|
||||
filled_title = title_input.input_value()
|
||||
filled_join = join_input.input_value()
|
||||
filled_max = max_players_input.input_value()
|
||||
print(f"Edit form values before save: title={filled_title!r}, join={filled_join!r}, max={filled_max!r}")
|
||||
|
||||
save_button = page.locator("button:has-text('Сохранить изменения')").first
|
||||
save_button.click()
|
||||
|
||||
page.wait_for_url(f"{base_url}/group/{group_id}", timeout=15000)
|
||||
try:
|
||||
page.wait_for_selector(f"text={updated_title}", timeout=15000)
|
||||
except Exception:
|
||||
dump_path = "e2e_edit_group_debug.html"
|
||||
with open(dump_path, "w", encoding="utf-8") as f:
|
||||
f.write(page.content())
|
||||
print("URL after edit save:", page.url)
|
||||
print(f"Group page HTML dumped to {dump_path}")
|
||||
page.screenshot(path="e2e_edit_group_debug.png")
|
||||
raise
|
||||
expect(page.locator(f"text={updated_title}").first).to_be_visible()
|
||||
|
||||
db_session = _get_session_from_db(session_id)
|
||||
@@ -328,7 +379,6 @@ def test_dashboard_session_edit_flow() -> None:
|
||||
assert db_session["title"] == updated_title
|
||||
assert db_session["join_link"] == updated_join_link
|
||||
assert db_session["max_players"] == 3
|
||||
assert db_session["publication_mode"] == "Catalog"
|
||||
|
||||
browser.close()
|
||||
finally:
|
||||
@@ -359,6 +409,7 @@ def test_dashboard_session_delete_flow() -> None:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
page.set_viewport_size({"width": 1920, "height": 1080})
|
||||
|
||||
_authenticate_page(page, base_url, bot_token, telegram_id)
|
||||
_wait_for_blazor(page)
|
||||
@@ -368,7 +419,7 @@ def test_dashboard_session_delete_flow() -> None:
|
||||
|
||||
page.on("dialog", lambda dialog: dialog.accept())
|
||||
|
||||
delete_button = page.locator("button:has-text('Удалить')").first
|
||||
delete_button = page.locator(".session-card-mobile button:has-text('Удалить')").first
|
||||
expect(delete_button).to_be_visible()
|
||||
delete_button.click()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user