feat: add campaign templates and recurring schedules
This commit is contained in:
@@ -460,13 +460,153 @@ public sealed class AuthorizedSessionServiceTests
|
||||
Assert.Equal(BatchCloneInterval.NextWeek, store.LastCloneInterval);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCampaignTemplateForGmAsync_CreatesTemplate_WhenGroupBelongsToGm()
|
||||
{
|
||||
var gmId = 1001L;
|
||||
var groupId = Guid.NewGuid();
|
||||
var store = new FakeSessionStore(
|
||||
groups:
|
||||
[
|
||||
new(groupId, 42, "Alpha", gmId)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
|
||||
await service.CreateCampaignTemplateForGmAsync(
|
||||
groupId,
|
||||
gmId,
|
||||
new CreateCampaignTemplateRequest(
|
||||
" Weekly arc ",
|
||||
" Kingmaker ",
|
||||
" https://example.test/kingmaker ",
|
||||
SessionCount: 6,
|
||||
IntervalDays: 7,
|
||||
MaxPlayers: 5,
|
||||
SessionNotificationMode.GroupOnly));
|
||||
|
||||
Assert.True(store.CreateCampaignTemplateCalled);
|
||||
Assert.Equal(groupId, store.LastCreatedCampaignTemplateGroupId);
|
||||
Assert.Equal("Weekly arc", store.LastCreatedCampaignTemplateRequest?.Name);
|
||||
Assert.Equal("Kingmaker", store.LastCreatedCampaignTemplateRequest?.Title);
|
||||
Assert.Equal("https://example.test/kingmaker", store.LastCreatedCampaignTemplateRequest?.JoinLink);
|
||||
Assert.Equal(6, store.LastCreatedCampaignTemplateRequest?.SessionCount);
|
||||
Assert.Equal(7, store.LastCreatedCampaignTemplateRequest?.IntervalDays);
|
||||
Assert.Equal(5, store.LastCreatedCampaignTemplateRequest?.MaxPlayers);
|
||||
Assert.Equal(SessionNotificationMode.GroupOnly, store.LastCreatedCampaignTemplateRequest?.NotificationMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCampaignTemplateForGmAsync_AllowsNoSeatLimit()
|
||||
{
|
||||
var gmId = 1001L;
|
||||
var groupId = Guid.NewGuid();
|
||||
var store = new FakeSessionStore(
|
||||
groups:
|
||||
[
|
||||
new(groupId, 42, "Alpha", gmId)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
|
||||
await service.CreateCampaignTemplateForGmAsync(
|
||||
groupId,
|
||||
gmId,
|
||||
new CreateCampaignTemplateRequest(
|
||||
"Open table",
|
||||
"West Marches",
|
||||
"https://example.test/west",
|
||||
8,
|
||||
7,
|
||||
null,
|
||||
SessionNotificationMode.GroupAndDirect));
|
||||
|
||||
Assert.True(store.CreateCampaignTemplateCalled);
|
||||
Assert.Null(store.LastCreatedCampaignTemplateRequest?.MaxPlayers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCampaignTemplateForGmAsync_Throws_WhenGroupBelongsToAnotherGm()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var store = new FakeSessionStore(
|
||||
groups:
|
||||
[
|
||||
new(groupId, 42, "Alpha", 2002L)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
|
||||
var action = () => service.CreateCampaignTemplateForGmAsync(
|
||||
groupId,
|
||||
1001L,
|
||||
new CreateCampaignTemplateRequest(
|
||||
"Weekly arc",
|
||||
"Kingmaker",
|
||||
"https://example.test/kingmaker",
|
||||
SessionCount: 6,
|
||||
IntervalDays: 7,
|
||||
MaxPlayers: 5,
|
||||
SessionNotificationMode.GroupOnly));
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.CreateCampaignTemplateCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBatchFromCampaignTemplateForGmAsync_CreatesBatch_WhenTemplateGroupBelongsToGm()
|
||||
{
|
||||
var gmId = 1001L;
|
||||
var groupId = Guid.NewGuid();
|
||||
var templateId = Guid.NewGuid();
|
||||
var firstScheduledAt = DateTime.UtcNow.AddDays(3);
|
||||
var store = new FakeSessionStore(
|
||||
groups:
|
||||
[
|
||||
new(groupId, 42, "Alpha", gmId)
|
||||
],
|
||||
templates:
|
||||
[
|
||||
new(templateId, groupId, "Weekly arc", "Kingmaker", "https://example.test/kingmaker", 6, 7, 5, SessionNotificationModeExtensions.GroupOnlyValue, DateTime.UtcNow, DateTime.UtcNow)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
|
||||
await service.CreateBatchFromCampaignTemplateForGmAsync(templateId, gmId, firstScheduledAt);
|
||||
|
||||
Assert.True(store.CreateBatchFromTemplateCalled);
|
||||
Assert.Equal(templateId, store.LastCreatedBatchTemplateId);
|
||||
Assert.Equal(groupId, store.LastCreatedBatchTemplateGroupId);
|
||||
Assert.Equal(firstScheduledAt, store.LastCreatedBatchFirstScheduledAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBatchFromCampaignTemplateForGmAsync_Throws_WhenTemplateGroupBelongsToAnotherGm()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var templateId = Guid.NewGuid();
|
||||
var store = new FakeSessionStore(
|
||||
groups:
|
||||
[
|
||||
new(groupId, 42, "Alpha", 2002L)
|
||||
],
|
||||
templates:
|
||||
[
|
||||
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 action = () => service.CreateBatchFromCampaignTemplateForGmAsync(templateId, 1001L, DateTime.UtcNow.AddDays(3));
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.CreateBatchFromTemplateCalled);
|
||||
}
|
||||
|
||||
private sealed class FakeSessionStore(
|
||||
IEnumerable<WebGameGroup>? groups = null,
|
||||
IEnumerable<WebSession>? sessions = null,
|
||||
IEnumerable<FakeGroupManager>? managers = null) : ISessionStore
|
||||
IEnumerable<FakeGroupManager>? managers = null,
|
||||
IEnumerable<WebCampaignTemplate>? templates = 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 Dictionary<Guid, WebCampaignTemplate> templatesById = templates?.ToDictionary(template => template.Id) ?? [];
|
||||
private readonly List<FakeGroupManager> managers = managers?.ToList() ?? [];
|
||||
|
||||
public bool UpdateCalled { get; private set; }
|
||||
@@ -475,6 +615,9 @@ public sealed class AuthorizedSessionServiceTests
|
||||
public bool UpdateBatchNotificationModeCalled { get; private set; }
|
||||
public bool RescheduleBatchCalled { get; private set; }
|
||||
public bool CloneBatchCalled { get; private set; }
|
||||
public bool CreateCampaignTemplateCalled { get; private set; }
|
||||
public bool DeleteCampaignTemplateCalled { get; private set; }
|
||||
public bool CreateBatchFromTemplateCalled { get; private set; }
|
||||
public bool AddCoGmCalled { get; private set; }
|
||||
public bool RemoveCoGmCalled { get; private set; }
|
||||
public Guid? LastUpdatedSessionId { get; private set; }
|
||||
@@ -499,6 +642,13 @@ public sealed class AuthorizedSessionServiceTests
|
||||
public Guid? LastClonedBatchId { get; private set; }
|
||||
public Guid? LastClonedBatchGroupId { get; private set; }
|
||||
public BatchCloneInterval? LastCloneInterval { get; private set; }
|
||||
public Guid? LastCreatedCampaignTemplateGroupId { get; private set; }
|
||||
public CreateCampaignTemplateRequest? LastCreatedCampaignTemplateRequest { get; private set; }
|
||||
public Guid? LastDeletedCampaignTemplateId { get; private set; }
|
||||
public Guid? LastDeletedCampaignTemplateGroupId { get; private set; }
|
||||
public Guid? LastCreatedBatchTemplateId { get; private set; }
|
||||
public Guid? LastCreatedBatchTemplateGroupId { get; private set; }
|
||||
public DateTime? LastCreatedBatchFirstScheduledAt { get; private set; }
|
||||
public Guid? LastAddedCoGmGroupId { get; private set; }
|
||||
public long? LastAddedCoGmTelegramId { get; private set; }
|
||||
public string? LastAddedCoGmDisplayName { get; private set; }
|
||||
@@ -642,6 +792,66 @@ public sealed class AuthorizedSessionServiceTests
|
||||
1));
|
||||
}
|
||||
|
||||
public Task<List<WebCampaignTemplate>> GetCampaignTemplatesAsync(Guid groupId) =>
|
||||
Task.FromResult(templatesById.Values.Where(template => template.GroupId == groupId).ToList());
|
||||
|
||||
public Task<WebCampaignTemplate?> GetCampaignTemplateAsync(Guid templateId)
|
||||
{
|
||||
templatesById.TryGetValue(templateId, out var template);
|
||||
return Task.FromResult(template);
|
||||
}
|
||||
|
||||
public Task<WebCampaignTemplate> CreateCampaignTemplateAsync(Guid groupId, CreateCampaignTemplateRequest request)
|
||||
{
|
||||
CreateCampaignTemplateCalled = true;
|
||||
LastCreatedCampaignTemplateGroupId = groupId;
|
||||
LastCreatedCampaignTemplateRequest = request;
|
||||
|
||||
var template = new WebCampaignTemplate(
|
||||
Guid.NewGuid(),
|
||||
groupId,
|
||||
request.Name,
|
||||
request.Title,
|
||||
request.JoinLink,
|
||||
request.SessionCount,
|
||||
request.IntervalDays,
|
||||
request.MaxPlayers,
|
||||
request.NotificationMode.ToDatabaseValue(),
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow);
|
||||
|
||||
templatesById[template.Id] = template;
|
||||
return Task.FromResult(template);
|
||||
}
|
||||
|
||||
public Task DeleteCampaignTemplateAsync(Guid templateId, Guid groupId)
|
||||
{
|
||||
DeleteCampaignTemplateCalled = true;
|
||||
LastDeletedCampaignTemplateId = templateId;
|
||||
LastDeletedCampaignTemplateGroupId = groupId;
|
||||
templatesById.Remove(templateId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<WebSessionBatch> CreateBatchFromTemplateAsync(Guid templateId, Guid groupId, DateTime firstScheduledAt)
|
||||
{
|
||||
CreateBatchFromTemplateCalled = true;
|
||||
LastCreatedBatchTemplateId = templateId;
|
||||
LastCreatedBatchTemplateGroupId = groupId;
|
||||
LastCreatedBatchFirstScheduledAt = firstScheduledAt;
|
||||
|
||||
var template = templatesById[templateId];
|
||||
return Task.FromResult(new WebSessionBatch(
|
||||
Guid.NewGuid(),
|
||||
groupId,
|
||||
template.Title,
|
||||
template.JoinLink,
|
||||
firstScheduledAt,
|
||||
firstScheduledAt.AddDays(template.IntervalDays * (template.SessionCount - 1)),
|
||||
template.SessionCount,
|
||||
template.NotificationMode));
|
||||
}
|
||||
|
||||
public Task AddGroupCoGmAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId, string displayName, string? telegramUsername)
|
||||
{
|
||||
AddCoGmCalled = true;
|
||||
|
||||
@@ -37,6 +37,35 @@ public sealed class BatchSchedulePlannerTests
|
||||
Assert.Throws<ArgumentOutOfRangeException>(action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRecurringSchedule_CreatesFixedIntervalScheduleFromFirstDate()
|
||||
{
|
||||
var firstScheduledAt = new DateTime(2026, 5, 4, 16, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var result = BatchSchedulePlanner.BuildRecurringSchedule(firstScheduledAt, sessionCount: 3, intervalDays: 14);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
firstScheduledAt,
|
||||
firstScheduledAt.AddDays(14),
|
||||
firstScheduledAt.AddDays(28)
|
||||
],
|
||||
result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 7)]
|
||||
[InlineData(53, 7)]
|
||||
[InlineData(3, 0)]
|
||||
public void BuildRecurringSchedule_RejectsInvalidTemplateShape(int sessionCount, int intervalDays)
|
||||
{
|
||||
var firstScheduledAt = new DateTime(2026, 5, 4, 16, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var action = () => BatchSchedulePlanner.BuildRecurringSchedule(firstScheduledAt, sessionCount, intervalDays);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(action);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(BatchCloneInterval.NextWeek, 2026, 5, 8)]
|
||||
[InlineData(BatchCloneInterval.NextMonth, 2026, 6, 1)]
|
||||
|
||||
Reference in New Issue
Block a user