From 8661faea704409322cac85949c470c2538f231d7 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 14:32:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8=20Discovery?= =?UTF-8?q?=20=D0=BD=D0=B0=20NotFoundException?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DiscoveryTasksService (get/patch/delete/start/pause) бросает NotFoundException вместо null; эндпоинты отдают 404 через общий обработчик. --- .../Deal.Api/Endpoints/DiscoveryEndpoints.cs | 37 ++++------ .../Services/DiscoveryTasksService.cs | 72 +++++++++---------- .../Discovery/DiscoveryTasksServiceTests.cs | 29 ++++---- 3 files changed, 61 insertions(+), 77 deletions(-) diff --git a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs index 25a9d39..f46981d 100644 --- a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs @@ -55,8 +55,7 @@ public static class DiscoveryEndpoints // Путь лога задачи (GET). private const string TaskLogPath = "/tasks/{task_id}/log"; - private const string TaskNotFoundDetail = "Задача не найдена"; - + // 404: кандидат не найден. private const string CandidateNotFoundDetail = "Кандидат не найден"; private const string AlreadyJoinedDetail = "Уже вступили в этот источник"; @@ -149,8 +148,8 @@ public static class DiscoveryEndpoints try { DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - DiscoveryTaskDto? task = await tasks.PatchAsync(task_id, ToPatch(body), ct); - return task is null ? EndpointResults.NotFound(TaskNotFoundDetail) : Results.Ok(task); + DiscoveryTaskDto task = await tasks.PatchAsync(task_id, ToPatch(body), ct); + return Results.Ok(task); } catch (DiscoveryValidationException exception) { @@ -169,8 +168,8 @@ public static class DiscoveryEndpoints } DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - bool deleted = await tasks.DeleteAsync(task_id, ct); - return deleted ? Results.Ok(new { ok = true }) : EndpointResults.NotFound(TaskNotFoundDetail); + await tasks.DeleteAsync(task_id, ct); + return Results.Ok(new { ok = true }); } private static async Task StartTaskAsync( @@ -186,8 +185,8 @@ public static class DiscoveryEndpoints try { DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - DiscoveryTaskDto? task = await tasks.StartAsync(task_id, ct); - return task is null ? EndpointResults.NotFound(TaskNotFoundDetail) : Results.Ok(task); + DiscoveryTaskDto task = await tasks.StartAsync(task_id, ct); + return Results.Ok(task); } catch (DiscoveryValidationException exception) { @@ -206,8 +205,8 @@ public static class DiscoveryEndpoints } DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - DiscoveryTaskDto? task = await tasks.PauseAsync(task_id, ct); - return task is null ? EndpointResults.NotFound(TaskNotFoundDetail) : Results.Ok(task); + DiscoveryTaskDto task = await tasks.PauseAsync(task_id, ct); + return Results.Ok(task); } private static async Task GenerateKeywordsAsync( @@ -221,11 +220,7 @@ public static class DiscoveryEndpoints } DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - DiscoveryTaskDto? task = await tasks.GetAsync(task_id, ct); - if (task is null) - { - return EndpointResults.NotFound(TaskNotFoundDetail); - } + DiscoveryTaskDto task = await tasks.GetAsync(task_id, ct); ISettingsStore settings = context.RequestServices.GetRequiredService(); if (!await ReadAiEnabledAsync(settings, ct)) @@ -268,11 +263,7 @@ public static class DiscoveryEndpoints } DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - DiscoveryTaskDto? task = await tasks.GetAsync(task_id, ct); - if (task is null) - { - return EndpointResults.NotFound(TaskNotFoundDetail); - } + DiscoveryTaskDto task = await tasks.GetAsync(task_id, ct); DiscoveryCandidatesService candidates = context.RequestServices.GetRequiredService(); IReadOnlyList items = await candidates.ListAsync(task_id, status, ct); @@ -403,11 +394,7 @@ public static class DiscoveryEndpoints } DiscoveryTasksService tasks = context.RequestServices.GetRequiredService(); - DiscoveryTaskDto? task = await tasks.GetAsync(task_id, ct); - if (task is null) - { - return EndpointResults.NotFound(TaskNotFoundDetail); - } + DiscoveryTaskDto task = await tasks.GetAsync(task_id, ct); DiscoveryLogService log = context.RequestServices.GetRequiredService(); IReadOnlyList items = await log.TaskLogAsync(task_id, ct); diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs index 3a6c515..831b150 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs @@ -4,6 +4,7 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Discovery.Application.Services; @@ -12,6 +13,9 @@ namespace Deal.Modules.Discovery.Application.Services; /// public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGuard planGuard, ISettingsStore settings) { + // Имя сущности для текста ошибки «не найдено». + private const string TaskEntityName = "Задача поиска"; + /// /// 400 create: пустое название после Trim. /// @@ -35,10 +39,12 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu /// Одна задача по id. /// /// Id задачи (dt_...). - /// Задача или null (404 «Задача не найдена» у эндпоинта). - public Task GetAsync(string taskId, CancellationToken ct) + /// Задача. + /// Задача не найдена. + public async Task GetAsync(string taskId, CancellationToken ct) { - return store.GetTaskAsync(taskId, ct); + return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); } /// @@ -102,18 +108,16 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu /// /// Id задачи (dt_...). /// Изменяемые поля (null — не меняется). - /// Обновлённая задача либо null (404 «Задача не найдена»). + /// Обновлённая задача. + /// Задача не найдена. /// Новый план вне границ / бюджет исчерпан. - public async Task PatchAsync( + public async Task PatchAsync( string taskId, DiscoveryTaskPatch patch, CancellationToken ct) { - DiscoveryTaskDto? current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); - if (current is null) - { - return null; - } + DiscoveryTaskDto current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); DiscoveryTaskPatch normalized = NormalizeTaskPatch(patch); if (normalized.PlanJoins is int newPlan) @@ -131,38 +135,34 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu } await store.PatchTaskAsync(taskId, normalized, ct).ConfigureAwait(false); - return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); + return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); } /// /// Удаляет задачу вместе с кандидатами и логом. /// /// Id задачи (dt_...). - /// True — задача удалена; false — строки нет (404 у эндпоинта). - public async Task DeleteAsync(string taskId, CancellationToken ct) + /// Задача не найдена. + public async Task DeleteAsync(string taskId, CancellationToken ct) { - DiscoveryTaskDto? current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); - if (current is null) - { - return false; - } + DiscoveryTaskDto current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); - return await store.DeleteTaskAsync(taskId, ct).ConfigureAwait(false); + await store.DeleteTaskAsync(current.Id, ct).ConfigureAwait(false); } /// /// Запускает поиск /// /// Id задачи (dt_...). - /// Задача в running либо null (404). + /// Задача в running. + /// Задача не найдена. /// Ключевых слов нет. - public async Task StartAsync(string taskId, CancellationToken ct) + public async Task StartAsync(string taskId, CancellationToken ct) { - DiscoveryTaskDto? current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); - if (current is null) - { - return null; - } + DiscoveryTaskDto current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); if (current.Keywords.Count == 0) { @@ -171,24 +171,24 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu bool resetProgress = DiscoveryTaskStatuses.IsFinished(current.Status); await store.SetTaskRunningAsync(taskId, resetProgress, ct).ConfigureAwait(false); - return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); + return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); } /// /// Ставит задачу на паузу. /// /// Id задачи (dt_...). - /// Задача в paused либо null (404). - public async Task PauseAsync(string taskId, CancellationToken ct) + /// Задача в paused. + /// Задача не найдена. + public async Task PauseAsync(string taskId, CancellationToken ct) { - DiscoveryTaskDto? current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); - if (current is null) - { - return null; - } + DiscoveryTaskDto current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); - await store.SetTaskPausedAsync(taskId, ct).ConfigureAwait(false); - return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); + await store.SetTaskPausedAsync(current.Id, ct).ConfigureAwait(false); + return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false) + ?? throw new NotFoundException(TaskEntityName, taskId); } /// diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs index d00bd44..d6ff143 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs @@ -1,8 +1,9 @@ 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.Modules.Settings; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Modules.Discovery; @@ -131,14 +132,13 @@ public sealed class DiscoveryTasksServiceTests } [Fact] - public async Task Patch_MissingTask_ReturnsNull() + public async Task Patch_MissingTask_ThrowsNotFound() { (DiscoveryTasksService service, _, _) = Create(); - DiscoveryTaskDto? patched = await service.PatchAsync( - "dt_missing", new DiscoveryTaskPatch { Name = "Новое" }, CancellationToken.None); - - Assert.Null(patched); + await Assert.ThrowsAsync( + () => service.PatchAsync( + "dt_missing", new DiscoveryTaskPatch { Name = "Новое" }, CancellationToken.None)); } [Fact] @@ -201,13 +201,12 @@ public sealed class DiscoveryTasksServiceTests } [Fact] - public async Task Start_MissingTask_ReturnsNull() + public async Task Start_MissingTask_ThrowsNotFound() { (DiscoveryTasksService service, _, _) = Create(); - DiscoveryTaskDto? task = await service.StartAsync("dt_missing", CancellationToken.None); - - Assert.Null(task); + await Assert.ThrowsAsync( + () => service.StartAsync("dt_missing", CancellationToken.None)); } [Fact] @@ -281,9 +280,8 @@ public sealed class DiscoveryTasksServiceTests store.SeedCandidate(Candidate("c_2", "dt_2")); await store.Store.UpsertBlacklistAsync("c_1", "Источник", "причина", CancellationToken.None); - bool deleted = await service.DeleteAsync("dt_1", CancellationToken.None); + await service.DeleteAsync("dt_1", CancellationToken.None); - Assert.True(deleted); Assert.Single(store.Tasks); // dt_2 осталась Assert.Equal("dt_2", Assert.Single(store.Tasks).Id); Assert.Single(store.Candidates); // кандидат dt_2 остался @@ -292,13 +290,12 @@ public sealed class DiscoveryTasksServiceTests } [Fact] - public async Task Delete_MissingTask_ReturnsFalse() + public async Task Delete_MissingTask_ThrowsNotFound() { (DiscoveryTasksService service, _, _) = Create(); - bool deleted = await service.DeleteAsync("dt_missing", CancellationToken.None); - - Assert.False(deleted); + await Assert.ThrowsAsync( + () => service.DeleteAsync("dt_missing", CancellationToken.None)); } [Fact]