feat(web): finalize Discord OAuth and platform-agnostic auth
PR Checks / test-and-build (pull_request) Successful in 5m47s
PR Checks / test-and-build (pull_request) Successful in 5m47s
- Bump version to 2.8.0 across all versioned files - Fix AuthorizedSessionServiceTests for platform-agnostic identity - Update Razor Pages to use *ForCurrentUserAsync APIs - Add backward-compatible constructors to WebGameGroup/WebGroupManager - Make DiscordOAuthOptions properties non-required for config binding Bump version → 2.8.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 2.7.2
|
||||
VERSION: 2.8.0
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>2.7.2</Version>
|
||||
<Version>2.8.0</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire.
|
||||
|
||||
**Текущая версия:** `v2.7.2`.
|
||||
**Текущая версия:** `v2.8.0`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ services:
|
||||
crond -f
|
||||
|
||||
bot:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:2.7.2
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:2.8.0
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
discord:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:2.7.2
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:2.8.0
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
web:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:2.7.2
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:2.8.0
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
private Guid selectedGroupId;
|
||||
private Guid? deletingTemplateId;
|
||||
private bool isCreatingTemplate;
|
||||
private long telegramId;
|
||||
private string? externalUserId;
|
||||
private string? errorMessage;
|
||||
private string? successMessage;
|
||||
private CampaignTemplateEditModel templateModel = new();
|
||||
@@ -195,13 +195,13 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!authState.User.TryGetTelegramId(out telegramId))
|
||||
if (!authState.User.TryGetPlatformIdentity(out var platform, out externalUserId))
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
groups = await SessionService.GetGroupsForGmAsync(telegramId);
|
||||
groups = await SessionService.GetGroupsForCurrentUserAsync();
|
||||
selectedGroupId = groups.FirstOrDefault()?.Id ?? Guid.Empty;
|
||||
|
||||
if (selectedGroupId != Guid.Empty)
|
||||
@@ -228,7 +228,7 @@
|
||||
campaignTemplates = null;
|
||||
campaignTemplateModels = [];
|
||||
|
||||
var templates = await SessionService.GetCampaignTemplatesForGmAsync(selectedGroupId, telegramId);
|
||||
var templates = await SessionService.GetCampaignTemplatesForCurrentUserAsync(selectedGroupId);
|
||||
if (templates is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
@@ -260,9 +260,8 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.CreateCampaignTemplateForGmAsync(
|
||||
await SessionService.CreateCampaignTemplateForCurrentUserAsync(
|
||||
selectedGroupId,
|
||||
telegramId,
|
||||
new CreateCampaignTemplateRequest(
|
||||
templateModel.Name,
|
||||
templateModel.Title,
|
||||
@@ -298,7 +297,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.DeleteCampaignTemplateForGmAsync(template.Id, telegramId);
|
||||
await SessionService.DeleteCampaignTemplateForCurrentUserAsync(template.Id);
|
||||
successMessage = "Шаблон кампании удалён.";
|
||||
await LoadTemplates();
|
||||
}
|
||||
|
||||
@@ -87,13 +87,13 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!authState.User.TryGetTelegramId(out var telegramId))
|
||||
if (!authState.User.TryGetPlatformIdentity(out var platform, out var externalUserId))
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
session = await SessionService.GetSessionForGmAsync(SessionId, telegramId);
|
||||
session = await SessionService.GetSessionForCurrentUserAsync(SessionId);
|
||||
if (session is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
@@ -114,7 +114,7 @@
|
||||
try
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!authState.User.TryGetTelegramId(out var telegramId))
|
||||
if (!authState.User.TryGetPlatformIdentity(out var platform, out var externalUserId))
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
@@ -122,7 +122,7 @@
|
||||
|
||||
var utcTime = new DateTimeOffset(model.ScheduledAtLocal, TimeSpan.FromHours(3)).ToUniversalTime().UtcDateTime;
|
||||
|
||||
await SessionService.UpdateSessionForGmAsync(SessionId, telegramId, model.Title, utcTime, model.JoinLink, model.MaxPlayers);
|
||||
await SessionService.UpdateSessionForCurrentUserAsync(SessionId, model.Title, utcTime, model.JoinLink, model.MaxPlayers);
|
||||
Navigation.NavigateTo($"/group/{session!.GroupId}");
|
||||
}
|
||||
catch (SessionAccessDeniedException)
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
</span>
|
||||
@if (groupManagement.CurrentUserIsOwner && manager.Role == GroupManagerRoleExtensions.CoGmValue)
|
||||
{
|
||||
<button type="button" class="btn-gm btn-gm-outline" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(removingCoGmId == manager.TelegramId)" @onclick="() => RemoveCoGm(manager.TelegramId)">
|
||||
@(removingCoGmId == manager.TelegramId ? "⏳ Удаляем..." : "Убрать")
|
||||
<button type="button" class="btn-gm btn-gm-outline" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(removingCoGmId == manager.ExternalUserId)" @onclick="() => RemoveCoGm(manager.ExternalUserId ?? manager.TelegramId.ToString())">
|
||||
@(removingCoGmId == manager.ExternalUserId ? "⏳ Удаляем..." : "Убрать")
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@@ -403,9 +403,9 @@
|
||||
private Guid? promotingSessionId;
|
||||
private Guid? processingBatchId;
|
||||
private Guid? processingTemplateId;
|
||||
private long? removingCoGmId;
|
||||
private string? removingCoGmId;
|
||||
private bool isAddingCoGm;
|
||||
private long telegramId;
|
||||
private string? externalUserId;
|
||||
private string? errorMessage;
|
||||
private string? successMessage;
|
||||
private CoGmEditModel coGmModel = new();
|
||||
@@ -417,7 +417,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!authState.User.TryGetTelegramId(out telegramId))
|
||||
if (!authState.User.TryGetPlatformIdentity(out var platform, out externalUserId))
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
@@ -428,21 +428,21 @@
|
||||
|
||||
private async Task LoadSessions()
|
||||
{
|
||||
groupManagement = await SessionService.GetGroupManagementForGmAsync(GroupId, telegramId);
|
||||
groupManagement = await SessionService.GetGroupManagementForCurrentUserAsync(GroupId);
|
||||
if (groupManagement is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
sessions = await SessionService.GetUpcomingSessionsForGmAsync(GroupId, telegramId);
|
||||
sessions = await SessionService.GetUpcomingSessionsForCurrentUserAsync(GroupId);
|
||||
if (sessions is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
campaignTemplates = await SessionService.GetCampaignTemplatesForGmAsync(GroupId, telegramId);
|
||||
campaignTemplates = await SessionService.GetCampaignTemplatesForCurrentUserAsync(GroupId);
|
||||
if (campaignTemplates is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
@@ -470,8 +470,8 @@
|
||||
{
|
||||
await SessionService.AddCoGmForOwnerAsync(
|
||||
GroupId,
|
||||
telegramId,
|
||||
coGmModel.TelegramId.Value,
|
||||
"Telegram",
|
||||
coGmModel.TelegramId.Value.ToString(),
|
||||
coGmModel.DisplayName,
|
||||
coGmModel.TelegramUsername);
|
||||
|
||||
@@ -493,15 +493,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveCoGm(long coGmTelegramId)
|
||||
private async Task RemoveCoGm(string coGmExternalUserId)
|
||||
{
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
removingCoGmId = coGmTelegramId;
|
||||
removingCoGmId = coGmExternalUserId;
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.RemoveCoGmForOwnerAsync(GroupId, telegramId, coGmTelegramId);
|
||||
await SessionService.RemoveCoGmForOwnerAsync(GroupId, "Telegram", coGmExternalUserId);
|
||||
successMessage = "Co-GM удалён.";
|
||||
await LoadSessions();
|
||||
}
|
||||
@@ -527,7 +527,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.PromoteWaitlistedPlayerForGmAsync(sessionId, telegramId);
|
||||
await SessionService.PromoteWaitlistedPlayerForCurrentUserAsync(sessionId);
|
||||
await LoadSessions();
|
||||
}
|
||||
catch (SessionAccessDeniedException)
|
||||
@@ -559,7 +559,7 @@
|
||||
loadingParticipantsSessionId = sessionId;
|
||||
try
|
||||
{
|
||||
var participants = await SessionService.GetSessionParticipantsForGmAsync(sessionId, telegramId);
|
||||
var participants = await SessionService.GetSessionParticipantsForCurrentUserAsync(sessionId);
|
||||
participantsCache[sessionId] = participants ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -582,7 +582,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.RemovePlayerFromSessionForGmAsync(sessionId, telegramId, participantId);
|
||||
await SessionService.RemovePlayerFromSessionForCurrentUserAsync(sessionId, participantId);
|
||||
participantsCache.Remove(sessionId);
|
||||
successMessage = "Игрок исключён.";
|
||||
await LoadSessions();
|
||||
@@ -658,10 +658,9 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.UpdateBatchDetailsForGmAsync(batch.BatchId, telegramId, batch.Title, batch.JoinLink);
|
||||
await SessionService.UpdateBatchNotificationModeForGmAsync(
|
||||
await SessionService.UpdateBatchDetailsForCurrentUserAsync(batch.BatchId, batch.Title, batch.JoinLink);
|
||||
await SessionService.UpdateBatchNotificationModeForCurrentUserAsync(
|
||||
batch.BatchId,
|
||||
telegramId,
|
||||
SessionNotificationModeExtensions.FromDatabaseValue(batch.NotificationMode));
|
||||
successMessage = "Настройки batch обновлены.";
|
||||
await LoadSessions();
|
||||
@@ -696,7 +695,7 @@
|
||||
try
|
||||
{
|
||||
var utcTime = new DateTimeOffset(batch.FirstScheduledAtLocal, TimeSpan.FromHours(3)).ToUniversalTime().UtcDateTime;
|
||||
await SessionService.RescheduleBatchForGmAsync(batch.BatchId, telegramId, utcTime, batch.IntervalDays);
|
||||
await SessionService.RescheduleBatchForCurrentUserAsync(batch.BatchId, utcTime, batch.IntervalDays);
|
||||
successMessage = "Расписание пачки обновлено.";
|
||||
await LoadSessions();
|
||||
}
|
||||
@@ -726,7 +725,7 @@
|
||||
? BatchCloneInterval.NextMonth
|
||||
: BatchCloneInterval.NextWeek;
|
||||
|
||||
var clonedBatch = await SessionService.CloneBatchForGmAsync(batch.BatchId, telegramId, interval);
|
||||
var clonedBatch = await SessionService.CloneBatchForCurrentUserAsync(batch.BatchId, interval);
|
||||
successMessage = $"Пачка склонирована: {clonedBatch.SessionCount} игр.";
|
||||
await LoadSessions();
|
||||
}
|
||||
@@ -759,7 +758,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var createdBatch = await SessionService.CreateBatchFromCampaignTemplateForGmAsync(template.Id, telegramId, utcTime);
|
||||
var createdBatch = await SessionService.CreateBatchFromCampaignTemplateForCurrentUserAsync(template.Id, utcTime);
|
||||
successMessage = $"Batch создан из шаблона: {createdBatch.SessionCount} игр.";
|
||||
await LoadSessions();
|
||||
}
|
||||
@@ -836,7 +835,7 @@
|
||||
private bool IsTemplateBusy(CampaignTemplateUsageModel template) => processingTemplateId == template.Id;
|
||||
|
||||
private string CurrentUserRole =>
|
||||
groupManagement?.Managers.FirstOrDefault(manager => manager.TelegramId == telegramId)?.Role
|
||||
groupManagement?.Managers.FirstOrDefault(manager => manager.ExternalUserId == externalUserId)?.Role
|
||||
?? GroupManagerRoleExtensions.CoGmValue;
|
||||
|
||||
private static string FormatRole(string role) =>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using System.Security.Claims
|
||||
@attribute [Authorize]
|
||||
@inject ISessionStore SessionStore
|
||||
@inject AuthorizedSessionService SessionService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@@ -85,9 +85,9 @@
|
||||
<td>
|
||||
<div class="player-info">
|
||||
<span class="player-name">@s.DisplayName</span>
|
||||
@if (!string.IsNullOrEmpty(s.TelegramUsername))
|
||||
@if (!string.IsNullOrEmpty(s.ExternalUsername))
|
||||
{
|
||||
<span class="player-username">@@@s.TelegramUsername</span>
|
||||
<span class="player-username">@@@s.ExternalUsername</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
@@ -171,21 +171,20 @@
|
||||
Navigation.NavigateTo("/login");
|
||||
return;
|
||||
}
|
||||
var telegramIdClaim = user.FindFirst("telegram_id")?.Value
|
||||
?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (!long.TryParse(telegramIdClaim, out var telegramId))
|
||||
if (!user.TryGetPlatformIdentity(out var platform, out var externalUserId))
|
||||
{
|
||||
Navigation.NavigateTo("/login");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!await SessionStore.IsGroupManagerAsync(GroupId, telegramId))
|
||||
var groupManagement = await SessionService.GetGroupManagementForCurrentUserAsync(GroupId);
|
||||
if (groupManagement is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
stats = await SessionStore.GetGroupAttendanceStatsAsync(GroupId) ?? new();
|
||||
stats = await SessionService.GetGroupAttendanceStatsForCurrentUserAsync(GroupId) ?? new();
|
||||
UpdateSortedStats();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="glass-card group-card">
|
||||
<div class="group-card-icon">🎮</div>
|
||||
<h3 class="group-card-title">@group.Name</h3>
|
||||
<p class="group-card-id">ID: @group.TelegramChatId</p>
|
||||
<p class="group-card-id">ID: @(group.Platform == "Discord" ? group.ExternalGroupId : group.TelegramChatId.ToString())</p>
|
||||
<span class="status-badge @(group.ManagerRole == GroupManagerRoleExtensions.OwnerValue ? "status-success" : "status-info")" style="align-self: flex-start; margin-bottom: 1rem;">
|
||||
@FormatRole(group.ManagerRole)
|
||||
</span>
|
||||
@@ -93,13 +93,13 @@
|
||||
var user = authState.User;
|
||||
userName = user.Identity?.Name ?? "Мастер Игры";
|
||||
|
||||
if (!user.TryGetTelegramId(out var telegramId))
|
||||
if (!user.TryGetPlatformIdentity(out _, out _))
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
groups = await SessionService.GetGroupsForGmAsync(telegramId);
|
||||
groups = await SessionService.GetGroupsForCurrentUserAsync();
|
||||
}
|
||||
|
||||
private static string FormatRole(string role) =>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td>@entry.ChangedAt.ToString("dd.MM.yyyy HH:mm") UTC</td>
|
||||
<td>@entry.ActorName (@entry.ActorTelegramId)</td>
|
||||
<td>@entry.ActorName (@entry.ActorExternalUserId)</td>
|
||||
<td>
|
||||
<span class="status-badge @(GetBadgeClass(entry.ChangeType))">
|
||||
@GetChangeTypeLabel(entry.ChangeType)
|
||||
@@ -82,13 +82,13 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!authState.User.TryGetTelegramId(out var telegramId))
|
||||
if (!authState.User.TryGetPlatformIdentity(out var platform, out var externalUserId))
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
var session = await SessionService.GetSessionForGmAsync(SessionId, telegramId);
|
||||
var session = await SessionService.GetSessionForCurrentUserAsync(SessionId);
|
||||
if (session is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
sessionTitle = session.Title;
|
||||
groupId = session.GroupId;
|
||||
entries = await SessionService.GetSessionHistoryForGmAsync(SessionId, telegramId);
|
||||
entries = await SessionService.GetSessionHistoryForCurrentUserAsync(SessionId);
|
||||
}
|
||||
|
||||
private string GetChangeTypeLabel(string changeType) => changeType switch
|
||||
|
||||
@@ -2,9 +2,9 @@ namespace GmRelay.Web;
|
||||
|
||||
public sealed class DiscordOAuthOptions
|
||||
{
|
||||
public required string ClientId { get; set; }
|
||||
public required string ClientSecret { get; set; }
|
||||
public required string RedirectUri { get; set; }
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
public string ClientSecret { get; set; } = string.Empty;
|
||||
public string RedirectUri { get; set; } = string.Empty;
|
||||
public string[] Scopes { get; set; } = ["identify", "guilds"];
|
||||
|
||||
public void Validate()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using GmRelay.Web;
|
||||
using GmRelay.Web.Components;
|
||||
using GmRelay.Web.Health;
|
||||
using GmRelay.Web.Services;
|
||||
|
||||
@@ -13,7 +13,16 @@ public sealed record WebGameGroup(
|
||||
string? ExternalGroupId,
|
||||
string Name,
|
||||
string? Platform,
|
||||
string ManagerRole = GroupManagerRoleExtensions.OwnerValue);
|
||||
string ManagerRole = GroupManagerRoleExtensions.OwnerValue)
|
||||
{
|
||||
public long GmTelegramId { get; init; }
|
||||
|
||||
public WebGameGroup(Guid id, long telegramChatId, string name, long gmTelegramId)
|
||||
: this(id, telegramChatId, null, name, null)
|
||||
{
|
||||
GmTelegramId = gmTelegramId;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record WebGroupManager(
|
||||
long TelegramId,
|
||||
@@ -22,7 +31,11 @@ public sealed record WebGroupManager(
|
||||
string? TelegramUsername,
|
||||
string? ExternalUsername,
|
||||
string Role,
|
||||
DateTime AddedAt);
|
||||
DateTime AddedAt)
|
||||
{
|
||||
public WebGroupManager(long telegramId, string displayName, string? telegramUsername, string role, DateTime addedAt)
|
||||
: this(telegramId, null, displayName, telegramUsername, null, role, addedAt) { }
|
||||
}
|
||||
|
||||
public sealed record WebGroupManagement(
|
||||
WebGameGroup Group,
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class DiscordProjectStructureTests
|
||||
var prChecks = File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "pr-checks.yml"));
|
||||
var deploy = File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml"));
|
||||
|
||||
Assert.Contains("gmrelay-discord-bot:2.7.2", compose);
|
||||
Assert.Contains("gmrelay-discord-bot:2.8.0", compose);
|
||||
Assert.Contains("Discord__Token=${DISCORD_BOT_TOKEN:?Set DISCORD_BOT_TOKEN in .env}", compose);
|
||||
Assert.Contains("src/GmRelay.DiscordBot/Dockerfile", deploy);
|
||||
Assert.Contains("DISCORD_BOT_TOKEN", deploy);
|
||||
@@ -75,13 +75,13 @@ public sealed class DiscordProjectStructureTests
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
|
||||
Assert.Contains("<Version>2.7.2</Version>", File.ReadAllText(Path.Combine(repoRoot, "Directory.Build.props")));
|
||||
Assert.Contains("VERSION: 2.7.2", File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml")));
|
||||
Assert.Contains("gmrelay-bot:2.7.2", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
|
||||
Assert.Contains("gmrelay-web:2.7.2", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
|
||||
Assert.Contains("gmrelay-discord-bot:2.7.2", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
|
||||
Assert.Contains("<Version>2.8.0</Version>", File.ReadAllText(Path.Combine(repoRoot, "Directory.Build.props")));
|
||||
Assert.Contains("VERSION: 2.8.0", File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml")));
|
||||
Assert.Contains("gmrelay-bot:2.8.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
|
||||
Assert.Contains("gmrelay-web:2.8.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
|
||||
Assert.Contains("gmrelay-discord-bot:2.8.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
|
||||
Assert.Contains(
|
||||
"v2.7.2",
|
||||
"v2.8.0",
|
||||
File.ReadAllText(Path.Combine(repoRoot, "src", "GmRelay.Web", "Components", "Layout", "NavMenu.razor")));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
using GmRelay.Web.Services;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using GmRelay.Shared.Domain;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Web;
|
||||
|
||||
public sealed class AuthorizedSessionServiceTests
|
||||
{
|
||||
private static IHttpContextAccessor CreateAccessor(string externalUserId, string? name = null)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, externalUserId),
|
||||
new Claim("TelegramId", externalUserId),
|
||||
new Claim("Platform", "Telegram")
|
||||
};
|
||||
if (name is not null)
|
||||
claims.Add(new Claim(ClaimTypes.Name, name));
|
||||
|
||||
var identity = new ClaimsIdentity(claims, "Test");
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var httpContext = new DefaultHttpContext { User = principal };
|
||||
return new HttpContextAccessor { HttpContext = httpContext };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUpcomingSessionsForGmAsync_ReturnsSessions_WhenGroupBelongsToGm()
|
||||
{
|
||||
@@ -19,9 +38,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, gmId);
|
||||
var sessions = await service.GetUpcomingSessionsForCurrentUserAsync(groupId);
|
||||
|
||||
Assert.NotNull(sessions);
|
||||
Assert.Single(sessions);
|
||||
@@ -47,9 +67,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, coGmId, GroupManagerRole.CoGm)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(coGmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, coGmId);
|
||||
var sessions = await service.GetUpcomingSessionsForCurrentUserAsync(groupId);
|
||||
|
||||
Assert.NotNull(sessions);
|
||||
Assert.Single(sessions);
|
||||
@@ -65,9 +86,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, 42, "Alpha", 2002L)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, 1001L);
|
||||
var sessions = await service.GetUpcomingSessionsForCurrentUserAsync(groupId);
|
||||
|
||||
Assert.Null(sessions);
|
||||
}
|
||||
@@ -87,9 +109,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var session = await service.GetSessionForGmAsync(sessionId, gmId);
|
||||
var session = await service.GetSessionForCurrentUserAsync(sessionId);
|
||||
|
||||
Assert.NotNull(session);
|
||||
Assert.Equal(sessionId, session.Id);
|
||||
@@ -109,9 +132,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var session = await service.GetSessionForGmAsync(sessionId, 1001L);
|
||||
var session = await service.GetSessionForCurrentUserAsync(sessionId);
|
||||
|
||||
Assert.Null(session);
|
||||
}
|
||||
@@ -130,9 +154,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.UpdateSessionForGmAsync(sessionId, 1001L, "Updated", DateTime.UtcNow.AddDays(1), "https://example.test/b", 5);
|
||||
var action = () => service.UpdateSessionForCurrentUserAsync(sessionId, "Updated", DateTime.UtcNow.AddDays(1), "https://example.test/b", 5);
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.UpdateCalled);
|
||||
@@ -154,9 +179,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.UpdateSessionForGmAsync(sessionId, gmId, "Updated", scheduledAt, "https://example.test/b", 5);
|
||||
await service.UpdateSessionForCurrentUserAsync(sessionId, "Updated", scheduledAt, "https://example.test/b", 5);
|
||||
|
||||
Assert.True(store.UpdateCalled);
|
||||
Assert.Equal(groupId, store.LastUpdatedGroupId);
|
||||
@@ -183,9 +209,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", originalTime, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.UpdateSessionForGmAsync(sessionId, gmId, "Updated Title", originalTime, "https://example.test/a", 4);
|
||||
await service.UpdateSessionForCurrentUserAsync(sessionId, "Updated Title", originalTime, "https://example.test/a", 4);
|
||||
|
||||
Assert.Single(store.LogEntries);
|
||||
Assert.Equal("Title", store.LogEntries[0].ChangeType);
|
||||
@@ -209,10 +236,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", originalTime, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var newTime = originalTime.AddDays(1);
|
||||
await service.UpdateSessionForGmAsync(sessionId, gmId, "Updated Title", newTime, "https://example.test/b", 5);
|
||||
await service.UpdateSessionForCurrentUserAsync(sessionId, "Updated Title", newTime, "https://example.test/b", 5);
|
||||
|
||||
Assert.Equal(4, store.LogEntries.Count);
|
||||
Assert.Contains(store.LogEntries, e => e.ChangeType == "Title");
|
||||
@@ -237,9 +265,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", originalTime, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.UpdateSessionForGmAsync(sessionId, gmId, "Session A", originalTime, "https://example.test/a", 4);
|
||||
await service.UpdateSessionForCurrentUserAsync(sessionId, "Session A", originalTime, "https://example.test/a", 4);
|
||||
|
||||
Assert.Empty(store.LogEntries);
|
||||
}
|
||||
@@ -259,9 +288,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var history = await service.GetSessionHistoryForGmAsync(sessionId, gmId);
|
||||
var history = await service.GetSessionHistoryForCurrentUserAsync(sessionId);
|
||||
|
||||
Assert.NotNull(history);
|
||||
Assert.Empty(history);
|
||||
@@ -281,9 +311,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var history = await service.GetSessionHistoryForGmAsync(sessionId, 1001L);
|
||||
var history = await service.GetSessionHistoryForCurrentUserAsync(sessionId);
|
||||
|
||||
Assert.Null(history);
|
||||
}
|
||||
@@ -303,9 +334,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 3, 1)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.PromoteWaitlistedPlayerForGmAsync(sessionId, gmId);
|
||||
await service.PromoteWaitlistedPlayerForCurrentUserAsync(sessionId);
|
||||
|
||||
Assert.True(store.PromoteCalled);
|
||||
Assert.Equal(groupId, store.LastPromotedGroupId);
|
||||
@@ -326,9 +358,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.UpdateBatchDetailsForGmAsync(batchId, 1001L, "Updated", "https://example.test/b");
|
||||
var action = () => service.UpdateBatchDetailsForCurrentUserAsync(batchId, "Updated", "https://example.test/b");
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.UpdateBatchDetailsCalled);
|
||||
@@ -349,9 +382,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.UpdateBatchDetailsForGmAsync(batchId, gmId, "Updated", "https://example.test/b");
|
||||
await service.UpdateBatchDetailsForCurrentUserAsync(batchId, "Updated", "https://example.test/b");
|
||||
|
||||
Assert.True(store.UpdateBatchDetailsCalled);
|
||||
Assert.Equal(batchId, store.LastUpdatedBatchId);
|
||||
@@ -380,9 +414,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, coGmId, GroupManagerRole.CoGm)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(coGmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.UpdateBatchDetailsForGmAsync(batchId, coGmId, "Updated", "https://example.test/b");
|
||||
await service.UpdateBatchDetailsForCurrentUserAsync(batchId, "Updated", "https://example.test/b");
|
||||
|
||||
Assert.True(store.UpdateBatchDetailsCalled);
|
||||
Assert.Equal(batchId, store.LastUpdatedBatchId);
|
||||
@@ -400,9 +435,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, 42, "Alpha", ownerId)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(ownerId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.AddCoGmForOwnerAsync(groupId, ownerId, coGmId, "Assistant GM", "assistant");
|
||||
await service.AddCoGmForOwnerAsync(groupId, "Telegram", coGmId.ToString(), "Assistant GM", "assistant");
|
||||
|
||||
Assert.True(store.AddCoGmCalled);
|
||||
Assert.Equal(groupId, store.LastAddedCoGmGroupId);
|
||||
@@ -427,9 +463,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, coGmId, GroupManagerRole.CoGm)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(coGmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.AddCoGmForOwnerAsync(groupId, coGmId, newCoGmId, "Second Assistant", null);
|
||||
var action = () => service.AddCoGmForOwnerAsync(groupId, "Telegram", newCoGmId.ToString(), "Second Assistant", null);
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.AddCoGmCalled);
|
||||
@@ -450,9 +487,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, coGmId, GroupManagerRole.CoGm)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(ownerId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.RemoveCoGmForOwnerAsync(groupId, ownerId, coGmId);
|
||||
await service.RemoveCoGmForOwnerAsync(groupId, "Telegram", coGmId.ToString());
|
||||
|
||||
Assert.True(store.RemoveCoGmCalled);
|
||||
Assert.Equal(groupId, store.LastRemovedCoGmGroupId);
|
||||
@@ -473,9 +511,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.UpdateBatchNotificationModeForGmAsync(batchId, 1001L, SessionNotificationMode.GroupOnly);
|
||||
var action = () => service.UpdateBatchNotificationModeForCurrentUserAsync(batchId, SessionNotificationMode.GroupOnly);
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.UpdateBatchNotificationModeCalled);
|
||||
@@ -496,9 +535,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.UpdateBatchNotificationModeForGmAsync(batchId, gmId, SessionNotificationMode.GroupOnly);
|
||||
await service.UpdateBatchNotificationModeForCurrentUserAsync(batchId, SessionNotificationMode.GroupOnly);
|
||||
|
||||
Assert.True(store.UpdateBatchNotificationModeCalled);
|
||||
Assert.Equal(batchId, store.LastUpdatedNotificationBatchId);
|
||||
@@ -521,9 +561,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.RescheduleBatchForGmAsync(batchId, gmId, DateTime.UtcNow.AddDays(7), intervalDays: 0);
|
||||
var action = () => service.RescheduleBatchForCurrentUserAsync(batchId, DateTime.UtcNow.AddDays(7), intervalDays: 0);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(action);
|
||||
Assert.False(store.RescheduleBatchCalled);
|
||||
@@ -545,9 +586,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.RescheduleBatchForGmAsync(batchId, gmId, firstScheduledAt, intervalDays: 14);
|
||||
await service.RescheduleBatchForCurrentUserAsync(batchId, firstScheduledAt, intervalDays: 14);
|
||||
|
||||
Assert.True(store.RescheduleBatchCalled);
|
||||
Assert.Equal(batchId, store.LastRescheduledBatchId);
|
||||
@@ -571,9 +613,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.CloneBatchForGmAsync(batchId, gmId, BatchCloneInterval.NextWeek);
|
||||
await service.CloneBatchForCurrentUserAsync(batchId, BatchCloneInterval.NextWeek);
|
||||
|
||||
Assert.True(store.CloneBatchCalled);
|
||||
Assert.Equal(batchId, store.LastClonedBatchId);
|
||||
@@ -591,11 +634,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, 42, "Alpha", gmId)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.CreateCampaignTemplateForGmAsync(
|
||||
await service.CreateCampaignTemplateForCurrentUserAsync(
|
||||
groupId,
|
||||
gmId,
|
||||
new CreateCampaignTemplateRequest(
|
||||
" Weekly arc ",
|
||||
" Kingmaker ",
|
||||
@@ -626,11 +669,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, 42, "Alpha", gmId)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.CreateCampaignTemplateForGmAsync(
|
||||
await service.CreateCampaignTemplateForCurrentUserAsync(
|
||||
groupId,
|
||||
gmId,
|
||||
new CreateCampaignTemplateRequest(
|
||||
"Open table",
|
||||
"West Marches",
|
||||
@@ -653,11 +696,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(groupId, 42, "Alpha", 2002L)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.CreateCampaignTemplateForGmAsync(
|
||||
var action = () => service.CreateCampaignTemplateForCurrentUserAsync(
|
||||
groupId,
|
||||
1001L,
|
||||
new CreateCampaignTemplateRequest(
|
||||
"Weekly arc",
|
||||
"Kingmaker",
|
||||
@@ -687,9 +730,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(templateId, groupId, "Weekly arc", "Kingmaker", "https://example.test/kingmaker", 6, 7, 5, SessionNotificationModeExtensions.GroupOnlyValue, DateTime.UtcNow, DateTime.UtcNow)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor(gmId.ToString());
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
await service.CreateBatchFromCampaignTemplateForGmAsync(templateId, gmId, firstScheduledAt);
|
||||
await service.CreateBatchFromCampaignTemplateForCurrentUserAsync(templateId, firstScheduledAt);
|
||||
|
||||
Assert.True(store.CreateBatchFromTemplateCalled);
|
||||
Assert.Equal(templateId, store.LastCreatedBatchTemplateId);
|
||||
@@ -711,9 +755,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
[
|
||||
new(templateId, groupId, "Weekly arc", "Kingmaker", "https://example.test/kingmaker", 6, 7, 5, SessionNotificationModeExtensions.GroupOnlyValue, DateTime.UtcNow, DateTime.UtcNow)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
var accessor = CreateAccessor("1001");
|
||||
var service = new AuthorizedSessionService(store, accessor);
|
||||
|
||||
var action = () => service.CreateBatchFromCampaignTemplateForGmAsync(templateId, 1001L, DateTime.UtcNow.AddDays(3));
|
||||
var action = () => service.CreateBatchFromCampaignTemplateForCurrentUserAsync(templateId, DateTime.UtcNow.AddDays(3));
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.CreateBatchFromTemplateCalled);
|
||||
@@ -1019,7 +1064,7 @@ public sealed class AuthorizedSessionServiceTests
|
||||
|
||||
public Task LogSessionChangeAsync(Guid sessionId, long actorTelegramId, string actorName, string changeType, string? oldValue, string? newValue)
|
||||
{
|
||||
var entry = new SessionAuditLogEntry(Guid.NewGuid(), sessionId, actorTelegramId, actorName, changeType, oldValue, newValue, DateTime.UtcNow);
|
||||
var entry = new SessionAuditLogEntry(Guid.NewGuid(), sessionId, actorTelegramId.ToString(), actorName, changeType, oldValue, newValue, DateTime.UtcNow);
|
||||
LogEntries.Add(entry);
|
||||
LastLogSessionId = sessionId;
|
||||
LastLogActorTelegramId = actorTelegramId;
|
||||
@@ -1033,12 +1078,62 @@ public sealed class AuthorizedSessionServiceTests
|
||||
public Task<List<SessionAuditLogEntry>> GetSessionHistoryAsync(Guid sessionId) =>
|
||||
Task.FromResult(LogEntries.Where(e => e.SessionId == sessionId).OrderByDescending(e => e.ChangedAt).ToList());
|
||||
|
||||
public Task<List<WebGameGroup>> GetGroupsForUserAsync(string platform, string externalUserId) =>
|
||||
Task.FromResult(groupsById.Values.Where(group => IsManager(group.Id, externalUserId)).ToList());
|
||||
|
||||
public Task<bool> IsGroupManagerAsync(Guid groupId, string platform, string externalUserId) =>
|
||||
Task.FromResult(IsManager(groupId, externalUserId));
|
||||
|
||||
public Task<bool> IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId) =>
|
||||
Task.FromResult(IsOwner(groupId, externalUserId));
|
||||
|
||||
public Task AddGroupCoGmAsync(Guid groupId, string ownerPlatform, string ownerExternalUserId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername)
|
||||
{
|
||||
AddCoGmCalled = true;
|
||||
LastAddedCoGmGroupId = groupId;
|
||||
LastAddedCoGmTelegramId = long.TryParse(coGmExternalUserId, out var id) ? id : null;
|
||||
LastAddedCoGmDisplayName = displayName;
|
||||
LastAddedCoGmUsername = externalUsername;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveGroupCoGmAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId)
|
||||
{
|
||||
RemoveCoGmCalled = true;
|
||||
LastRemovedCoGmGroupId = groupId;
|
||||
LastRemovedCoGmTelegramId = long.TryParse(coGmExternalUserId, out var id) ? id : null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task LogSessionChangeAsync(Guid sessionId, string actorExternalUserId, string actorName, string changeType, string? oldValue, string? newValue)
|
||||
{
|
||||
var entry = new SessionAuditLogEntry(Guid.NewGuid(), sessionId, actorExternalUserId, actorName, changeType, oldValue, newValue, DateTime.UtcNow);
|
||||
LogEntries.Add(entry);
|
||||
LastLogSessionId = sessionId;
|
||||
LastLogActorTelegramId = long.TryParse(actorExternalUserId, out var id) ? (long?)id : null;
|
||||
LastLogActorName = actorName;
|
||||
LastLogChangeType = changeType;
|
||||
LastLogOldValue = oldValue;
|
||||
LastLogNewValue = newValue;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
private bool IsManager(Guid groupId, long telegramId) =>
|
||||
IsOwner(groupId, telegramId) ||
|
||||
managers.Any(manager => manager.GroupId == groupId && manager.TelegramId == telegramId);
|
||||
|
||||
private bool IsOwner(Guid groupId, long telegramId) =>
|
||||
groupsById.TryGetValue(groupId, out var group) && group.GmTelegramId == telegramId;
|
||||
|
||||
private bool IsManager(Guid groupId, string externalUserId) =>
|
||||
IsOwner(groupId, externalUserId) ||
|
||||
managers.Any(manager => manager.GroupId == groupId && manager.TelegramId.ToString() == externalUserId);
|
||||
|
||||
private bool IsOwner(Guid groupId, string externalUserId) =>
|
||||
groupsById.TryGetValue(groupId, out var group) && group.GmTelegramId.ToString() == externalUserId;
|
||||
}
|
||||
|
||||
private sealed record FakeGroupManager(Guid GroupId, long TelegramId, GroupManagerRole Role);
|
||||
|
||||
Reference in New Issue
Block a user