Files
GmRelayBot/tests/GmRelay.Bot.Tests/Web/AuthorizedSessionServiceTests.cs
T
Toutsu 2529df4157
Deploy Telegram Bot / build-and-push (push) Successful in 3m51s
Deploy Telegram Bot / deploy (push) Successful in 11s
feat: support co-gm group delegation
2026-04-27 14:27:16 +03:00

673 lines
25 KiB
C#

using GmRelay.Web.Services;
using GmRelay.Shared.Domain;
namespace GmRelay.Bot.Tests.Web;
public sealed class AuthorizedSessionServiceTests
{
[Fact]
public async Task GetUpcomingSessionsForGmAsync_ReturnsSessions_WhenGroupBelongsToGm()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
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 sessions = await service.GetUpcomingSessionsForGmAsync(groupId, gmId);
Assert.NotNull(sessions);
Assert.Single(sessions);
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()
{
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", 2002L)
]);
var service = new AuthorizedSessionService(store);
var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, 1001L);
Assert.Null(sessions);
}
[Fact]
public async Task GetSessionForGmAsync_ReturnsSession_WhenSessionBelongsToOwnedGroup()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
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 session = await service.GetSessionForGmAsync(sessionId, gmId);
Assert.NotNull(session);
Assert.Equal(sessionId, session.Id);
}
[Fact]
public async Task GetSessionForGmAsync_ReturnsNull_WhenSessionBelongsToAnotherGm()
{
var groupId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", 2002L)
],
sessions:
[
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 session = await service.GetSessionForGmAsync(sessionId, 1001L);
Assert.Null(session);
}
[Fact]
public async Task UpdateSessionForGmAsync_Throws_WhenSessionBelongsToAnotherGm()
{
var groupId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", 2002L)
],
sessions:
[
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 action = () => service.UpdateSessionForGmAsync(sessionId, 1001L, "Updated", DateTime.UtcNow.AddDays(1), "https://example.test/b", 5);
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
Assert.False(store.UpdateCalled);
}
[Fact]
public async Task UpdateSessionForGmAsync_UpdatesOwnedSession()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var scheduledAt = DateTime.UtcNow.AddDays(1);
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
]);
var service = new AuthorizedSessionService(store);
await service.UpdateSessionForGmAsync(sessionId, gmId, "Updated", scheduledAt, "https://example.test/b", 5);
Assert.True(store.UpdateCalled);
Assert.Equal(groupId, store.LastUpdatedGroupId);
Assert.Equal(sessionId, store.LastUpdatedSessionId);
Assert.Equal("Updated", store.LastUpdatedTitle);
Assert.Equal(scheduledAt, store.LastUpdatedScheduledAt);
Assert.Equal("https://example.test/b", store.LastUpdatedJoinLink);
Assert.Equal(5, store.LastUpdatedMaxPlayers);
}
[Fact]
public async Task PromoteWaitlistedPlayerForGmAsync_PromotesOwnedSession()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 3, 1)
]);
var service = new AuthorizedSessionService(store);
await service.PromoteWaitlistedPlayerForGmAsync(sessionId, gmId);
Assert.True(store.PromoteCalled);
Assert.Equal(groupId, store.LastPromotedGroupId);
Assert.Equal(sessionId, store.LastPromotedSessionId);
}
[Fact]
public async Task UpdateBatchDetailsForGmAsync_Throws_WhenBatchBelongsToAnotherGm()
{
var batchId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", 2002L)
],
sessions:
[
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 action = () => service.UpdateBatchDetailsForGmAsync(batchId, 1001L, "Updated", "https://example.test/b");
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
Assert.False(store.UpdateBatchDetailsCalled);
}
[Fact]
public async Task UpdateBatchDetailsForGmAsync_UpdatesOwnedBatch()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var batchId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
]);
var service = new AuthorizedSessionService(store);
await service.UpdateBatchDetailsForGmAsync(batchId, gmId, "Updated", "https://example.test/b");
Assert.True(store.UpdateBatchDetailsCalled);
Assert.Equal(batchId, store.LastUpdatedBatchId);
Assert.Equal(groupId, store.LastUpdatedBatchGroupId);
Assert.Equal("Updated", store.LastUpdatedBatchTitle);
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()
{
var batchId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", 2002L)
],
sessions:
[
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 action = () => service.UpdateBatchNotificationModeForGmAsync(batchId, 1001L, SessionNotificationMode.GroupOnly);
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
Assert.False(store.UpdateBatchNotificationModeCalled);
}
[Fact]
public async Task UpdateBatchNotificationModeForGmAsync_UpdatesOwnedBatch()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var batchId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
]);
var service = new AuthorizedSessionService(store);
await service.UpdateBatchNotificationModeForGmAsync(batchId, gmId, SessionNotificationMode.GroupOnly);
Assert.True(store.UpdateBatchNotificationModeCalled);
Assert.Equal(batchId, store.LastUpdatedNotificationBatchId);
Assert.Equal(groupId, store.LastUpdatedNotificationGroupId);
Assert.Equal(SessionNotificationMode.GroupOnly, store.LastUpdatedNotificationMode);
}
[Fact]
public async Task RescheduleBatchForGmAsync_RejectsNonPositiveInterval()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var batchId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
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 action = () => service.RescheduleBatchForGmAsync(batchId, gmId, DateTime.UtcNow.AddDays(7), intervalDays: 0);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(action);
Assert.False(store.RescheduleBatchCalled);
}
[Fact]
public async Task RescheduleBatchForGmAsync_ReschedulesOwnedBatch()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var batchId = Guid.NewGuid();
var firstScheduledAt = DateTime.UtcNow.AddDays(7);
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
]);
var service = new AuthorizedSessionService(store);
await service.RescheduleBatchForGmAsync(batchId, gmId, firstScheduledAt, intervalDays: 14);
Assert.True(store.RescheduleBatchCalled);
Assert.Equal(batchId, store.LastRescheduledBatchId);
Assert.Equal(groupId, store.LastRescheduledBatchGroupId);
Assert.Equal(firstScheduledAt, store.LastRescheduledFirstScheduledAt);
Assert.Equal(14, store.LastRescheduledIntervalDays);
}
[Fact]
public async Task CloneBatchForGmAsync_ClonesOwnedBatch()
{
var gmId = 1001L;
var groupId = Guid.NewGuid();
var batchId = Guid.NewGuid();
var store = new FakeSessionStore(
groups:
[
new(groupId, 42, "Alpha", gmId)
],
sessions:
[
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
]);
var service = new AuthorizedSessionService(store);
await service.CloneBatchForGmAsync(batchId, gmId, BatchCloneInterval.NextWeek);
Assert.True(store.CloneBatchCalled);
Assert.Equal(batchId, store.LastClonedBatchId);
Assert.Equal(groupId, store.LastClonedBatchGroupId);
Assert.Equal(BatchCloneInterval.NextWeek, store.LastCloneInterval);
}
private sealed class FakeSessionStore(
IEnumerable<WebGameGroup>? groups = null,
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; }
public bool UpdateBatchDetailsCalled { get; private set; }
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; }
public DateTime? LastUpdatedScheduledAt { get; private set; }
public string? LastUpdatedJoinLink { get; private set; }
public int? LastUpdatedMaxPlayers { get; private set; }
public Guid? LastPromotedSessionId { get; private set; }
public Guid? LastPromotedGroupId { get; private set; }
public Guid? LastUpdatedBatchId { get; private set; }
public Guid? LastUpdatedBatchGroupId { get; private set; }
public string? LastUpdatedBatchTitle { get; private set; }
public string? LastUpdatedBatchJoinLink { get; private set; }
public Guid? LastUpdatedNotificationBatchId { get; private set; }
public Guid? LastUpdatedNotificationGroupId { get; private set; }
public SessionNotificationMode? LastUpdatedNotificationMode { get; private set; }
public Guid? LastRescheduledBatchId { get; private set; }
public Guid? LastRescheduledBatchGroupId { get; private set; }
public DateTime? LastRescheduledFirstScheduledAt { get; private set; }
public int? LastRescheduledIntervalDays { get; private set; }
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 => IsManager(group.Id, gmId)).ToList());
public Task<WebGameGroup?> GetGroupAsync(Guid groupId)
{
groupsById.TryGetValue(groupId, out var group);
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());
public Task<WebSession?> GetSessionAsync(Guid sessionId)
{
sessionsById.TryGetValue(sessionId, out var session);
return Task.FromResult(session);
}
public Task<WebSessionBatch?> GetBatchAsync(Guid batchId)
{
var batchSessions = sessionsById.Values
.Where(session => session.BatchId == batchId)
.OrderBy(session => session.ScheduledAt)
.ToList();
if (batchSessions.Count == 0)
{
return Task.FromResult<WebSessionBatch?>(null);
}
var firstSession = batchSessions[0];
return Task.FromResult<WebSessionBatch?>(new(
batchId,
firstSession.GroupId,
firstSession.Title,
firstSession.JoinLink,
firstSession.ScheduledAt,
batchSessions[^1].ScheduledAt,
batchSessions.Count));
}
public Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers)
{
UpdateCalled = true;
LastUpdatedSessionId = sessionId;
LastUpdatedGroupId = groupId;
LastUpdatedTitle = title;
LastUpdatedScheduledAt = scheduledAt;
LastUpdatedJoinLink = joinLink;
LastUpdatedMaxPlayers = maxPlayers;
return Task.CompletedTask;
}
public Task PromoteWaitlistedPlayerAsync(Guid sessionId, Guid groupId)
{
PromoteCalled = true;
LastPromotedSessionId = sessionId;
LastPromotedGroupId = groupId;
return Task.CompletedTask;
}
public Task UpdateBatchDetailsAsync(Guid batchId, Guid groupId, string title, string joinLink)
{
UpdateBatchDetailsCalled = true;
LastUpdatedBatchId = batchId;
LastUpdatedBatchGroupId = groupId;
LastUpdatedBatchTitle = title;
LastUpdatedBatchJoinLink = joinLink;
return Task.CompletedTask;
}
public Task UpdateBatchNotificationModeAsync(Guid batchId, Guid groupId, SessionNotificationMode notificationMode)
{
UpdateBatchNotificationModeCalled = true;
LastUpdatedNotificationBatchId = batchId;
LastUpdatedNotificationGroupId = groupId;
LastUpdatedNotificationMode = notificationMode;
return Task.CompletedTask;
}
public Task RescheduleBatchAsync(Guid batchId, Guid groupId, DateTime firstScheduledAt, int intervalDays)
{
RescheduleBatchCalled = true;
LastRescheduledBatchId = batchId;
LastRescheduledBatchGroupId = groupId;
LastRescheduledFirstScheduledAt = firstScheduledAt;
LastRescheduledIntervalDays = intervalDays;
return Task.CompletedTask;
}
public Task<WebSessionBatch> CloneBatchAsync(Guid batchId, Guid groupId, BatchCloneInterval interval)
{
CloneBatchCalled = true;
LastClonedBatchId = batchId;
LastClonedBatchGroupId = groupId;
LastCloneInterval = interval;
return Task.FromResult(new WebSessionBatch(
Guid.NewGuid(),
groupId,
"Session A",
"https://example.test/a",
DateTime.UtcNow.AddDays(7),
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);
}