Implement functionality to add shows to existing collections from suggestions
Added a new endpoint for adding shows to already assembled franchises based on the same suggestion key. Updated the CollectionSuggestion and CollectionSuggestionDto records to include a CollectionId for tracking existing collections. Enhanced the CollectionSuggestions component in the frontend to support this new functionality, allowing users to add missing parts to existing collections. Updated localization strings to reflect the new addition feature in both English and Russian, ensuring clarity for users. Adjusted tests to verify the correct behavior of the addition process and its integration with existing collection logic.
This commit is contained in:
@@ -49,6 +49,8 @@ public static class CollectionEndpoints
|
||||
admin
|
||||
.MapPost("/suggestions", CreateFromSuggestion)
|
||||
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||||
// Дополнение уже собранной франшизы: коллекция определяется тем же ключом предложения.
|
||||
admin.MapPost("/suggestions/add", AddFromSuggestion).Produces<int>();
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -110,6 +112,16 @@ public static class CollectionEndpoints
|
||||
: result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> AddFromSuggestion(
|
||||
CreateCollectionFromSuggestionBody body,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new AddSuggestedShowsCommand(body.Key), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListCollections(
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.Collections.Suggest;
|
||||
|
||||
/// <summary>
|
||||
/// Дополняет уже собранную франшизу по предложению: состав сервер пересобирает по ключу, а не берёт
|
||||
/// с клиента. Между показом и нажатием библиотека могла измениться — добавиться должно актуальное.
|
||||
/// </summary>
|
||||
public sealed record AddSuggestedShowsCommand(string Key) : ICommand<Result<int>>;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.Collections.Suggest;
|
||||
|
||||
public sealed class AddSuggestedShowsCommandHandler(
|
||||
IAppDbContext dbContext,
|
||||
CollectionSuggestionBuilder builder,
|
||||
CollectionPosterPicker posters
|
||||
) : ICommandHandler<AddSuggestedShowsCommand, Result<int>>
|
||||
{
|
||||
public async Task<Result<int>> Handle(
|
||||
AddSuggestedShowsCommand command,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var suggestions = await builder.BuildAsync(cancellationToken);
|
||||
var suggestion = suggestions.FirstOrDefault(s => s.Key == command.Key);
|
||||
|
||||
// Ключ создания сюда приходить не должен: у такого предложения нет коллекции, которую
|
||||
// дополнять. Для админа это одно и то же — список предложений устарел.
|
||||
if (suggestion?.CollectionId is not { } collectionId)
|
||||
return Result.Failure<int>(CollectionErrors.SuggestionNotFound);
|
||||
|
||||
var collection = await dbContext
|
||||
.Collections.Include(c => c.Items)
|
||||
.FirstOrDefaultAsync(c => c.Id == collectionId, cancellationToken);
|
||||
if (collection is null)
|
||||
return Result.Failure<int>(CollectionErrors.NotFound);
|
||||
|
||||
// Части предложения уже упорядочены по году, а AddShow ставит в конец: франшиза пополняется
|
||||
// в хронологии, и переставлять руками обычно не приходится.
|
||||
var added = suggestion.Parts.Count(part => collection.AddShow(part.ShowId) is not null);
|
||||
|
||||
// Коллекция без постера могла остаться такой именно потому, что первой части не хватало.
|
||||
// Уже выбранный постер подборщик не трогает — проверять это здесь незачем.
|
||||
await posters.FillAsync([collection], cancellationToken);
|
||||
|
||||
return Result.Success(added);
|
||||
}
|
||||
}
|
||||
+128
-14
@@ -5,12 +5,17 @@ using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Application.Library.Collections.Suggest;
|
||||
|
||||
/// <summary>Предложение франшизы: имя и части в предлагаемом порядке.</summary>
|
||||
/// <summary>
|
||||
/// Предложение франшизы: имя и части в предлагаемом порядке. <paramref name="CollectionId"/>
|
||||
/// задан — франшиза уже собрана, и предлагается дополнить её найденными частями, а не заводить
|
||||
/// вторую такую же.
|
||||
/// </summary>
|
||||
public sealed record CollectionSuggestion(
|
||||
string Key,
|
||||
CollectionSuggestionSource Source,
|
||||
string Name,
|
||||
IReadOnlyList<CollectionSuggestionPartDto> Parts
|
||||
IReadOnlyList<CollectionSuggestionPartDto> Parts,
|
||||
Guid? CollectionId = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@@ -21,8 +26,12 @@ public sealed record CollectionSuggestion(
|
||||
/// намеренно осторожна: совпадать должно осмысленное начало названия, а не первое слово, иначе
|
||||
/// «Человек-паук» и «Человек дождя» уедут в одну франшизу.
|
||||
///
|
||||
/// Решение всё равно за человеком: предложение показывает состав целиком, а создаётся коллекция
|
||||
/// кнопкой. Сериалы сюда не попадают — у них своя форма продолжения, сезоны.
|
||||
/// Предложений тоже два вида: собрать новую франшизу и **дополнить уже собранную**. Второй важнее,
|
||||
/// чем кажется: библиотека пополняется по одному фильму, и четвёртая часть, приехавшая после сборки
|
||||
/// коллекции, сама по себе франшизы не образует — раньше она просто не показывалась нигде.
|
||||
///
|
||||
/// Решение всё равно за человеком: предложение показывает состав целиком, а применяется кнопкой.
|
||||
/// Сериалы сюда не попадают — у них своя форма продолжения, сезоны.
|
||||
/// </summary>
|
||||
public sealed class CollectionSuggestionBuilder(IAppDbContext dbContext)
|
||||
{
|
||||
@@ -50,15 +59,14 @@ public sealed class CollectionSuggestionBuilder(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var inCollections = await dbContext
|
||||
var members = await dbContext
|
||||
.CollectionItems.AsNoTracking()
|
||||
.Select(i => i.ShowId)
|
||||
.Distinct()
|
||||
.Select(i => new MemberRow(i.CollectionId, i.ShowId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var movies = await dbContext
|
||||
var allMovies = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => s.Kind == ShowKind.Single && !inCollections.Contains(s.Id))
|
||||
.Where(s => s.Kind == ShowKind.Single)
|
||||
.Select(s => new MovieRow(
|
||||
s.Id,
|
||||
s.Name,
|
||||
@@ -68,13 +76,29 @@ public sealed class CollectionSuggestionBuilder(IAppDbContext dbContext)
|
||||
s.FranchiseName
|
||||
))
|
||||
.ToListAsync(cancellationToken);
|
||||
if (movies.Count < MinParts)
|
||||
|
||||
var inCollections = members.Select(m => m.ShowId).ToHashSet();
|
||||
var movies = allMovies.Where(m => !inCollections.Contains(m.Id)).ToList();
|
||||
if (movies.Count == 0)
|
||||
return [];
|
||||
|
||||
var suggestions = new List<CollectionSuggestion>();
|
||||
var taken = new HashSet<Guid>();
|
||||
|
||||
foreach (var franchise in ByMetadata(movies))
|
||||
// Дополнение существующих идёт первым: если франшиза уже собрана, вторую такую же заводить
|
||||
// нельзя (имя занято), и предложить админу «создать» вместо «добавить» значило бы завести
|
||||
// его в конфликт.
|
||||
var additions = await ToExistingAsync(movies, members, allMovies, cancellationToken);
|
||||
foreach (var addition in additions)
|
||||
{
|
||||
suggestions.Add(addition);
|
||||
foreach (var part in addition.Parts)
|
||||
taken.Add(part.ShowId);
|
||||
}
|
||||
|
||||
var free = movies.Where(m => !taken.Contains(m.Id)).ToList();
|
||||
|
||||
foreach (var franchise in ByMetadata(free))
|
||||
{
|
||||
suggestions.Add(franchise);
|
||||
foreach (var part in franchise.Parts)
|
||||
@@ -83,17 +107,99 @@ public sealed class CollectionSuggestionBuilder(IAppDbContext dbContext)
|
||||
|
||||
// По названиям добираем только то, что не разобрала метадата: иначе одни и те же фильмы
|
||||
// попали бы в два предложения сразу и админ выбирал бы между ними вслепую.
|
||||
suggestions.AddRange(ByName(movies.Where(m => !taken.Contains(m.Id)).ToList()));
|
||||
suggestions.AddRange(ByName(free.Where(m => !taken.Contains(m.Id)).ToList()));
|
||||
|
||||
return
|
||||
[
|
||||
.. suggestions
|
||||
.OrderBy(s => s.Source)
|
||||
// Дополнения выше новых франшиз: это точечное действие на один-два фильма, и оно
|
||||
// не должно теряться под списком крупных предложений.
|
||||
.OrderByDescending(s => s.CollectionId is not null)
|
||||
.ThenBy(s => s.Source)
|
||||
.ThenByDescending(s => s.Parts.Count)
|
||||
.ThenBy(s => s.Name, StringComparer.CurrentCultureIgnoreCase),
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Чего не хватает уже собранным франшизам. Фильм признаётся частью коллекции по тем же двум
|
||||
/// признакам, что и при сборке новой: франшиза из метаданных совпала с франшизой любого её
|
||||
/// участника либо название подошло по правилам догадки.
|
||||
///
|
||||
/// Метадата проверяется по всем коллекциям раньше догадки: точное совпадение не должно
|
||||
/// проигрывать похожему названию из соседней франшизы.
|
||||
/// </summary>
|
||||
private async Task<IReadOnlyList<CollectionSuggestion>> ToExistingAsync(
|
||||
List<MovieRow> orphans,
|
||||
List<MemberRow> members,
|
||||
List<MovieRow> allMovies,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var collections = await dbContext
|
||||
.Collections.AsNoTracking()
|
||||
.Select(c => new { c.Id, c.Name })
|
||||
.ToListAsync(cancellationToken);
|
||||
if (collections.Count == 0)
|
||||
return [];
|
||||
|
||||
var byId = allMovies.ToDictionary(m => m.Id);
|
||||
var membersByCollection = members
|
||||
.GroupBy(m => m.CollectionId)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
// Сериал в коллекции — не ориентир для франшизы полнометражек: в справочнике
|
||||
// фильмов его просто нет, поэтому он и отсеивается по словарю.
|
||||
g => g.Where(m => byId.ContainsKey(m.ShowId)).Select(m => byId[m.ShowId]).ToList()
|
||||
);
|
||||
|
||||
var hits = new Dictionary<Guid, List<MovieRow>>();
|
||||
|
||||
foreach (var orphan in orphans)
|
||||
{
|
||||
var target = collections.FirstOrDefault(c =>
|
||||
Members(membersByCollection, c.Id).Any(m => SameMetadataFranchise(orphan, m))
|
||||
);
|
||||
var byMetadata = target is not null;
|
||||
|
||||
target ??= collections.FirstOrDefault(c =>
|
||||
Members(membersByCollection, c.Id)
|
||||
.Any(m => SameFranchise(Words(Title(orphan)), Words(Title(m))))
|
||||
);
|
||||
if (target is null)
|
||||
continue;
|
||||
|
||||
if (!hits.TryGetValue(target.Id, out var found))
|
||||
hits[target.Id] = found = [];
|
||||
found.Add(orphan with { MatchedByMetadata = byMetadata });
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
.. hits.Select(pair => new CollectionSuggestion(
|
||||
$"existing:{pair.Key:N}",
|
||||
// Источник — по самому слабому попаданию: одна догадка в наборе означает, что
|
||||
// весь набор надо проверить глазами.
|
||||
pair.Value.All(m => m.MatchedByMetadata)
|
||||
? CollectionSuggestionSource.Metadata
|
||||
: CollectionSuggestionSource.NameGuess,
|
||||
collections.First(c => c.Id == pair.Key).Name,
|
||||
Parts(pair.Value),
|
||||
pair.Key
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
private static List<MovieRow> Members(
|
||||
Dictionary<Guid, List<MovieRow>> membersByCollection,
|
||||
Guid collectionId
|
||||
) => membersByCollection.TryGetValue(collectionId, out var list) ? list : [];
|
||||
|
||||
/// <summary>Обе части знают свою франшизу из метаданных, и это одна и та же франшиза.</summary>
|
||||
private static bool SameMetadataFranchise(MovieRow first, MovieRow second) =>
|
||||
first.FranchiseExternalId is not null
|
||||
&& first.FranchiseExternalId == second.FranchiseExternalId;
|
||||
|
||||
private static IEnumerable<CollectionSuggestion> ByMetadata(List<MovieRow> movies)
|
||||
{
|
||||
var groups = movies
|
||||
@@ -228,12 +334,20 @@ public sealed class CollectionSuggestionBuilder(IAppDbContext dbContext)
|
||||
return trimmed.Trim();
|
||||
}
|
||||
|
||||
/// <param name="MatchedByMetadata">
|
||||
/// Как фильм попал в дополнение существующей коллекции: по франшизе из метаданных или догадкой
|
||||
/// по названию. Живёт здесь, а не отдельным списком, чтобы признак не разъезжался с самим
|
||||
/// фильмом при группировке.
|
||||
/// </param>
|
||||
private sealed record MovieRow(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? OriginalName,
|
||||
int? Year,
|
||||
string? FranchiseExternalId,
|
||||
string? FranchiseName
|
||||
string? FranchiseName,
|
||||
bool MatchedByMetadata = false
|
||||
);
|
||||
|
||||
private sealed record MemberRow(Guid CollectionId, Guid ShowId);
|
||||
}
|
||||
|
||||
+7
-3
@@ -5,16 +5,20 @@ namespace TeleWave.Application.Library.Collections.Suggest;
|
||||
/// <summary>Какие франшизы имеет смысл собрать из уже загруженных полнометражек.</summary>
|
||||
public sealed record SuggestCollectionsQuery : IQuery<IReadOnlyList<CollectionSuggestionDto>>;
|
||||
|
||||
/// <param name="Key">Ключ предложения — с ним же приходит команда создания.</param>
|
||||
/// <param name="Key">Ключ предложения — с ним же приходит команда создания или дополнения.</param>
|
||||
/// <param name="Source">Откуда взялось: франшиза источника либо догадка по названиям.</param>
|
||||
/// <param name="Parts">Части в предлагаемом порядке.</param>
|
||||
/// <param name="Parts">Части в предлагаемом порядке; у дополнения — только те, которых не хватает.</param>
|
||||
/// <param name="AlreadyExists">Коллекция с таким названием уже есть — создавать нечего.</param>
|
||||
/// <param name="CollectionId">
|
||||
/// Задан — франшиза уже собрана, и предлагается дополнить её, а не заводить вторую такую же.
|
||||
/// </param>
|
||||
public sealed record CollectionSuggestionDto(
|
||||
string Key,
|
||||
CollectionSuggestionSource Source,
|
||||
string Name,
|
||||
IReadOnlyList<CollectionSuggestionPartDto> Parts,
|
||||
bool AlreadyExists
|
||||
bool AlreadyExists,
|
||||
Guid? CollectionId = null
|
||||
);
|
||||
|
||||
public sealed record CollectionSuggestionPartDto(Guid ShowId, string Name, int? Year);
|
||||
|
||||
+7
-1
@@ -26,6 +26,11 @@ public sealed class SuggestCollectionsQueryHandler(
|
||||
.Select(n => n.Trim())
|
||||
.ToHashSet(StringComparer.CurrentCultureIgnoreCase);
|
||||
|
||||
// У дополнения имя — это имя самой коллекции, и совпадение с ней ничего не значит: такое
|
||||
// предложение и живёт ровно потому, что франшиза уже собрана.
|
||||
bool AlreadyCreated(CollectionSuggestion s) =>
|
||||
s.CollectionId is null && existing.Contains(s.Name);
|
||||
|
||||
return
|
||||
[
|
||||
.. suggestions.Select(s => new CollectionSuggestionDto(
|
||||
@@ -33,7 +38,8 @@ public sealed class SuggestCollectionsQueryHandler(
|
||||
s.Source,
|
||||
s.Name,
|
||||
s.Parts,
|
||||
existing.Contains(s.Name)
|
||||
AlreadyCreated(s),
|
||||
s.CollectionId
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Library.Collections;
|
||||
using TeleWave.Application.Library.Collections.AddShowsToCollection;
|
||||
using TeleWave.Application.Library.Collections.Suggest;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
@@ -63,8 +64,13 @@ public class CollectionSuggestionsTests
|
||||
Assert.DoesNotContain(suggestions, s => s.Source == CollectionSuggestionSource.NameGuess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Часть, приехавшая после сборки коллекции, сама франшизы не образует — её предлагают
|
||||
/// добавить в существующую. Иначе она не показывалась бы нигде: создавать вторую франшизу
|
||||
/// с тем же именем нельзя.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SkipsShows_AlreadyInCollections()
|
||||
public async Task SuggestsAddition_ToExistingCollection_ByMetadata()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var first = Movie("Рокки", 1976, franchise: "1241");
|
||||
@@ -72,17 +78,129 @@ public class CollectionSuggestionsTests
|
||||
var collection = Collection.Create("Рокки");
|
||||
collection.AddShow(first.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
await SeedWithCollectionAsync(fixture, collection, first, second);
|
||||
|
||||
var suggestion = Assert.Single(await BuildAsync(fixture));
|
||||
|
||||
Assert.Equal(collection.Id, suggestion.CollectionId);
|
||||
Assert.Equal(CollectionSuggestionSource.Metadata, suggestion.Source);
|
||||
Assert.Equal("Рокки", suggestion.Name);
|
||||
// В дополнении только недостающее: то, что уже в коллекции, предлагать нечего.
|
||||
Assert.Equal([second.Id], suggestion.Parts.Select(p => p.ShowId));
|
||||
}
|
||||
|
||||
/// <summary>Без метаданных дополнение опирается на догадку — и подписано как догадка.</summary>
|
||||
[Fact]
|
||||
public async Task SuggestsAddition_ToExistingCollection_ByName()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var first = Movie("Рокки", 1976);
|
||||
var second = Movie("Рокки 2", 1979);
|
||||
var collection = Collection.Create("Рокки");
|
||||
collection.AddShow(first.Id);
|
||||
|
||||
await SeedWithCollectionAsync(fixture, collection, first, second);
|
||||
|
||||
var suggestion = Assert.Single(await BuildAsync(fixture));
|
||||
|
||||
Assert.Equal(collection.Id, suggestion.CollectionId);
|
||||
Assert.Equal(CollectionSuggestionSource.NameGuess, suggestion.Source);
|
||||
Assert.Equal([second.Id], suggestion.Parts.Select(p => p.ShowId));
|
||||
}
|
||||
|
||||
/// <summary>Чужой фильм в существующую франшизу не уезжает — правила те же, что при сборке.</summary>
|
||||
[Fact]
|
||||
public async Task DoesNotSuggestAddition_ForUnrelatedMovie()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var member = Movie("Рокки", 1976, franchise: "1241");
|
||||
var stranger = Movie("Человек дождя", 1988, franchise: "777");
|
||||
var collection = Collection.Create("Рокки");
|
||||
collection.AddShow(member.Id);
|
||||
|
||||
await SeedWithCollectionAsync(fixture, collection, member, stranger);
|
||||
|
||||
Assert.Empty(await BuildAsync(fixture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Фильм, попавший в дополнение, из предложений «создать» уходит: два предложения на один
|
||||
/// фильм заставили бы выбирать вслепую, а создание к тому же упёрлось бы в занятое имя.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AdditionWins_OverNewFranchise()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var member = Movie("Рокки", 1976, franchise: "1241");
|
||||
var second = Movie("Рокки 2", 1979, franchise: "1241");
|
||||
var third = Movie("Рокки 3", 1982, franchise: "1241");
|
||||
var collection = Collection.Create("Рокки");
|
||||
collection.AddShow(member.Id);
|
||||
|
||||
await SeedWithCollectionAsync(fixture, collection, member, second, third);
|
||||
|
||||
var suggestion = Assert.Single(await BuildAsync(fixture));
|
||||
|
||||
Assert.Equal(collection.Id, suggestion.CollectionId);
|
||||
Assert.Equal([second.Id, third.Id], suggestion.Parts.Select(p => p.ShowId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSuggestedShows_AppendsToCollection()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var first = Movie("Рокки", 1976, franchise: "1241");
|
||||
var second = Movie("Рокки 2", 1979, franchise: "1241");
|
||||
var collection = Collection.Create("Рокки");
|
||||
collection.AddShow(first.Id);
|
||||
|
||||
await SeedWithCollectionAsync(fixture, collection, first, second);
|
||||
|
||||
var key = (await BuildAsync(fixture)).Single().Key;
|
||||
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
seed.Shows.AddRange(first, second);
|
||||
seed.Collections.Add(collection);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
var result = await new AddSuggestedShowsCommandHandler(
|
||||
db,
|
||||
new CollectionSuggestionBuilder(db),
|
||||
new CollectionPosterPicker(db)
|
||||
).Handle(new AddSuggestedShowsCommand(key), CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(1, result.Value);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var suggestions = await BuildAsync(fixture);
|
||||
await using var check = fixture.New();
|
||||
var stored = check.Collections.Include(c => c.Items).Single();
|
||||
// Дополнение встаёт в конец в хронологии — франшиза остаётся в порядке просмотра.
|
||||
Assert.Equal(
|
||||
[first.Id, second.Id],
|
||||
stored.Items.OrderBy(i => i.Position).Select(i => i.ShowId)
|
||||
);
|
||||
|
||||
// Одна часть уже разобрана — на франшизу оставшегося не хватает.
|
||||
Assert.Empty(suggestions);
|
||||
// Предложение исчерпано: добавлять больше нечего.
|
||||
Assert.Empty(await BuildAsync(fixture));
|
||||
}
|
||||
|
||||
/// <summary>Ключ предложения «создать» дополнением не применяется — коллекции у него нет.</summary>
|
||||
[Fact]
|
||||
public async Task AddSuggestedShows_RejectsCreationKey()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await SeedAsync(fixture, Movie("Рокки", 1976), Movie("Рокки 2", 1979));
|
||||
|
||||
var key = (await BuildAsync(fixture)).Single().Key;
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new AddSuggestedShowsCommandHandler(
|
||||
db,
|
||||
new CollectionSuggestionBuilder(db),
|
||||
new CollectionPosterPicker(db)
|
||||
).Handle(new AddSuggestedShowsCommand(key), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal(CollectionErrors.SuggestionNotFound.Code, result.Error.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -134,6 +252,18 @@ public class CollectionSuggestionsTests
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task SeedWithCollectionAsync(
|
||||
TestDb fixture,
|
||||
Collection collection,
|
||||
params Show[] shows
|
||||
)
|
||||
{
|
||||
await using var seed = fixture.New();
|
||||
seed.Shows.AddRange(shows);
|
||||
seed.Collections.Add(collection);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<CollectionSuggestion>> BuildAsync(TestDb fixture)
|
||||
{
|
||||
await using var db = fixture.New();
|
||||
|
||||
Reference in New Issue
Block a user