Enhanced the group management system by introducing dynamic group capabilities, allowing for the exclusion of elements from dynamic groups. Updated the Group and GroupItem models to support a new GroupMode, enabling the distinction between static and dynamic groups. Implemented API endpoints for excluding elements and updated related services to handle group composition based on the selected mode. Improved the frontend to accommodate these changes, including new API functions and UI components for managing exclusions and group modes, thereby enhancing the overall user experience in group management.
192 lines
7.3 KiB
C#
192 lines
7.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using TeleWave.Application.Programming.Groups;
|
|
using TeleWave.Application.Programming.Groups.Suggest;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Programming;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Programming;
|
|
|
|
/// <summary>
|
|
/// Предложения групп: разбор библиотеки должен быть объяснимым — что показали, то и создастся.
|
|
/// </summary>
|
|
public class GroupSuggestionsTests
|
|
{
|
|
[Fact]
|
|
public async Task SuggestsGenre_WhenAtLeastTwoShowsShareIt()
|
|
{
|
|
var fixture = new TestDb();
|
|
var cartoons = Genre.Create("Мультфильм", "animation");
|
|
var drama = Genre.Create("Драма", "drama");
|
|
var first = WithGenre(Show.Create("A", ShowKind.Series), cartoons);
|
|
var second = WithGenre(Show.Create("B", ShowKind.Series), cartoons);
|
|
// Один драматический — на группу не тянет.
|
|
var lonely = WithGenre(Show.Create("C", ShowKind.Single), drama);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Genres.AddRange(cartoons, drama);
|
|
seed.Shows.AddRange(first, second, lonely);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var suggestions = await BuildAsync(fixture);
|
|
|
|
var genre = Assert.Single(suggestions, s => s.Kind == GroupSuggestionKind.Genre);
|
|
Assert.Equal("Мультфильм", genre.Name);
|
|
Assert.Equal(2, genre.ShowIds.Count);
|
|
Assert.Equal([cartoons.Id], genre.Filter!.GenreIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SuggestsBigSeries_Separately()
|
|
{
|
|
var fixture = new TestDb();
|
|
var big = Show.Create("Гравити Фолз", ShowKind.Series);
|
|
for (var i = 0; i < 20; i++)
|
|
big.AddEpisode(Guid.NewGuid());
|
|
var small = Show.Create("Короткое", ShowKind.Series);
|
|
small.AddEpisode(Guid.NewGuid());
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Shows.AddRange(big, small);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var suggestions = await BuildAsync(fixture);
|
|
|
|
var series = Assert.Single(suggestions, s => s.Kind == GroupSuggestionKind.BigSeries);
|
|
Assert.Equal("Гравити Фолз", series.Name);
|
|
Assert.Equal([big.Id], series.ShowIds);
|
|
// Правила у одиночного шоу нет: «именно это шоу» фильтром не выражается.
|
|
Assert.Null(series.Filter);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SkipsAudienceTier_WhenItCoversEverything()
|
|
{
|
|
// Если под «не строже PG» попадает вся библиотека, группа повторяет её — предлагать нечего.
|
|
var fixture = new TestDb();
|
|
var first = Show.Create("A", ShowKind.Series);
|
|
first.SetAudience(ShowAudience.G);
|
|
var second = Show.Create("B", ShowKind.Series);
|
|
second.SetAudience(ShowAudience.Pg);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Shows.AddRange(first, second);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var suggestions = await BuildAsync(fixture);
|
|
|
|
Assert.DoesNotContain(suggestions, s => s.Kind == GroupSuggestionKind.Audience);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SuggestsAudienceTier_WhenStricterContentExists()
|
|
{
|
|
var fixture = new TestDb();
|
|
var kids = Show.Create("A", ShowKind.Series);
|
|
kids.SetAudience(ShowAudience.G);
|
|
var adult = Show.Create("B", ShowKind.Series);
|
|
adult.SetAudience(ShowAudience.R);
|
|
var unrated = Show.Create("C", ShowKind.Series);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Shows.AddRange(kids, adult, unrated);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var suggestions = await BuildAsync(fixture);
|
|
|
|
var tier = suggestions.First(s => s.Kind == GroupSuggestionKind.Audience);
|
|
// Непроставленный рейтинг остаётся в выборке — как в правиле набора и в планировщике.
|
|
Assert.Equal(2, tier.ShowIds.Count);
|
|
Assert.Contains(kids.Id, tier.ShowIds);
|
|
Assert.Contains(unrated.Id, tier.ShowIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFromSuggestion_SavesFilterAndComposition()
|
|
{
|
|
var fixture = new TestDb();
|
|
var genre = Genre.Create("Мультфильм", "animation");
|
|
var first = WithGenre(Show.Create("A", ShowKind.Series), genre);
|
|
var second = WithGenre(Show.Create("B", ShowKind.Series), genre);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Genres.Add(genre);
|
|
seed.Shows.AddRange(first, second);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
await using var db = fixture.New();
|
|
var handler = new CreateGroupFromSuggestionCommandHandler(
|
|
db,
|
|
Builder(db),
|
|
GroupServices.Stats(db)
|
|
);
|
|
|
|
var created = await handler.Handle(
|
|
new CreateGroupFromSuggestionCommand($"genre:{genre.Id:N}"),
|
|
CancellationToken.None
|
|
);
|
|
Assert.True(created.IsSuccess);
|
|
await db.SaveChangesAsync(CancellationToken.None);
|
|
|
|
await using var check = fixture.New();
|
|
var group = check.Groups.Include(g => g.Items).Single();
|
|
Assert.Equal("Мультфильм", group.Name);
|
|
Assert.Equal(2, group.Items.Count);
|
|
Assert.All(group.Items, item => Assert.Equal(GroupElementKind.Show, item.ElementKind));
|
|
Assert.Equal([genre.Id], GroupFilter.FromJson(group.FilterJson)!.GenreIds);
|
|
|
|
// Повторное создание того же — конфликт: две одноимённые группы в слоте неразличимы.
|
|
var again = await handler.Handle(
|
|
new CreateGroupFromSuggestionCommand($"genre:{genre.Id:N}"),
|
|
CancellationToken.None
|
|
);
|
|
Assert.False(again.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFromSuggestion_FailsForUnknownKey()
|
|
{
|
|
var fixture = new TestDb();
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Shows.Add(Show.Create("A", ShowKind.Series));
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
await using var db = fixture.New();
|
|
var result = await new CreateGroupFromSuggestionCommandHandler(
|
|
db,
|
|
Builder(db),
|
|
GroupServices.Stats(db)
|
|
).Handle(new CreateGroupFromSuggestionCommand("genre:нет-такого"), CancellationToken.None);
|
|
|
|
Assert.False(result.IsSuccess);
|
|
}
|
|
|
|
private static Show WithGenre(Show show, Genre genre)
|
|
{
|
|
show.SetGenres([genre.Id]);
|
|
return show;
|
|
}
|
|
|
|
private static GroupSuggestionBuilder Builder(Infrastructure.Persistence.AppDbContext db) =>
|
|
new(db, new GroupElementResolver(db));
|
|
|
|
private static async Task<IReadOnlyList<GroupSuggestion>> BuildAsync(TestDb fixture)
|
|
{
|
|
await using var db = fixture.New();
|
|
return await Builder(db).BuildAsync(CancellationToken.None);
|
|
}
|
|
}
|