diff --git a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs index 0e2054e..42894a2 100644 --- a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs @@ -136,10 +136,8 @@ public static class CardDetailsEndpoints } CardsService service = context.RequestServices.GetRequiredService(); - CardDto? card = await service.TakeCardAsync(body.CardId ?? body.LeadId ?? string.Empty, ct); - return card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : await ReadCardAsync(context, card.Id, ct); + CardDto card = await service.TakeCardAsync(body.CardId ?? body.LeadId ?? string.Empty, ct); + return await ReadCardAsync(context, card.Id, ct); } // POST /api/cards/clear-rejected: полная очистка терминальной стадии «Отклонено». @@ -261,11 +259,7 @@ public static class CardDetailsEndpoints foreach (IFormFile file in form.Files) { await using Stream content = file.OpenReadStream(); - CardFileDto? entry = await cardsService.AddFileAsync(cardId, file.FileName, file.ContentType, content, file.Length, ct); - if (entry is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + await cardsService.AddFileAsync(cardId, file.FileName, file.ContentType, content, file.Length, ct); } return await ReadCardAsync(context, cardId, ct); @@ -286,11 +280,7 @@ public static class CardDetailsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - CardFileDto? entry = await cardsService.GetFileEntryAsync(cardId, fileId, ct); - if (entry is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + CardFileDto entry = await cardsService.GetFileEntryAsync(cardId, fileId, ct); if (string.IsNullOrWhiteSpace(entry.ObjectKey)) { @@ -339,10 +329,8 @@ public static class CardDetailsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? card = await cardsService.RemoveFileAsync(cardId, fileId, ct); - return card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : await ReadCardAsync(context, cardId, ct); + await cardsService.RemoveFileAsync(cardId, fileId, ct); + return await ReadCardAsync(context, cardId, ct); } // POST /api/cards/{cardId}/reminder {at: epoch-ms}: установить напоминание. Ответ — карточка. diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs index c2f41e1..a000360 100644 --- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs @@ -209,11 +209,7 @@ public static class CardsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? card = await cardsService.TrashCardAsync(cardId, ct); - if (card is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + await cardsService.TrashCardAsync(cardId, ct); await AuditAppender.AppendTenantAsync(context, AuditEvents.CardTrashed, new { cardId }, ct); return Results.Ok(new { ok = true }); @@ -230,11 +226,7 @@ public static class CardsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - string? col = await cardsService.RestoreCardAsync(cardId, ct); - if (col is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + string col = await cardsService.RestoreCardAsync(cardId, ct); await AuditAppender.AppendTenantAsync(context, AuditEvents.CardRestored, new { cardId, col }, ct); return Results.Ok(new { ok = true, col }); diff --git a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs index bbab4aa..8e63672 100644 --- a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs @@ -19,9 +19,6 @@ public static class ContainersEndpoints // OpenAPI-тег группы. private const string OpenApiTag = "containers"; - // 404 PATCH/accept: контейнер не найден. - private const string ContainerNotFoundDetail = "Контейнер не найден"; - // 400: отсутствующий/явный null name контейнера. private const string ContainerNameRequiredDetail = "Укажите название колонки"; @@ -143,7 +140,7 @@ public static class ContainersEndpoints } ContainersService containers = context.RequestServices.GetRequiredService(); - ContainerDto? updated = await containers.PatchAsync( + ContainerDto updated = await containers.PatchAsync( containerId, new ContainerPatchDto( patchBody.Name, @@ -155,10 +152,6 @@ public static class ContainersEndpoints NormalizeWireRules(patchBody.Rules), patchBody.Policy), ct); - if (updated is null) - { - return EndpointResults.NotFound(ContainerNotFoundDetail); - } await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, new { id = updated.Id }, ct); return Results.Ok(new { id = updated.Id }); @@ -176,11 +169,7 @@ public static class ContainersEndpoints } ContainersService containers = context.RequestServices.GetRequiredService(); - ContainerDto? accepted = await containers.AcceptSuggestedAsync(containerId, ct); - if (accepted is null) - { - return EndpointResults.NotFound(ContainerNotFoundDetail); - } + ContainerDto accepted = await containers.AcceptSuggestedAsync(containerId, ct); await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, new { id = accepted.Id }, ct); return Results.Ok(accepted); diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs index 33e0ee1..f0bfc89 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs @@ -1,5 +1,6 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Modules.Kanban.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Kanban.Application.Services; @@ -18,6 +19,10 @@ public sealed partial class CardsService private const string DefaultAttachmentName = "file"; + // Имена сущностей для текстов ошибок «не найдено». + private const string CardEntityName = "Карточка"; + private const string CardFileEntityName = "Файл карточки"; + /// /// Добавляет файл карточке /// @@ -26,8 +31,9 @@ public sealed partial class CardsService /// MIME-тип загрузки (может быть null/пустым — детект по расширению). /// Поток содержимого файла (читается хранилищем с позиции 0). /// Длина содержимого в байтах (пишется в метаданные записи). - /// Метаданные добавленного файла или null — карточки нет (404). - public async Task AddFileAsync( + /// Метаданные добавленного файла. + /// Карточка не найдена. + public async Task AddFileAsync( string cardId, string fileName, string? contentType, @@ -37,11 +43,8 @@ public sealed partial class CardsService { ArgumentNullException.ThrowIfNull(content); - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return null; - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); string name = string.IsNullOrWhiteSpace(fileName) ? DefaultAttachmentName : fileName; CardFileKind kind = FileKindDetector.Detect(name, contentType); @@ -62,7 +65,7 @@ public sealed partial class CardsService // Карточка исчезла между чтением и записью (гонка): объект-сирота в хранилище не нужен — // удаляем и отвечаем 404-семантикой (DeleteAsync сбои не бросает). await _storage.DeleteAsync(objectKey, ct); - return null; + throw new NotFoundException(CardEntityName, cardId); } return entry; @@ -73,19 +76,18 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Id записи файла (pf_...). - /// Метаданные записи файла либо null (карточка/запись не найдены). - public async Task GetFileEntryAsync( + /// Метаданные записи файла. + /// Карточка или запись файла не найдены. + public async Task GetFileEntryAsync( string cardId, string fileId, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return null; - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); - return card.Files.FirstOrDefault(file => file.Id == fileId); + return card.Files.FirstOrDefault(file => file.Id == fileId) + ?? throw new NotFoundException(CardFileEntityName, fileId); } /// @@ -93,17 +95,15 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Id удаляемой записи файла (pf_...). - /// Карточка после удаления (без записи) либо null — карточки нет (404-семантика). - public async Task RemoveFileAsync( + /// Карточка после удаления (без записи). + /// Карточка или запись файла не найдены. + public async Task RemoveFileAsync( string cardId, string fileId, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return null; - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); CardFileDto? entry = card.Files.FirstOrDefault(file => file.Id == fileId); if (entry is not null && !string.IsNullOrWhiteSpace(entry.ObjectKey)) @@ -113,11 +113,11 @@ public sealed partial class CardsService if (!await _store.RemoveFileAsync(cardId, fileId, ct)) { - return null; + throw new NotFoundException(CardFileEntityName, fileId); } return await _store.GetCardAsync(cardId, ct) - ?? throw new InvalidOperationException("Карточка не прочиталась после удаления файла: " + cardId); + ?? throw new NotFoundException(CardEntityName, cardId); } private static string BuildObjectKey( diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs index df3471a..1378c99 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs @@ -2,6 +2,7 @@ using Deal.Contracts.Integrations.Models; using Deal.Modules.Cards.Application.Models; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Kanban.Application.Services; @@ -87,8 +88,9 @@ public sealed partial class CardsService /// Перенос карточки в корзину /// /// Id карточки (c_...). - /// Карточка после переноса (при no-op — как была) либо null — карточки нет (404). - public Task TrashCardAsync(string cardId, CancellationToken ct) + /// Карточка после переноса (при no-op — как была). + /// Карточка не найдена. + public Task TrashCardAsync(string cardId, CancellationToken ct) { return TrashCardAsync(cardId, teach: true, ct); } @@ -98,17 +100,15 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// True — писать сигнал «спам» (действие пользователя); false — не писать. - /// Карточка после переноса (при no-op — как была) либо null — карточки нет (404). - public async Task TrashCardAsync( + /// Карточка после переноса (при no-op — как была). + /// Карточка не найдена. + public async Task TrashCardAsync( string cardId, bool teach, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return null; - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); if (card.Col == CardIds.Trash) { @@ -122,21 +122,20 @@ public sealed partial class CardsService await _mlClient.PushAsync(text, MlLearningLabels.Spam, PushWeightUser, ct); } - return await _store.GetCardAsync(cardId, ct); + return await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); } /// /// Возврат карточки из архива/корзины на канбан. /// /// Id карточки (c_...). - /// Колонка возврата (inbox/доска) либо null — карточки нет (404). - public async Task RestoreCardAsync(string cardId, CancellationToken ct) + /// Колонка возврата (inbox/доска). + /// Карточка не найдена. + public async Task RestoreCardAsync(string cardId, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return null; - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); string back = await ResolveReturnColAsync(card.PrevCol, ct); string text = LearningText(card); diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs index bce0780..69899fd 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs @@ -3,6 +3,7 @@ using Deal.Modules.Cards.Application.Models; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Kanban.Application.Services; @@ -101,14 +102,12 @@ public sealed partial class CardsService /// «Взять в работу» /// /// Id карточки (c_...). - /// Карточка в стадии planned; null — карточки нет (404). - public async Task TakeCardAsync(string cardId, CancellationToken ct) + /// Карточка в стадии planned. + /// Карточка не найдена. + public async Task TakeCardAsync(string cardId, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return null; - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); if (CardsDefaultContainers.Contains(card.Col)) { @@ -120,14 +119,14 @@ public sealed partial class CardsService if (!await _store.MoveCardStageAsync(cardId, PlannedStage, entry, nowMs, ct)) { // Карточка исчезла между чтением и переносом (гонка с удалением). - return null; + throw new NotFoundException(CardEntityName, cardId); } await _store.AddCommentAsync( PrefixId.New(KanbanIdPrefixes.Comment), cardId, CommentAuthor, TakenCommentText, ct); return await _store.GetCardAsync(cardId, ct) - ?? throw new InvalidOperationException("Карточка не прочиталась после take: " + cardId); + ?? throw new NotFoundException(CardEntityName, cardId); } /// @@ -135,8 +134,9 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Тело PATCH: ключ → JSON-значение (наличие ключа = поле меняется). - /// Обновлённая карточка или null — карточки нет (404). - public async Task PatchCardAsync( + /// Обновлённая карточка. + /// Карточка не найдена. + public async Task PatchCardAsync( string cardId, IReadOnlyDictionary body, CancellationToken ct) @@ -144,7 +144,13 @@ public sealed partial class CardsService ArgumentNullException.ThrowIfNull(body); bool updated = await _store.PatchCardAsync(cardId, ResolvePatch(body), ct); - return updated ? await _store.GetCardAsync(cardId, ct) : null; + if (!updated) + { + throw new NotFoundException(CardEntityName, cardId); + } + + return await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); } /// diff --git a/src/core/Deal.Modules.Kanban/Application/Services/ContainersService.cs b/src/core/Deal.Modules.Kanban/Application/Services/ContainersService.cs index a1168a8..6d75d61 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/ContainersService.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/ContainersService.cs @@ -4,6 +4,7 @@ using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Kanban.Application.Services; @@ -19,6 +20,9 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings) /// public const string DefaultContainerName = "Новая колонка"; + // Имя сущности для текста ошибки «не найдено». + private const string ContainerEntityName = "Контейнер"; + // Палитра колонок по умолчанию: цвет = Palette[order % 8], если цвет не задан. private static readonly string[] Palette = ["#818cf8", "#fbbf24", "#22d3ee", "#e879f9", "#34d399", "#fb7185", "#a78bfa", "#f97316"]; @@ -53,17 +57,15 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings) } /// - /// Один контейнер со счётчиками; null — контейнера нет. + /// Один контейнер со счётчиками /// /// Id контейнера. - /// Контейнер со счётчиками либо null. - public async Task GetAsync(string containerId, CancellationToken ct) + /// Контейнер со счётчиками. + /// Контейнер не найден. + public async Task GetAsync(string containerId, CancellationToken ct) { - ContainerDto? container = await store.GetContainerAsync(containerId, ct); - if (container is null) - { - return null; - } + ContainerDto container = await store.GetContainerAsync(containerId, ct) + ?? throw new NotFoundException(ContainerEntityName, containerId); IReadOnlyDictionary counts = await store.CountCardsByColAsync(ct); ContainerCountsDto containerCounts = counts.TryGetValue(container.Id, out CardColumnCountDto? count) @@ -107,17 +109,15 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings) /// /// Id контейнера. /// Изменения; null-поле означает «не менять». - /// Контейнер после патча; null — контейнера нет (404 «Контейнер не найден»). - public async Task PatchAsync( + /// Контейнер после патча. + /// Контейнер не найден. + public async Task PatchAsync( string containerId, ContainerPatchDto patch, CancellationToken ct) { - ContainerDto? current = await store.GetContainerAsync(containerId, ct); - if (current is null) - { - return null; - } + ContainerDto current = await store.GetContainerAsync(containerId, ct) + ?? throw new NotFoundException(ContainerEntityName, containerId); ContainerDto updated = ApplyPatch(current, patch); await store.UpdateContainerAsync(updated, ct); @@ -128,8 +128,9 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings) /// Принимает ИИ-предложение /// /// Id контейнера-предложения. - /// Контейнер после принятия; null — контейнера нет (404). - public Task AcceptSuggestedAsync(string containerId, CancellationToken ct) + /// Контейнер после принятия. + /// Контейнер не найден. + public Task AcceptSuggestedAsync(string containerId, CancellationToken ct) { return PatchAsync(containerId, new ContainerPatchDto( Name: null, diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs index 717e77e..d6ec709 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs @@ -1,9 +1,10 @@ using Deal.Contracts.Integrations.Models; -using Deal.Tests.Unit.Support; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Contracts; @@ -343,13 +344,13 @@ public sealed class CardsServiceTests } [Fact] - public async Task Trash_CardMissing_ReturnsNull() + public async Task Trash_CardMissing_ThrowsNotFound() { (CardsService service, _, _, TestMlClient ml) = Create(); - CardDto? result = await service.TrashCardAsync("l_ghost", CancellationToken.None); + await Assert.ThrowsAsync( + () => service.TrashCardAsync("l_ghost", CancellationToken.None)); - Assert.Null(result); // эндпоинт отвечает 404 «Карточка не найдена» Assert.Empty(ml.Pushed); } @@ -425,13 +426,12 @@ public sealed class CardsServiceTests } [Fact] - public async Task Restore_CardMissing_ReturnsNull() + public async Task Restore_CardMissing_ThrowsNotFound() { (CardsService service, _, _, _) = Create(); - string? back = await service.RestoreCardAsync("l_ghost", CancellationToken.None); - - Assert.Null(back); // эндпоинт отвечает 404 «Карточка не найдена» + await Assert.ThrowsAsync( + () => service.RestoreCardAsync("l_ghost", CancellationToken.None)); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs index cecfa82..d78ade6 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs @@ -1,6 +1,7 @@ using System.Text; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Modules.Settings; @@ -73,14 +74,13 @@ public sealed class CardsServiceFilesTests } [Fact] - public async Task Add_CardMissing_ReturnsNullAndDoesNotWriteObject() + public async Task Add_CardMissing_ThrowsAndDoesNotWriteObject() { (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); - CardFileDto? entry = await service.AddFileAsync( - "c_missing", "photo.png", "image/png", new MemoryStream("x"u8.ToArray()), 1, CancellationToken.None); + await Assert.ThrowsAsync(() => service.AddFileAsync( + "c_missing", "photo.png", "image/png", new MemoryStream("x"u8.ToArray()), 1, CancellationToken.None)); - Assert.Null(entry); // эндпоинт отвечает 404 «Карточка не найдена» Assert.Empty(storage.StoredObjectKeys); // «add на несуществующей карточке не пишет объект» Assert.Empty(store.CardDtos); } @@ -151,25 +151,23 @@ public sealed class CardsServiceFilesTests } [Fact] - public async Task GetEntry_CardMissing_ReturnsNull() + public async Task GetEntry_CardMissing_ThrowsNotFound() { (CardsService service, _, _) = Create(); - CardFileDto? entry = await service.GetFileEntryAsync("c_missing", "pf_1", CancellationToken.None); - - Assert.Null(entry); // 404 «Карточка не найдена» у эндпоинта + await Assert.ThrowsAsync( + () => service.GetFileEntryAsync("c_missing", "pf_1", CancellationToken.None)); } [Fact] - public async Task GetEntry_FileNotInMetadata_ReturnsNull() + public async Task GetEntry_FileNotInMetadata_ThrowsNotFound() { (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1") with { Files = new[] { new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "k") } }); - CardFileDto? entry = await service.GetFileEntryAsync("c_1", "pf_ghost", CancellationToken.None); - - Assert.Null(entry); // файла нет в метаданных карточки — 404-семантика + await Assert.ThrowsAsync( + () => service.GetFileEntryAsync("c_1", "pf_ghost", CancellationToken.None)); } @@ -225,13 +223,13 @@ public sealed class CardsServiceFilesTests } [Fact] - public async Task Remove_CardMissing_ReturnsNullWithoutStorageDelete() + public async Task Remove_CardMissing_ThrowsWithoutStorageDelete() { (CardsService service, _, TestFileStorage storage) = Create(); - CardDto? card = await service.RemoveFileAsync("c_missing", "pf_1", CancellationToken.None); + await Assert.ThrowsAsync( + () => service.RemoveFileAsync("c_missing", "pf_1", CancellationToken.None)); - Assert.Null(card); // эндпоинт отвечает 404 «Карточка не найдена» Assert.Empty(storage.DeletedKeys); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs index e2317c2..871d678 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Modules.Settings; @@ -99,13 +100,13 @@ public sealed class CardsServiceSelectedTests [Fact] - public async Task TakeCard_CardMissing_ReturnsNullAndCreatesNothing() + public async Task TakeCard_CardMissing_ThrowsAndCreatesNothing() { (CardsService service, TestKanjStore store, _, _) = Create(); - CardDto? card = await service.TakeCardAsync("c_missing", CancellationToken.None); + await Assert.ThrowsAsync( + () => service.TakeCardAsync("c_missing", CancellationToken.None)); - Assert.Null(card); Assert.Empty(store.CardDtos); } @@ -122,8 +123,7 @@ public sealed class CardsServiceSelectedTests stack: new[] { "Python", "aiogram" }, budget: new CardBudgetDto(From: 1600, To: 2200, Cur: "USD"))); - CardDto card = await service.TakeCardAsync("c_1", CancellationToken.None) - ?? throw new InvalidOperationException("take вернул null при существующей карточке"); + CardDto card = await service.TakeCardAsync("c_1", CancellationToken.None); Assert.Equal("c_1", card.Id); Assert.Equal("planned", card.Col); @@ -182,8 +182,7 @@ public sealed class CardsServiceSelectedTests ("tzText", "ТЗ"), ("stack", new[] { "C#", ".NET" }), // стек — полная замена массива ("budget", new { from = 500, cur = "EUR" })), - CancellationToken.None) - ?? throw new InvalidOperationException("patch вернул null при существующей карточке"); + CancellationToken.None); Assert.Equal("Новый заголовок", card.Title); Assert.Equal(string.Empty, card.Summary); // summary очищена пустой строкой @@ -255,16 +254,14 @@ public sealed class CardsServiceSelectedTests } [Fact] - public async Task Patch_CardMissing_ReturnsNull() + public async Task Patch_CardMissing_ThrowsNotFound() { (CardsService service, _, _, _) = Create(); - CardDto? card = await service.PatchCardAsync( + await Assert.ThrowsAsync(() => service.PatchCardAsync( "c_missing", PatchBody(("title", "Т")), - CancellationToken.None); - - Assert.Null(card); + CancellationToken.None)); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs index a329ea2..5084724 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -237,14 +238,12 @@ public sealed class ContainersServiceTests } [Fact] - public async Task Patch_UnknownContainer_ReturnsNull() + public async Task Patch_UnknownContainer_ThrowsNotFound() { (ContainersService service, _, _) = Create(); - ContainerDto? result = await service.PatchAsync( - "b_missing", Patch(name: "X"), CancellationToken.None); - - Assert.Null(result); // эндпоинт отвечает 404 «Контейнер не найден» + await Assert.ThrowsAsync( + () => service.PatchAsync("b_missing", Patch(name: "X"), CancellationToken.None)); } [Fact]