feat: support co-gm group delegation
Deploy Telegram Bot / build-and-push (push) Successful in 3m51s
Deploy Telegram Bot / deploy (push) Successful in 11s

This commit is contained in:
2026-04-27 14:27:16 +03:00
parent a8f2b10956
commit 2529df4157
20 changed files with 805 additions and 63 deletions
@@ -28,6 +28,34 @@ public sealed class AuthorizedSessionServiceTests
Assert.Equal("Session A", sessions[0].Title);
}
[Fact]
public async Task GetUpcomingSessionsForGmAsync_ReturnsSessions_WhenUserIsCoGm()
{
var ownerId = 1001L;
var coGmId = 2002L;
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", ownerId)
],
sessions:
[
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
],
managers:
[
new(groupId, coGmId, GroupManagerRole.CoGm)
]);
var service = new AuthorizedSessionService(store);
var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, coGmId);
Assert.NotNull(sessions);
Assert.Single(sessions);
Assert.Equal("Session A", sessions[0].Title);
}
[Fact]
public async Task GetUpcomingSessionsForGmAsync_ReturnsNull_WhenGroupBelongsToAnotherGm()
{
@@ -211,6 +239,105 @@ public sealed class AuthorizedSessionServiceTests
Assert.Equal("https://example.test/b", store.LastUpdatedBatchJoinLink);
}
[Fact]
public async Task UpdateBatchDetailsForGmAsync_UpdatesBatch_WhenUserIsCoGm()
{
var ownerId = 1001L;
var coGmId = 2002L;
var groupId = Guid.NewGuid();
var batchId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", ownerId)
],
sessions:
[
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
],
managers:
[
new(groupId, coGmId, GroupManagerRole.CoGm)
]);
var service = new AuthorizedSessionService(store);
await service.UpdateBatchDetailsForGmAsync(batchId, coGmId, "Updated", "https://example.test/b");
Assert.True(store.UpdateBatchDetailsCalled);
Assert.Equal(batchId, store.LastUpdatedBatchId);
Assert.Equal(groupId, store.LastUpdatedBatchGroupId);
}
[Fact]
public async Task AddCoGmForOwnerAsync_AddsCoGm_WhenUserIsOwner()
{
var ownerId = 1001L;
var coGmId = 2002L;
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", ownerId)
]);
var service = new AuthorizedSessionService(store);
await service.AddCoGmForOwnerAsync(groupId, ownerId, coGmId, "Assistant GM", "assistant");
Assert.True(store.AddCoGmCalled);
Assert.Equal(groupId, store.LastAddedCoGmGroupId);
Assert.Equal(coGmId, store.LastAddedCoGmTelegramId);
Assert.Equal("Assistant GM", store.LastAddedCoGmDisplayName);
Assert.Equal("assistant", store.LastAddedCoGmUsername);
}
[Fact]
public async Task AddCoGmForOwnerAsync_Throws_WhenUserIsCoGm()
{
var ownerId = 1001L;
var coGmId = 2002L;
var newCoGmId = 3003L;
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", ownerId)
],
managers:
[
new(groupId, coGmId, GroupManagerRole.CoGm)
]);
var service = new AuthorizedSessionService(store);
var action = () => service.AddCoGmForOwnerAsync(groupId, coGmId, newCoGmId, "Second Assistant", null);
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
Assert.False(store.AddCoGmCalled);
}
[Fact]
public async Task RemoveCoGmForOwnerAsync_RemovesCoGm_WhenUserIsOwner()
{
var ownerId = 1001L;
var coGmId = 2002L;
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", ownerId)
],
managers:
[
new(groupId, coGmId, GroupManagerRole.CoGm)
]);
var service = new AuthorizedSessionService(store);
await service.RemoveCoGmForOwnerAsync(groupId, ownerId, coGmId);
Assert.True(store.RemoveCoGmCalled);
Assert.Equal(groupId, store.LastRemovedCoGmGroupId);
Assert.Equal(coGmId, store.LastRemovedCoGmTelegramId);
}
[Fact]
public async Task UpdateBatchNotificationModeForGmAsync_Throws_WhenBatchBelongsToAnotherGm()
{
@@ -335,10 +462,12 @@ public sealed class AuthorizedSessionServiceTests
private sealed class FakeSessionStore(
IEnumerable<WebGameGroup>? groups = null,
IEnumerable<WebSession>? sessions = null) : ISessionStore
IEnumerable<WebSession>? sessions = null,
IEnumerable<FakeGroupManager>? managers = null) : ISessionStore
{
private readonly Dictionary<Guid, WebGameGroup> groupsById = groups?.ToDictionary(group => group.Id) ?? [];
private readonly Dictionary<Guid, WebSession> sessionsById = sessions?.ToDictionary(session => session.Id) ?? [];
private readonly List<FakeGroupManager> managers = managers?.ToList() ?? [];
public bool UpdateCalled { get; private set; }
public bool PromoteCalled { get; private set; }
@@ -346,6 +475,8 @@ public sealed class AuthorizedSessionServiceTests
public bool UpdateBatchNotificationModeCalled { get; private set; }
public bool RescheduleBatchCalled { get; private set; }
public bool CloneBatchCalled { get; private set; }
public bool AddCoGmCalled { get; private set; }
public bool RemoveCoGmCalled { get; private set; }
public Guid? LastUpdatedSessionId { get; private set; }
public Guid? LastUpdatedGroupId { get; private set; }
public string? LastUpdatedTitle { get; private set; }
@@ -368,9 +499,15 @@ public sealed class AuthorizedSessionServiceTests
public Guid? LastClonedBatchId { get; private set; }
public Guid? LastClonedBatchGroupId { get; private set; }
public BatchCloneInterval? LastCloneInterval { get; private set; }
public Guid? LastAddedCoGmGroupId { get; private set; }
public long? LastAddedCoGmTelegramId { get; private set; }
public string? LastAddedCoGmDisplayName { get; private set; }
public string? LastAddedCoGmUsername { get; private set; }
public Guid? LastRemovedCoGmGroupId { get; private set; }
public long? LastRemovedCoGmTelegramId { get; private set; }
public Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId) =>
Task.FromResult(groupsById.Values.Where(group => group.GmTelegramId == gmId).ToList());
Task.FromResult(groupsById.Values.Where(group => IsManager(group.Id, gmId)).ToList());
public Task<WebGameGroup?> GetGroupAsync(Guid groupId)
{
@@ -378,6 +515,36 @@ public sealed class AuthorizedSessionServiceTests
return Task.FromResult(group);
}
public Task<bool> IsGroupManagerAsync(Guid groupId, long telegramId) =>
Task.FromResult(IsManager(groupId, telegramId));
public Task<bool> IsGroupOwnerAsync(Guid groupId, long telegramId) =>
Task.FromResult(IsOwner(groupId, telegramId));
public Task<List<WebGroupManager>> GetGroupManagersAsync(Guid groupId)
{
if (!groupsById.TryGetValue(groupId, out var group))
{
return Task.FromResult(new List<WebGroupManager>());
}
var result = new List<WebGroupManager>
{
new(group.GmTelegramId, "Owner GM", null, GroupManagerRoleExtensions.OwnerValue, DateTime.UtcNow)
};
result.AddRange(managers
.Where(manager => manager.GroupId == groupId)
.Select(manager => new WebGroupManager(
manager.TelegramId,
$"Co-GM {manager.TelegramId}",
null,
manager.Role.ToDatabaseValue(),
DateTime.UtcNow)));
return Task.FromResult(result);
}
public Task<List<WebSession>> GetUpcomingSessionsAsync(Guid groupId) =>
Task.FromResult(sessionsById.Values.Where(session => session.GroupId == groupId).ToList());
@@ -474,5 +641,32 @@ public sealed class AuthorizedSessionServiceTests
DateTime.UtcNow.AddDays(7),
1));
}
public Task AddGroupCoGmAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId, string displayName, string? telegramUsername)
{
AddCoGmCalled = true;
LastAddedCoGmGroupId = groupId;
LastAddedCoGmTelegramId = coGmTelegramId;
LastAddedCoGmDisplayName = displayName;
LastAddedCoGmUsername = telegramUsername;
return Task.CompletedTask;
}
public Task RemoveGroupCoGmAsync(Guid groupId, long coGmTelegramId)
{
RemoveCoGmCalled = true;
LastRemovedCoGmGroupId = groupId;
LastRemovedCoGmTelegramId = coGmTelegramId;
return 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 sealed record FakeGroupManager(Guid GroupId, long TelegramId, GroupManagerRole Role);
}