Перевести «не найдено» карточек и колонок на исключения

CardsService (карточка/файл) и ContainersService бросают NotFoundException вместо возврата null; эндпоинты больше не проверяют null — 404 отдаёт общий обработчик. Тесты обновлены под новое поведение.
This commit is contained in:
2026-09-13 14:32:24 +03:00
parent 7c43c40282
commit 685c5fdf46
11 changed files with 121 additions and 152 deletions
@@ -136,10 +136,8 @@ public static class CardDetailsEndpoints
}
CardsService service = context.RequestServices.GetRequiredService<CardsService>();
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<CardsService>();
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<CardsService>();
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}: установить напоминание. Ответ — карточка.
+2 -10
View File
@@ -209,11 +209,7 @@ public static class CardsEndpoints
}
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
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<CardsService>();
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 });
@@ -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<ContainersService>();
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<ContainersService>();
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);
@@ -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 = "Файл карточки";
/// <summary>
/// Добавляет файл карточке
/// </summary>
@@ -26,8 +31,9 @@ public sealed partial class CardsService
/// <param name="contentType">MIME-тип загрузки (может быть null/пустым — детект по расширению).</param>
/// <param name="content">Поток содержимого файла (читается хранилищем с позиции 0).</param>
/// <param name="size">Длина содержимого в байтах (пишется в метаданные записи).</param>
/// <returns>Метаданные добавленного файла или null — карточки нет (404).</returns>
public async Task<CardFileDto?> AddFileAsync(
/// <returns>Метаданные добавленного файла.</returns>
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
public async Task<CardFileDto> 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
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <param name="fileId">Id записи файла (<c>pf_...</c>).</param>
/// <returns>Метаданные записи файла либо null (карточка/запись не найдены).</returns>
public async Task<CardFileDto?> GetFileEntryAsync(
/// <returns>Метаданные записи файла.</returns>
/// <exception cref="NotFoundException">Карточка или запись файла не найдены.</exception>
public async Task<CardFileDto> 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);
}
/// <summary>
@@ -93,17 +95,15 @@ public sealed partial class CardsService
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <param name="fileId">Id удаляемой записи файла (<c>pf_...</c>).</param>
/// <returns>Карточка после удаления (без записи) либо null — карточки нет (404-семантика).</returns>
public async Task<CardDto?> RemoveFileAsync(
/// <returns>Карточка после удаления (без записи).</returns>
/// <exception cref="NotFoundException">Карточка или запись файла не найдены.</exception>
public async Task<CardDto> 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(
@@ -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
/// Перенос карточки в корзину
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <returns>Карточка после переноса (при no-op — как была) либо null — карточки нет (404).</returns>
public Task<CardDto?> TrashCardAsync(string cardId, CancellationToken ct)
/// <returns>Карточка после переноса (при no-op — как была).</returns>
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
public Task<CardDto> TrashCardAsync(string cardId, CancellationToken ct)
{
return TrashCardAsync(cardId, teach: true, ct);
}
@@ -98,17 +100,15 @@ public sealed partial class CardsService
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <param name="teach">True — писать сигнал «спам» (действие пользователя); false — не писать.</param>
/// <returns>Карточка после переноса (при no-op — как была) либо null — карточки нет (404).</returns>
public async Task<CardDto?> TrashCardAsync(
/// <returns>Карточка после переноса (при no-op — как была).</returns>
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
public async Task<CardDto> 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);
}
/// <summary>
/// Возврат карточки из архива/корзины на канбан.
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <returns>Колонка возврата (inbox/доска) либо null — карточки нет (404).</returns>
public async Task<string?> RestoreCardAsync(string cardId, CancellationToken ct)
/// <returns>Колонка возврата (inbox/доска).</returns>
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
public async Task<string> 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);
@@ -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
/// «Взять в работу»
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <returns>Карточка в стадии planned; null — карточки нет (404).</returns>
public async Task<CardDto?> TakeCardAsync(string cardId, CancellationToken ct)
/// <returns>Карточка в стадии planned.</returns>
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
public async Task<CardDto> 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);
}
/// <summary>
@@ -135,8 +134,9 @@ public sealed partial class CardsService
/// </summary>
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
/// <param name="body">Тело PATCH: ключ → JSON-значение (наличие ключа = поле меняется).</param>
/// <returns>Обновлённая карточка или null — карточки нет (404).</returns>
public async Task<CardDto?> PatchCardAsync(
/// <returns>Обновлённая карточка.</returns>
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
public async Task<CardDto> PatchCardAsync(
string cardId,
IReadOnlyDictionary<string, JsonElement> 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);
}
/// <summary>
@@ -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)
/// </summary>
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)
}
/// <summary>
/// Один контейнер со счётчиками; null — контейнера нет.
/// Один контейнер со счётчиками
/// </summary>
/// <param name="containerId">Id контейнера.</param>
/// <returns>Контейнер со счётчиками либо null.</returns>
public async Task<ContainerDto?> GetAsync(string containerId, CancellationToken ct)
/// <returns>Контейнер со счётчиками.</returns>
/// <exception cref="NotFoundException">Контейнер не найден.</exception>
public async Task<ContainerDto> 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<string, CardColumnCountDto> 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)
/// </summary>
/// <param name="containerId">Id контейнера.</param>
/// <param name="patch">Изменения; null-поле означает «не менять».</param>
/// <returns>Контейнер после патча; null — контейнера нет (404 «Контейнер не найден»).</returns>
public async Task<ContainerDto?> PatchAsync(
/// <returns>Контейнер после патча.</returns>
/// <exception cref="NotFoundException">Контейнер не найден.</exception>
public async Task<ContainerDto> 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)
/// Принимает ИИ-предложение
/// </summary>
/// <param name="containerId">Id контейнера-предложения.</param>
/// <returns>Контейнер после принятия; null — контейнера нет (404).</returns>
public Task<ContainerDto?> AcceptSuggestedAsync(string containerId, CancellationToken ct)
/// <returns>Контейнер после принятия.</returns>
/// <exception cref="NotFoundException">Контейнер не найден.</exception>
public Task<ContainerDto> AcceptSuggestedAsync(string containerId, CancellationToken ct)
{
return PatchAsync(containerId, new ContainerPatchDto(
Name: null,
@@ -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<NotFoundException>(
() => 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<NotFoundException>(
() => service.RestoreCardAsync("l_ghost", CancellationToken.None));
}
@@ -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<NotFoundException>(() => 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<NotFoundException>(
() => 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<NotFoundException>(
() => 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<NotFoundException>(
() => service.RemoveFileAsync("c_missing", "pf_1", CancellationToken.None));
Assert.Null(card); // эндпоинт отвечает 404 «Карточка не найдена»
Assert.Empty(storage.DeletedKeys);
}
@@ -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<NotFoundException>(
() => 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<NotFoundException>(() => service.PatchCardAsync(
"c_missing",
PatchBody(("title", "Т")),
CancellationToken.None);
Assert.Null(card);
CancellationToken.None));
}
@@ -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<NotFoundException>(
() => service.PatchAsync("b_missing", Patch(name: "X"), CancellationToken.None));
}
[Fact]