diff --git a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs index 42894a2..e37d653 100644 --- a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs @@ -202,9 +202,7 @@ public static class CardDetailsEndpoints return EndpointResults.BadRequest(result.Error); } - return result.Card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : await ReadCardAsync(context, cardId, ct); + return await ReadCardAsync(context, cardId, ct); } // DELETE /api/cards/{cardId}/links/{linkId}: удалить ссылку. Ответ — карточка. @@ -220,10 +218,8 @@ public static class CardDetailsEndpoints } CardsService service = context.RequestServices.GetRequiredService(); - CardResultDto result = await service.RemoveLinkAsync(cardId, linkId, ct); - return result.Card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : await ReadCardAsync(context, cardId, ct); + await service.RemoveLinkAsync(cardId, linkId, ct); + return await ReadCardAsync(context, cardId, ct); } // POST /api/cards/{cardId}/files: загрузка вложений (multipart/form-data, поле files). @@ -240,10 +236,7 @@ public static class CardDetailsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - if (await cardsService.GetCardAsync(cardId, ct) is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + await cardsService.GetCardAsync(cardId, ct); IFormCollection form; try @@ -357,9 +350,7 @@ public static class CardDetailsEndpoints return EndpointResults.BadRequest(result.Error); } - return result.Card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : await ReadCardAsync(context, cardId, ct); + return await ReadCardAsync(context, cardId, ct); } // DELETE /api/cards/{cardId}/reminder: снять напоминание. Ответ — карточка. @@ -374,9 +365,8 @@ public static class CardDetailsEndpoints } CardsService service = context.RequestServices.GetRequiredService(); - return await service.ClearReminderAsync(cardId, ct) - ? await ReadCardAsync(context, cardId, ct) - : EndpointResults.NotFound(CardNotFoundDetail); + await service.ClearReminderAsync(cardId, ct); + return await ReadCardAsync(context, cardId, ct); } // POST /api/cards/{cardId}/reminder/snooze: «напомнить позже» (now + 24 ч). Ответ — карточка. @@ -391,9 +381,8 @@ public static class CardDetailsEndpoints } CardsService service = context.RequestServices.GetRequiredService(); - return await service.SnoozeReminderAsync(cardId, ct) - ? await ReadCardAsync(context, cardId, ct) - : EndpointResults.NotFound(CardNotFoundDetail); + await service.SnoozeReminderAsync(cardId, ct); + return await ReadCardAsync(context, cardId, ct); } // Читает карточку через единый сервис и возвращает её как ответ (404 — карточки нет). @@ -412,11 +401,7 @@ public static class CardDetailsEndpoints CancellationToken ct) { CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? card = await cardsService.GetCardAsync(cardId, ct); - if (card is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + CardDto card = await cardsService.GetCardAsync(cardId, ct); SourceContent content = await cardsService.ResolveSourceAsync(card, ct); return Results.Ok(content); @@ -428,10 +413,8 @@ public static class CardDetailsEndpoints CancellationToken ct) { CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? card = await cardsService.GetCardAsync(cardId, ct); - return card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : Results.Ok(card); + CardDto card = await cardsService.GetCardAsync(cardId, ct); + return Results.Ok(card); } // Имя файла для Content-Disposition без кавычек «"». diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs index a000360..68856da 100644 --- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs @@ -128,10 +128,8 @@ public static class CardsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? card = await cardsService.GetCardAsync(cardId, ct); - return card is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : Results.Ok(card); + CardDto card = await cardsService.GetCardAsync(cardId, ct); + return Results.Ok(card); } private static async Task MarkAllSeenAsync(HttpContext context, CancellationToken ct) @@ -184,18 +182,11 @@ public static class CardsEndpoints return EndpointResults.BadRequest(outcome.Error); } - if (!outcome.Exists) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } - await AuditAppender.AppendTenantAsync(context, AuditEvents.CardMoved, new { cardId, to = body.To }, ct); CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? unified = await cardsService.GetCardAsync(cardId, ct); - return unified is null - ? EndpointResults.NotFound(CardNotFoundDetail) - : Results.Ok(unified); + CardDto unified = await cardsService.GetCardAsync(cardId, ct); + return Results.Ok(unified); } private static async Task TrashAsync( @@ -353,11 +344,7 @@ public static class CardsEndpoints } CardsService cardsService = context.RequestServices.GetRequiredService(); - CardDto? card = await cardsService.GetCardAsync(cardId, ct); - if (card is null) - { - return EndpointResults.NotFound(CardNotFoundDetail); - } + CardDto card = await cardsService.GetCardAsync(cardId, ct); CardReclassifier reclassifier = context.RequestServices.GetRequiredService(); ReclassifyResultDto result = await reclassifier.ReclassifyCardAsync(card, ct); diff --git a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs index f46981d..d17eeaa 100644 --- a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs @@ -55,9 +55,6 @@ public static class DiscoveryEndpoints // Путь лога задачи (GET). private const string TaskLogPath = "/tasks/{task_id}/log"; - // 404: кандидат не найден. - private const string CandidateNotFoundDetail = "Кандидат не найден"; - private const string AlreadyJoinedDetail = "Уже вступили в этот источник"; private const string JoinedRejectDetail = "Уже вступили — удалите источник из каналов"; @@ -281,11 +278,7 @@ public static class DiscoveryEndpoints } DiscoveryCandidatesService candidates = context.RequestServices.GetRequiredService(); - DiscoveryCandidateDto? row = await candidates.GetAsync(dialog_id, ct); - if (row is null) - { - return EndpointResults.NotFound(CandidateNotFoundDetail); - } + DiscoveryCandidateDto row = await candidates.GetAsync(dialog_id, ct); if (row.Status == DiscoveryCandidateStatuses.Joined) { @@ -314,8 +307,8 @@ public static class DiscoveryEndpoints try { - DiscoveryCandidateDto? joined = await candidates.MarkJoinedAsync(dialog_id, auto: false, ct); - return joined is null ? EndpointResults.NotFound(CandidateNotFoundDetail) : Results.Ok(joined); + DiscoveryCandidateDto joined = await candidates.MarkJoinedAsync(dialog_id, auto: false, ct); + return Results.Ok(joined); } catch (DiscoveryValidationException exception) { @@ -334,11 +327,7 @@ public static class DiscoveryEndpoints } DiscoveryCandidatesService candidates = context.RequestServices.GetRequiredService(); - DiscoveryCandidateDto? row = await candidates.GetAsync(dialog_id, ct); - if (row is null) - { - return EndpointResults.NotFound(CandidateNotFoundDetail); - } + DiscoveryCandidateDto row = await candidates.GetAsync(dialog_id, ct); if (row.Status == DiscoveryCandidateStatuses.Joined) { @@ -347,8 +336,8 @@ public static class DiscoveryEndpoints try { - DiscoveryCandidateDto? rejected = await candidates.MarkRejectedAsync(dialog_id, ManualRejectReason, ct); - return rejected is null ? EndpointResults.NotFound(CandidateNotFoundDetail) : Results.Ok(rejected); + DiscoveryCandidateDto rejected = await candidates.MarkRejectedAsync(dialog_id, ManualRejectReason, ct); + return Results.Ok(rejected); } catch (DiscoveryValidationException exception) { diff --git a/src/core/Deal.Api/Endpoints/MlEndpoints.cs b/src/core/Deal.Api/Endpoints/MlEndpoints.cs index 64a4a4d..b7d816a 100644 --- a/src/core/Deal.Api/Endpoints/MlEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/MlEndpoints.cs @@ -37,8 +37,6 @@ public static class MlEndpoints private const string EnterTextDetail = "Введите текст"; - private const string MessageNotFoundDetail = "Исходное сообщение не найдено"; - /// /// Регистрирует группу /api/ml /// @@ -141,12 +139,7 @@ public static class MlEndpoints } MlReviewService review = context.RequestServices.GetRequiredService(); - MlApplyResult? result = await review.ApplyAsync(body.DialogId, body.MsgId, body.Action, ct); - if (result is null) - { - return EndpointResults.NotFound(MessageNotFoundDetail); - } - + MlApplyResult result = await review.ApplyAsync(body.DialogId, body.MsgId, body.Action, ct); if (result.Error is not null) { return EndpointResults.BadRequest(result.Error); diff --git a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs index bafc30c..73f8f05 100644 --- a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs @@ -144,12 +144,7 @@ public static class OperatorTenantsEndpoints return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail); } - TenantDetailDto? tenant = await tenantAdminService.GetAsync(id, ct); - if (tenant is null) - { - return EndpointResults.NotFound(TenantNotFoundDetail); - } - + TenantDetailDto tenant = await tenantAdminService.GetAsync(id, ct); return Results.Ok(tenant); } diff --git a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs index f7ea7cb..24166af 100644 --- a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs @@ -33,8 +33,6 @@ public static class PipelineEndpoints // Путь возврата записи отсева в обработку (POST). private const string RejectedReturnPath = "/rejected/{rejId}/return"; - private const string RejectedNotFoundDetail = "Запись не найдена"; - private const int DefaultPageSize = 100; /// @@ -140,11 +138,7 @@ public static class PipelineEndpoints } PipelineProcessingService processing = context.RequestServices.GetRequiredService(); - RejectReturnResultDto? result = await processing.ReturnAsync(rejId, body.Reason ?? string.Empty, ct); - if (result is null) - { - return EndpointResults.NotFound(RejectedNotFoundDetail); - } + RejectReturnResultDto result = await processing.ReturnAsync(rejId, body.Reason ?? string.Empty, ct); return result.Error is not null ? EndpointResults.BadRequest(result.Error) diff --git a/src/core/Deal.Infrastructure/Services/CardMover.cs b/src/core/Deal.Infrastructure/Services/CardMover.cs index 5739095..1f59b4c 100644 --- a/src/core/Deal.Infrastructure/Services/CardMover.cs +++ b/src/core/Deal.Infrastructure/Services/CardMover.cs @@ -22,8 +22,6 @@ public sealed class CardMover(CardsService cardsService) : ICardMover CardResultDto result = CardsDefaultContainers.Contains(toContainerId) ? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct) : await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct); - return result.Error is not null - ? new CardMoveResultDto(result.Error, Exists: true) - : new CardMoveResultDto(null, Exists: result.Card is not null); + return new CardMoveResultDto(result.Error); } } diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ICardMover.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ICardMover.cs index 904ae3f..21f1a26 100644 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ICardMover.cs +++ b/src/core/Deal.Modules.Cards/Application/Abstractions/ICardMover.cs @@ -1,5 +1,6 @@ using Deal.Modules.Cards.Application.Dtos; using Deal.Modules.Cards.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Cards.Application.Abstractions; @@ -14,7 +15,8 @@ public interface ICardMover /// Id карточки. /// Id контейнера назначения (стадия «Выбранных» либо дашборд-контейнер). /// Контекст перехода (инициатор, причина, обучение). - /// Результат: Error (400-текст отказа) | Exists=false (карточки нет, 404) | успех (Exists=true). + /// Результат: Error (400-текст отказа) либо null (успех). + /// Карточка не найдена. public Task MoveAsync( string cardId, string toContainerId, diff --git a/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs b/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs index 166419e..06bfa6a 100644 --- a/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs +++ b/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs @@ -5,6 +5,5 @@ namespace Deal.Modules.Cards.Application.Dtos; /// /// Результат перехода карточки единым механизмом . /// -/// Текст 400-ошибки либо null. -/// True — карточка найдена и переход выполнен (либо перенос был no-op). -public sealed record CardMoveResultDto(string? Error, bool Exists); +/// Текст 400-ошибки либо null (успех). +public sealed record CardMoveResultDto(string? Error); diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs index ecabc16..6115bad 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs @@ -2,6 +2,7 @@ using Deal.Contracts.Integrations.Models; using Deal.Modules.Discovery.Application.Abstractions; using Deal.Modules.Discovery.Application.Exceptions; using Deal.Modules.Discovery.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Discovery.Application.Services; @@ -13,6 +14,10 @@ public sealed class DiscoveryCandidatesService( DiscoveryLogService log, DiscoveryBlacklistService blacklist) { + private const string CandidateEntityName = "Кандидат"; + + private const string TaskEntityName = "Задача поиска"; + /// /// 400 mark_rejected /// @@ -53,10 +58,12 @@ public sealed class DiscoveryCandidatesService( /// Кандидат по dialog_id. /// /// Подписанный id источника. - /// Кандидат или null (404 «Кандидат не найден»). - public Task GetAsync(string dialogId, CancellationToken ct) + /// Кандидат. + /// Кандидат не найден. + public async Task GetAsync(string dialogId, CancellationToken ct) { - return store.GetCandidateAsync(dialogId, ct); + return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); } /// @@ -78,11 +85,8 @@ public sealed class DiscoveryCandidatesService( string hue, CancellationToken ct) { - DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); - if (task is null) - { - return null; - } + DiscoveryTaskDto task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); if (await store.IsDialogMonitoredAsync(dialogId, ct).ConfigureAwait(false)) { @@ -125,7 +129,8 @@ public sealed class DiscoveryCandidatesService( }; await store.CreateCandidateAsync(row, ct).ConfigureAwait(false); await store.BumpTaskCounterAsync(taskId, DiscoveryCounterField.Found, 1, ct).ConfigureAwait(false); - return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); + return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); } /// @@ -134,18 +139,21 @@ public sealed class DiscoveryCandidatesService( /// Id задачи. /// Подписанный id источника. /// Изменяемые поля (null — не менять). - /// Обновлённый кандидат либо null — задачи/кандидата нет (или кандидат другой задачи). - public async Task SetAsync( + /// Обновлённый кандидат. + /// Задача или кандидат не найдены. + public async Task SetAsync( string taskId, string dialogId, DiscoveryCandidatePatch patch, CancellationToken ct) { - DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); - DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); - if (task is null || current is null || current.TaskId != taskId) + _ = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); + DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); + if (current.TaskId != taskId) { - return null; + throw new NotFoundException(CandidateEntityName, dialogId); } DiscoveryCandidatePatch normalized = NormalizePatch(patch, current); @@ -159,7 +167,8 @@ public sealed class DiscoveryCandidatesService( } await store.PatchCandidateAsync(dialogId, normalized, ct).ConfigureAwait(false); - return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); + return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); } /// @@ -167,9 +176,10 @@ public sealed class DiscoveryCandidatesService( /// /// Подписанный id источника. /// Новый статус: new|review. - /// Обновлённый кандидат либо null (кандидата нет). + /// Обновлённый кандидат. + /// Кандидат не найден. /// Статус не new/review. - public async Task SetStatusAsync( + public async Task SetStatusAsync( string dialogId, string status, CancellationToken ct) @@ -179,11 +189,8 @@ public sealed class DiscoveryCandidatesService( throw new DiscoveryValidationException(string.Format(TransitionNotAllowedFormat, status)); } - DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); - if (current is null) - { - return null; - } + DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); await store.SetCandidateStatusAsync(dialogId, status, ct).ConfigureAwait(false); if (status == DiscoveryCandidateStatuses.Review) @@ -195,7 +202,8 @@ public sealed class DiscoveryCandidatesService( ct).ConfigureAwait(false); } - return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); + return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); } /// @@ -203,17 +211,15 @@ public sealed class DiscoveryCandidatesService( /// /// Подписанный id источника. /// True — авто-вступление воркера; false — ручное. - /// Кандидат в joined либо null (кандидата нет). Повторный вызов для joined — идемпотентен. - public async Task MarkJoinedAsync( + /// Кандидат в joined. Повторный вызов для joined — идемпотентен. + /// Кандидат не найден. + public async Task MarkJoinedAsync( string dialogId, bool auto, CancellationToken ct) { - DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); - if (current is null) - { - return null; - } + DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); if (current.Status == DiscoveryCandidateStatuses.Joined) { @@ -224,7 +230,8 @@ public sealed class DiscoveryCandidatesService( await store.BumpTaskCounterAsync(current.TaskId, DiscoveryCounterField.Joined, 1, ct).ConfigureAwait(false); string logEvent = auto ? DiscoveryLogEvents.JoinAuto : DiscoveryLogEvents.JoinManual; await log.AddAsync(current.TaskId, logEvent, string.Format(JoinedLogFormat, dialogId), ct).ConfigureAwait(false); - return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); + return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); } /// @@ -232,18 +239,16 @@ public sealed class DiscoveryCandidatesService( /// /// Подписанный id источника. /// Причина отклонения (в лог/чёрный список; «отклонено вручную» — эндпоинт reject). - /// Кандидат в rejected либо null (кандидата нет). Повторный вызов для rejected — идемпотентен. + /// Кандидат в rejected. Повторный вызов для rejected — идемпотентен. + /// Кандидат не найден. /// Источник уже joined — отклонить нельзя. - public async Task MarkRejectedAsync( + public async Task MarkRejectedAsync( string dialogId, string reason, CancellationToken ct) { - DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); - if (current is null) - { - return null; - } + DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); if (current.Status == DiscoveryCandidateStatuses.Joined) { @@ -260,7 +265,8 @@ public sealed class DiscoveryCandidatesService( string text = string.IsNullOrEmpty(reason) ? string.Format(RejectedLogFormat, dialogId) : reason; await log.AddAsync(current.TaskId, DiscoveryLogEvents.Reject, text, ct).ConfigureAwait(false); await blacklist.AddAsync(dialogId, current.Name, reason ?? string.Empty, ct).ConfigureAwait(false); - return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); + return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(CandidateEntityName, dialogId); } /// 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 1378c99..d24c6a8 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs @@ -27,9 +27,16 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Карточка или null — строки нет (эндпоинт отвечает 404 «Карточка не найдена»). - public Task GetCardAsync(string cardId, CancellationToken ct) + /// + /// Одна карточка по id. + /// + /// Id карточки (c_...). + /// Карточка. + /// Карточка не найдена. + public async Task GetCardAsync(string cardId, CancellationToken ct) { - return _store.GetCardAsync(cardId, ct); + return await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); } @@ -57,7 +64,7 @@ public sealed partial class CardsService CardDto? card = await _store.GetCardAsync(cardId, ct); if (card is null) { - return new CardResultDto(null, null); + throw new NotFoundException(CardEntityName, cardId); } if (card.Col == CardIds.Archive || card.Col == CardIds.Trash) @@ -81,7 +88,10 @@ public sealed partial class CardsService await _mlClient.PushAsync(text, toCol, PushWeightUser, ct); } - return new CardResultDto(null, await _store.GetCardAsync(cardId, ct)); + return new CardResultDto( + null, + await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId)); } /// diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs index c1c6b56..f4214ff 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs @@ -1,5 +1,6 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Settings.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Kanban.Application.Services; @@ -22,17 +23,15 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Время напоминания, epoch-ms. - /// Результат: Error (400) | Card=null без Error (404) | Card — карточка с напоминанием. + /// Результат: Error (400) | Card — карточка с напоминанием. + /// Карточка не найдена. public async Task SetReminderAsync( string cardId, long atMs, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return new CardResultDto(null, null); - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); if (!await ReadRemindersEnabledAsync(ct)) { @@ -41,7 +40,7 @@ public sealed partial class CardsService await _store.SetReminderAsync(cardId, atMs, ct); CardDto saved = await _store.GetCardAsync(cardId, ct) - ?? throw new InvalidOperationException("Карточка не прочиталась после установки напоминания: " + cardId); + ?? throw new NotFoundException(CardEntityName, cardId); return new CardResultDto(null, saved); } @@ -49,35 +48,27 @@ public sealed partial class CardsService /// Снимает напоминание карточки. /// /// Id карточки (c_...). - /// True — карточка есть и напоминание снято; false — карточки нет (404). - public async Task ClearReminderAsync(string cardId, CancellationToken ct) + /// Карточка не найдена. + public async Task ClearReminderAsync(string cardId, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return false; - } + _ = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); await _store.ClearReminderAsync(cardId, ct); - return true; } /// /// «Напомнить позже» /// /// Id карточки (c_...). - /// True — карточка есть и напоминание отложено; false — карточки нет (404). - public async Task SnoozeReminderAsync(string cardId, CancellationToken ct) + /// Карточка не найдена. + public async Task SnoozeReminderAsync(string cardId, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return false; - } + _ = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); long snoozedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + ReminderSnoozeMs; await _store.SetReminderAsync(cardId, snoozedAtMs, ct); - return true; } /// 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 69899fd..34b97ab 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs @@ -12,6 +12,9 @@ namespace Deal.Modules.Kanban.Application.Services; /// public sealed partial class CardsService { + // Имя сущности для текста ошибки «не найдено». + private const string CardLinkEntityName = "Ссылка карточки"; + /// /// 400 перенос по стадии /// @@ -159,18 +162,16 @@ public sealed partial class CardsService /// Id карточки (c_...). /// Название ссылки; пустое после Trim → name = url. /// URL ссылки (без схемы — добавится https://). - /// Результат: Error (400 «Пустая ссылка») | Card=null без Error (404) | Card — карточка со ссылкой. + /// Результат: Error (400 «Пустая ссылка») | Card — карточка со ссылкой. + /// Карточка не найдена. public async Task AddLinkAsync( string cardId, string name, string url, CancellationToken ct) { - CardDto? card = await _store.GetCardAsync(cardId, ct); - if (card is null) - { - return new CardResultDto(null, null); - } + CardDto card = await _store.GetCardAsync(cardId, ct) + ?? throw new NotFoundException(CardEntityName, cardId); string normalizedUrl = (url ?? string.Empty).Trim(); if (normalizedUrl.Length == 0) @@ -191,11 +192,11 @@ public sealed partial class CardsService normalizedUrl); if (!await _store.AddLinkAsync(cardId, link, ct)) { - return new CardResultDto(null, null); + throw new NotFoundException(CardEntityName, cardId); } CardDto saved = await _store.GetCardAsync(cardId, ct) - ?? throw new InvalidOperationException("Карточка не прочиталась после добавления ссылки: " + cardId); + ?? throw new NotFoundException(CardEntityName, cardId); return new CardResultDto(null, saved); } @@ -204,7 +205,8 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Id удаляемой ссылки (pl_...). - /// Результат: Card=null без Error (404) | Card — карточка без ссылки. + /// Карточка без ссылки. + /// Карточка или ссылка не найдены. public async Task RemoveLinkAsync( string cardId, string linkId, @@ -212,11 +214,11 @@ public sealed partial class CardsService { if (!await _store.RemoveLinkAsync(cardId, linkId, ct)) { - return new CardResultDto(null, null); + throw new NotFoundException(CardLinkEntityName, linkId); } CardDto saved = await _store.GetCardAsync(cardId, ct) - ?? throw new InvalidOperationException("Карточка не прочиталась после удаления ссылки: " + cardId); + ?? throw new NotFoundException(CardEntityName, cardId); return new CardResultDto(null, saved); } @@ -225,7 +227,8 @@ public sealed partial class CardsService /// /// Id карточки (c_...). /// Новый контейнер-стадия — id каталога . - /// Результат: Error «Неизвестная стадия» (400) | Card=null без Error (карточки нет, 404) | Card — карточка после переноса. + /// Результат: Error «Неизвестная стадия» (400) | Card — карточка после переноса. + /// Карточка не найдена. public async Task MoveStageCardAsync( string cardId, string containerId, @@ -241,11 +244,11 @@ public sealed partial class CardsService bool moved = await _store.MoveCardStageAsync(cardId, containerId, entry, nowMs, ct); if (!moved) { - return new CardResultDto(null, null); + throw new NotFoundException(CardEntityName, cardId); } CardDto card = await _store.GetCardAsync(cardId, ct) - ?? throw new InvalidOperationException("Карточка не прочиталась после move: " + cardId); + ?? throw new NotFoundException(CardEntityName, cardId); return new CardResultDto(null, card); } diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs index 2fabdbf..14c2426 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs @@ -8,6 +8,7 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Pipeline.Application.Services; @@ -34,6 +35,9 @@ public sealed class MlReviewService( // Размер одного чтения из очереди/отсева при объединении кандидатов. private const int MaxScan = 500; + // Имя сущности для текста ошибки «не найдено». + private const string MessageEntityName = "Исходное сообщение"; + private const int TextPreviewLength = 600; /// @@ -157,8 +161,9 @@ public sealed class MlReviewService( /// Оригинал источника (OriginRef) записи. /// Внешний id записи в источнике. /// Действие: skip | spam | board:<id>. - /// Результат решения; null — исходная запись не найдена (404-семантика эндпоинта). - public async Task ApplyAsync( + /// Результат решения. + /// Исходная запись не найдена. + public async Task ApplyAsync( string dialogId, long msgId, string? action, @@ -168,17 +173,14 @@ public sealed class MlReviewService( string dialog = (dialogId ?? string.Empty).Trim(); string externalId = msgId.ToString(CultureInfo.InvariantCulture); - SourceRef? source = await ResolveSourceAsync(dialog, externalId, ct); - if (source is null) - { - return null; // 404: исходная запись не найдена - } + SourceRef source = await ResolveSourceAsync(dialog, externalId, ct) + ?? throw new NotFoundException(MessageEntityName, externalId); CardDto? card = await cardStore.GetCardBySourceAsync(source, ct); string? text = await FindTextAsync(dialog, externalId, card, ct); if (string.IsNullOrWhiteSpace(text)) { - return null; // 404: исходная запись не найдена + throw new NotFoundException(MessageEntityName, externalId); } if (normalized == ActionSkip) diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs index d845dd0..2a79770 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs @@ -4,6 +4,7 @@ using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Parse; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Pipeline.Application.Services; @@ -48,6 +49,9 @@ public sealed class PipelineProcessingService( private const string DuplicateSource = "dup"; + // Имя сущности для текста ошибки «не найдено». + private const string RejectedEntityName = "Запись отсева"; + private static readonly HashSet SpamStages = new(StringComparer.Ordinal) { "spam_ml", @@ -154,17 +158,15 @@ public sealed class PipelineProcessingService( /// /// Id записи отсева (r_...). /// Причина возврата (trim, ≤500; пишется на запись для аудита). - /// null — записи нет (404); иначе результат: Error (400) либо {id, returned:true, returnedAt}. - public async Task ReturnAsync( + /// Результат: Error (400) либо {id, returned:true, returnedAt}. + /// Запись отсева не найдена. + public async Task ReturnAsync( string rejectedId, string reason, CancellationToken ct) { - RejectedItemDto? row = await store.GetAsync(rejectedId, ct); - if (row is null) - { - return null; - } + RejectedItemDto row = await store.GetAsync(rejectedId, ct) + ?? throw new NotFoundException(RejectedEntityName, rejectedId); if (row.Returned) { diff --git a/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs b/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs index 7a4a553..70ec5f5 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs @@ -1,6 +1,7 @@ using Deal.Modules.Tenants.Application.Abstractions; using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel; +using Deal.SharedKernel.Errors; using Deal.SharedKernel.Utilities; namespace Deal.Modules.Tenants.Application.Services; @@ -17,6 +18,9 @@ public sealed class TenantAdminService( // Случайные байты одноразового пароля владельца: 12 → ровно 16 символов Base64Url (как InviteCodeGenerator). private const int InitialPasswordRandomByteCount = 12; + // Имя сущности для текста ошибки «не найдено» (пользователь = тенант с его окружением). + private const string TenantEntityName = "Пользователь"; + /// /// Создаёт тенанта оператором /// @@ -103,14 +107,12 @@ public sealed class TenantAdminService( /// Детали тенанта с пользователями /// /// Идентификатор тенанта. - /// Детали и пользователи тенанта (по CreatedAt) или null, если тенанта нет. - public async Task GetAsync(Guid id, CancellationToken ct) + /// Детали и пользователи тенанта (по CreatedAt). + /// Тенант не найден. + public async Task GetAsync(Guid id, CancellationToken ct) { - var tenant = await tenantRepository.FindByIdAsync(id, ct); - if (tenant is null) - { - return null; - } + var tenant = await tenantRepository.FindByIdAsync(id, ct) + ?? throw new NotFoundException(TenantEntityName, id.ToString("N")); IReadOnlyList users = await authStore.ListUsersByTenantIdAsync(id, ct); return new TenantDetailDto(tenant.Id, tenant.Name, tenant.Status, tenant.CreatedAt, users); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs index d6ec709..f008aa6 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs @@ -53,13 +53,12 @@ public sealed class CardsServiceTests } [Fact] - public async Task GetCard_Missing_ReturnsNull() + public async Task GetCard_Missing_ThrowsNotFound() { (CardsService service, _, _, _) = Create(); - CardDto? card = await service.GetCardAsync("l_missing", CancellationToken.None); - - Assert.Null(card); + await Assert.ThrowsAsync( + () => service.GetCardAsync("l_missing", CancellationToken.None)); } @@ -146,15 +145,14 @@ public sealed class CardsServiceTests } [Fact] - public async Task Move_CardMissing_ReturnsNullLead() + public async Task Move_CardMissing_ThrowsNotFound() { (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); - CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None); + await Assert.ThrowsAsync( + () => service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None)); - Assert.Null(result.Error); - Assert.Null(result.Card); // эндпоинт отвечает 404 «Карточка не найдена» Assert.Empty(store.Moves); Assert.Empty(ml.Pushed); } diff --git a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs index 8bcd7bd..b33382b 100644 --- a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs @@ -3,10 +3,11 @@ using Deal.Modules.Cards.Application.Dtos; using Deal.Modules.Cards.Application.Models; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Models; -using Deal.Tests.Unit.Support; using Deal.Modules.Kanban.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Infrastructure; @@ -32,7 +33,6 @@ public sealed class CardMoverTests CardMoveResultDto result = await mover.MoveAsync("c_1", CardsDefaultContainers.Planned, UserMove, CancellationToken.None); Assert.Null(result.Error); - Assert.True(result.Exists); CardDto card = Assert.Single(store.CardDtos); Assert.Equal(CardsDefaultContainers.Planned, card.Col); Assert.Null(card.Reminder); // move по стадии сбрасывает напоминание @@ -50,7 +50,6 @@ public sealed class CardMoverTests CardMoveResultDto result = await mover.MoveAsync("c_1", "b_py", UserMove, CancellationToken.None); Assert.Null(result.Error); - Assert.True(result.Exists); Assert.Equal("b_py", Assert.Single(store.CardDtos).Col); Assert.Single(store.Moves); // журнал CardMoves дашборд-переноса } @@ -64,18 +63,15 @@ public sealed class CardMoverTests CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None); Assert.Equal(CardsService.MoveTargetInvalidDetail, result.Error); - Assert.True(result.Exists); // ошибка важнее признака наличия } [Fact] - public async Task Move_ToStage_CardMissing_ReportsNotFound() + public async Task Move_ToStage_CardMissing_ThrowsNotFound() { (CardMover mover, _) = Create(); - CardMoveResultDto result = await mover.MoveAsync("c_ghost", CardsDefaultContainers.Planned, UserMove, CancellationToken.None); - - Assert.Null(result.Error); - Assert.False(result.Exists); + await Assert.ThrowsAsync( + () => mover.MoveAsync("c_ghost", CardsDefaultContainers.Planned, UserMove, CancellationToken.None)); } private static (CardMover Mover, TestKanjStore Store) Create() diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs index 3f68ee5..61f3931 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs @@ -1,7 +1,8 @@ using Deal.Modules.Discovery.Application.Exceptions; using Deal.Modules.Discovery.Application.Models; -using Deal.Tests.Unit.Support; using Deal.Modules.Discovery.Application.Services; +using Deal.SharedKernel.Errors; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Modules.Discovery; @@ -114,14 +115,13 @@ public sealed class DiscoveryCandidatesServiceTests } [Fact] - public async Task Add_MissingTask_ReturnsNullWithoutLog() + public async Task Add_MissingTask_ThrowsNotFoundWithoutLog() { (DiscoveryCandidatesService service, TestDiscoveryStore store) = Create(); - DiscoveryCandidateDto? candidate = await service.AddAsync( - "dt_missing", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None); + await Assert.ThrowsAsync(() => service.AddAsync( + "dt_missing", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None)); - Assert.Null(candidate); Assert.Empty(store.Candidates); Assert.Empty(store.Log); } @@ -175,13 +175,12 @@ public sealed class DiscoveryCandidatesServiceTests } [Fact] - public async Task MarkJoined_MissingCandidate_ReturnsNull() + public async Task MarkJoined_MissingCandidate_ThrowsNotFound() { (DiscoveryCandidatesService service, _) = Create(); - DiscoveryCandidateDto? candidate = await service.MarkJoinedAsync("-1001", auto: true, CancellationToken.None); - - Assert.Null(candidate); + await Assert.ThrowsAsync( + () => service.MarkJoinedAsync("-1001", auto: true, CancellationToken.None)); } [Fact] @@ -238,14 +237,12 @@ public sealed class DiscoveryCandidatesServiceTests } [Fact] - public async Task MarkRejected_MissingCandidate_ReturnsNull() + public async Task MarkRejected_MissingCandidate_ThrowsNotFound() { (DiscoveryCandidatesService service, _) = Create(); - DiscoveryCandidateDto? candidate = await service.MarkRejectedAsync( - "-1001", "причина", CancellationToken.None); - - Assert.Null(candidate); + await Assert.ThrowsAsync( + () => service.MarkRejectedAsync("-1001", "причина", CancellationToken.None)); } [Fact] @@ -315,17 +312,15 @@ public sealed class DiscoveryCandidatesServiceTests } [Fact] - public async Task Set_WrongTask_ReturnsNull() + public async Task Set_WrongTask_ThrowsNotFound() { (DiscoveryCandidatesService service, TestDiscoveryStore store) = Create(); store.SeedTask(Task("dt_1")); store.SeedTask(Task("dt_2")); store.SeedCandidate(Candidate("-1001", "dt_1")); - DiscoveryCandidateDto? candidate = await service.SetAsync( - "dt_2", "-1001", new DiscoveryCandidatePatch { Participants = 10 }, CancellationToken.None); - - Assert.Null(candidate); // кандидат другой задачи (python L465–466) + await Assert.ThrowsAsync(() => service.SetAsync( + "dt_2", "-1001", new DiscoveryCandidatePatch { Participants = 10 }, CancellationToken.None)); } [Fact] diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs index ea7bd08..b6974a8 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs @@ -1,9 +1,10 @@ using Deal.Modules.Kanban.Application.Models; -using Deal.Tests.Unit.Support; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Settings.Application.Models; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Modules.Kanban; @@ -47,14 +48,12 @@ public sealed class CardsServiceRemindersTests } [Fact] - public async Task Set_MissingCard_Returns404EvenWhenDisabled() + public async Task Set_MissingCard_ThrowsNotFoundEvenWhenDisabled() { (CardsService service, _, _) = Create(remindersEnabled: false); - CardResultDto result = await service.SetReminderAsync("c_missing", NowMs(), CancellationToken.None); - - Assert.Null(result.Error); - Assert.Null(result.Card); // эндпоинт отвечает 404 «Карточка не найдена» + await Assert.ThrowsAsync( + () => service.SetReminderAsync("c_missing", NowMs(), CancellationToken.None)); } [Fact] @@ -73,25 +72,23 @@ public sealed class CardsServiceRemindersTests [Fact] - public async Task Clear_WithReminder_ClearsItAndReturnsTrue() + public async Task Clear_WithReminder_ClearsIt() { (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) }); - bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None); + await service.ClearReminderAsync("c_1", CancellationToken.None); - Assert.True(cleared); Assert.Null(Assert.Single(store.CardDtos).Reminder); // reminder_at=NULL, fired сброшен } [Fact] - public async Task Clear_MissingCard_ReturnsFalse() + public async Task Clear_MissingCard_ThrowsNotFound() { (CardsService service, _, _) = Create(); - bool cleared = await service.ClearReminderAsync("c_missing", CancellationToken.None); - - Assert.False(cleared); // эндпоинт отвечает 404 «Карточка не найдена» + await Assert.ThrowsAsync( + () => service.ClearReminderAsync("c_missing", CancellationToken.None)); } [Fact] @@ -100,9 +97,8 @@ public sealed class CardsServiceRemindersTests (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() + DayMs) }); - bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None); + await service.ClearReminderAsync("c_1", CancellationToken.None); - Assert.True(cleared); Assert.Null(Assert.Single(store.CardDtos).Reminder); } @@ -114,22 +110,20 @@ public sealed class CardsServiceRemindersTests store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) }); long beforeMs = NowMs(); - bool snoozed = await service.SnoozeReminderAsync("c_1", CancellationToken.None); + await service.SnoozeReminderAsync("c_1", CancellationToken.None); - Assert.True(snoozed); long afterMs = NowMs(); CardReminderDto reminder = Assert.Single(store.CardDtos).Reminder!; Assert.InRange(reminder.At, beforeMs + DayMs, afterMs + DayMs); // now + 24 ч } [Fact] - public async Task Snooze_MissingCard_ReturnsFalse() + public async Task Snooze_MissingCard_ThrowsNotFound() { (CardsService service, _, _) = Create(); - bool snoozed = await service.SnoozeReminderAsync("c_missing", CancellationToken.None); - - Assert.False(snoozed); // эндпоинт отвечает 404 «Карточка не найдена» + await Assert.ThrowsAsync( + () => service.SnoozeReminderAsync("c_missing", CancellationToken.None)); } [Fact] @@ -138,9 +132,8 @@ public sealed class CardsServiceRemindersTests (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) }); - bool snoozed = await service.SnoozeReminderAsync("c_1", CancellationToken.None); + await service.SnoozeReminderAsync("c_1", CancellationToken.None); - Assert.True(snoozed); Assert.NotNull(Assert.Single(store.CardDtos).Reminder); } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs index 9a2e827..9551349 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs @@ -4,6 +4,7 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Support; @@ -241,12 +242,11 @@ public sealed class MlReviewServiceTests } [Fact] - public async Task Apply_MessageNotFound_ReturnsNull() + public async Task Apply_MessageNotFound_ThrowsNotFound() { MlReviewService service = Create(new TestPipelineStore(), new TestKanjStore(), new TestMlClient(), out _); - MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None); - - Assert.Null(result); + await Assert.ThrowsAsync( + () => service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None)); } } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs index 6ea34d0..6b31631 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs @@ -2,6 +2,7 @@ using System.Globalization; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Support; @@ -165,13 +166,13 @@ public sealed class PipelineProcessingServiceTests [Fact] - public async Task ReturnAsync_RecordNotFound_ReturnsNull() + public async Task ReturnAsync_RecordNotFound_ThrowsNotFound() { (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create(); - RejectReturnResultDto? result = await service.ReturnAsync("r_missing", string.Empty, CancellationToken.None); + await Assert.ThrowsAsync( + () => service.ReturnAsync("r_missing", string.Empty, CancellationToken.None)); - Assert.Null(result); // эндпоинт ответит 404 «Запись не найдена» (Ruling 10) Assert.Empty(store.Queue); Assert.Empty(ml.Pushed); } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs index e0cb343..111639f 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs @@ -1,7 +1,8 @@ +using Deal.Modules.Tenants.Application.Abstractions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Support; -using Deal.Modules.Tenants.Application.Abstractions; namespace Deal.Tests.Unit.Modules.Tenants; @@ -148,11 +149,12 @@ public sealed class TenantAdminServiceTests } [Fact] - public async Task GetAsync_ForUnknownTenant_ReturnsNull() + public async Task GetAsync_ForUnknownTenant_ThrowsNotFound() { var service = NewService(new TestTenantStore(), new TestAuthStore()); - Assert.Null(await service.GetAsync(Guid.NewGuid(), CancellationToken.None)); + await Assert.ThrowsAsync( + () => service.GetAsync(Guid.NewGuid(), CancellationToken.None)); } [Fact] diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs index 871d678..21fa9a5 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs @@ -40,13 +40,12 @@ public sealed class CardsServiceSelectedTests } [Fact] - public async Task Get_Missing_ReturnsNull() + public async Task Get_Missing_ThrowsNotFound() { (CardsService service, _, _, _) = Create(); - CardDto? card = await service.GetCardAsync("c_missing", CancellationToken.None); - - Assert.Null(card); + await Assert.ThrowsAsync( + () => service.GetCardAsync("c_missing", CancellationToken.None)); } @@ -319,14 +318,12 @@ public sealed class CardsServiceSelectedTests } [Fact] - public async Task Move_CardMissing_ReturnsNullCard() + public async Task Move_CardMissing_ThrowsNotFound() { (CardsService service, _, _, _) = Create(); - CardResultDto result = await service.MoveStageCardAsync("c_missing", "work", CancellationToken.None); - - Assert.Null(result.Error); - Assert.Null(result.Card); // 404 «Карточка не найдена» — текст у эндпоинта + await Assert.ThrowsAsync( + () => service.MoveStageCardAsync("c_missing", "work", CancellationToken.None)); } @@ -465,14 +462,13 @@ public sealed class CardsServiceSelectedTests } [Fact] - public async Task AddLink_CardMissing_ReturnsNullCardBeforeUrlValidation() + public async Task AddLink_CardMissing_ThrowsBeforeUrlValidation() { (CardsService service, TestKanjStore store, _, _) = Create(); - CardResultDto result = await service.AddLinkAsync("c_missing", string.Empty, "example.com", CancellationToken.None); + await Assert.ThrowsAsync( + () => service.AddLinkAsync("c_missing", string.Empty, "example.com", CancellationToken.None)); - Assert.Null(result.Error); - Assert.Null(result.Card); // 404-семантика: карточки нет раньше валидации url Assert.Empty(store.CardDtos); } @@ -514,14 +510,13 @@ public sealed class CardsServiceSelectedTests } [Fact] - public async Task RemoveLink_CardMissing_ReturnsNullCard() + public async Task RemoveLink_CardMissing_ThrowsNotFound() { (CardsService service, TestKanjStore store, _, _) = Create(); - CardResultDto result = await service.RemoveLinkAsync("c_missing", "pl_1", CancellationToken.None); + await Assert.ThrowsAsync( + () => service.RemoveLinkAsync("c_missing", "pl_1", CancellationToken.None)); - Assert.Null(result.Error); - Assert.Null(result.Card); // эндпоинт отвечает 404 «Карточка не найдена» Assert.Empty(store.CardDtos); }