Refactor collection and group suggestion handling to improve name comparison logic
ci / build-backend (push) Successful in 1m25s
ci / build-frontend (push) Successful in 42s
ci / tests (push) Successful in 1m38s
ci / sonar (push) Successful in 6m30s

Updated the CreateCollectionFromSuggestionCommandHandler and CreateGroupFromSuggestionCommandHandler to compare names in memory rather than using SQL queries for case-insensitive checks. This change addresses potential discrepancies with SQL's case sensitivity, particularly with special characters. Additionally, modified unit tests to use array syntax for assertions, enhancing readability and consistency across the test suite.
This commit is contained in:
Leonid Pershin
2026-07-27 06:08:23 +03:00
parent 5ad28b2966
commit 97fa77287f
10 changed files with 67 additions and 41 deletions
@@ -21,11 +21,20 @@ public sealed class CreateCollectionFromSuggestionCommandHandler(
if (suggestion is null)
return Result.Failure<Guid>(CollectionErrors.SuggestionNotFound);
var taken = await dbContext.Collections.AnyAsync(
c => c.Name.ToLower() == suggestion.Name.ToLower(),
cancellationToken
);
if (taken)
// Сравниваем имена в памяти, а не запросом: коллекций десятки, зато проверка идёт по тем же
// правилам, что и пометка «уже есть» в списке предложений. SQL-регистронезависимость
// (lower()) в тонких местах вроде «ё» с ней расходится, и предложение то показывалось бы
// доступным, то отбивалось конфликтом.
var names = await dbContext.Collections.Select(c => c.Name).ToListAsync(cancellationToken);
if (
names.Any(name =>
string.Equals(
name.Trim(),
suggestion.Name,
StringComparison.CurrentCultureIgnoreCase
)
)
)
return Result.Failure<Guid>(CollectionErrors.SuggestionAlreadyCreated);
var collection = Collection.Create(suggestion.Name);
@@ -22,12 +22,19 @@ public sealed class CreateGroupFromSuggestionCommandHandler(
if (suggestion is null)
return Result.Failure<Guid>(GroupErrors.SuggestionNotFound);
// Имя — ключ узнаваемости группы, и две «Мультфильм» в списке слота неразличимы.
var taken = await dbContext.Groups.AnyAsync(
g => g.Name.ToLower() == suggestion.Name.ToLower(),
cancellationToken
);
if (taken)
// Имя — ключ узнаваемости группы, и две «Мультфильм» в списке слота неразличимы. Сравниваем
// в памяти, а не запросом: групп десятки, зато проверка идёт по тем же правилам, что и
// пометка «уже есть» в списке предложений, — SQL-регистронезависимость с ней расходится.
var names = await dbContext.Groups.Select(g => g.Name).ToListAsync(cancellationToken);
if (
names.Any(name =>
string.Equals(
name.Trim(),
suggestion.Name,
StringComparison.CurrentCultureIgnoreCase
)
)
)
return Result.Failure<Guid>(GroupErrors.SuggestionAlreadyCreated);
var group = Group.Create(suggestion.Name);
@@ -58,7 +58,7 @@ public class ListShowsTests
);
// Ролики-врезки живут на своей странице и в общей библиотеке только мешали бы.
Assert.Equal(new[] { "Автофильм", "Сериал" }, shows.Select(s => s.Name).ToArray());
Assert.Equal(["Автофильм", "Сериал"], shows.Select(s => s.Name).ToArray());
}
[Fact]
@@ -77,7 +77,7 @@ public class ListShowsTests
Assert.Equal(2, series.SeasonCount);
Assert.True(series.HasPoster);
Assert.Equal("Боевик", series.PrimaryGenre);
Assert.Equal(new[] { "Драма" }, series.OtherGenres.ToArray());
Assert.Equal(["Драма"], series.OtherGenres.ToArray());
}
[Fact]
@@ -54,8 +54,13 @@ public class UploadsCleanupTests : IDisposable
[Fact]
public void Sweep_SurvivesMissingDirectory()
{
// Каталога может не быть на самом первом старте — это не повод падать.
UploadsCleaner.Sweep(Paths(), TimeSpan.FromHours(24), NullLogger.Instance);
// Каталога может не быть на самом первом старте — это не повод падать. И заводить его
// уборка тоже не должна: каталоги создаёт хранилище, когда в них появляется нужда.
var paths = Paths();
UploadsCleaner.Sweep(paths, TimeSpan.FromHours(24), NullLogger.Instance);
Assert.False(Directory.Exists(paths.UploadsDir));
}
/// <summary>
@@ -79,7 +79,7 @@ public class OmdbMetadataProviderTests
Assert.Equal("Сериал", show.Title);
Assert.Equal(2001, show.Year);
Assert.Equal("описание", show.Overview);
Assert.Equal(new[] { "Action", "Sci-Fi" }, show.Genres);
Assert.Equal(["Action", "Sci-Fi"], show.Genres);
Assert.Equal("TV-14", show.ContentRating);
}
@@ -185,7 +185,7 @@ public class OmdbMetadataProviderTests
var seasons = await Provider(http).GetSeasonNumbersAsync("tt1", CancellationToken.None);
Assert.Equal(new[] { 1, 2, 3 }, seasons);
Assert.Equal([1, 2, 3], seasons);
}
[Fact]
@@ -97,7 +97,7 @@ public class TmdbMetadataProviderTests
Assert.Equal("Сериал", show.Title);
Assert.Equal(2001, show.Year);
// Идентификатор впереди названия, безымянный жанр — только идентификатором.
Assert.Equal(new[] { "tmdb:28", "Боевик", "Драма", "tmdb:99" }, show.Genres);
Assert.Equal(["tmdb:28", "Боевик", "Драма", "tmdb:99"], show.Genres);
// US приоритетнее RU, хотя в ответе идёт вторым.
Assert.Equal("TV-14", show.ContentRating);
}
@@ -236,7 +236,7 @@ public class TmdbMetadataProviderTests
var seasons = await Provider(http).GetSeasonNumbersAsync("42", CancellationToken.None);
Assert.Equal(new[] { 1, 2 }, seasons);
Assert.Equal([1, 2], seasons);
}
[Fact]
@@ -360,7 +360,7 @@ public class TemplateEditingTests
Assert.Equal(30, layer.Priority);
Assert.False(layer.IsEnabled);
var stored = LayerApplicability.FromJson(layer.ApplicabilityJson);
Assert.Equal(new[] { 5, 6 }, stored?.Weekdays);
Assert.Equal([5, 6], stored?.Weekdays);
}
[Fact]
@@ -47,10 +47,7 @@ public class ListPublicChannelsTests
);
// Ненумерованный уходит в конец: на телевизоре порядок задаёт номер.
Assert.Equal(
new[] { "Первый", "Второй", "Аврора" },
channels.Select(c => c.Name).ToArray()
);
Assert.Equal(["Первый", "Второй", "Аврора"], channels.Select(c => c.Name).ToArray());
Assert.Null(channels[^1].Number);
Assert.All(channels, c => Assert.Null(c.CurrentShowId));
}