Перевести задачи Discovery на NotFoundException
ci / build-test (pull_request) Successful in 2m52s

DiscoveryTasksService (get/patch/delete/start/pause) бросает NotFoundException вместо null; эндпоинты отдают 404 через общий обработчик.
This commit is contained in:
2026-09-13 14:32:27 +03:00
parent 685c5fdf46
commit 8661faea70
3 changed files with 61 additions and 77 deletions
@@ -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<DiscoveryTasksService>();
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<DiscoveryTasksService>();
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<IResult> StartTaskAsync(
@@ -186,8 +185,8 @@ public static class DiscoveryEndpoints
try
{
DiscoveryTasksService tasks = context.RequestServices.GetRequiredService<DiscoveryTasksService>();
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<DiscoveryTasksService>();
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<IResult> GenerateKeywordsAsync(
@@ -221,11 +220,7 @@ public static class DiscoveryEndpoints
}
DiscoveryTasksService tasks = context.RequestServices.GetRequiredService<DiscoveryTasksService>();
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<ISettingsStore>();
if (!await ReadAiEnabledAsync(settings, ct))
@@ -268,11 +263,7 @@ public static class DiscoveryEndpoints
}
DiscoveryTasksService tasks = context.RequestServices.GetRequiredService<DiscoveryTasksService>();
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<DiscoveryCandidatesService>();
IReadOnlyList<DiscoveryCandidateDto> items = await candidates.ListAsync(task_id, status, ct);
@@ -403,11 +394,7 @@ public static class DiscoveryEndpoints
}
DiscoveryTasksService tasks = context.RequestServices.GetRequiredService<DiscoveryTasksService>();
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<DiscoveryLogService>();
IReadOnlyList<DiscoveryLogDto> items = await log.TaskLogAsync(task_id, ct);
@@ -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;
/// </summary>
public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGuard planGuard, ISettingsStore settings)
{
// Имя сущности для текста ошибки «не найдено».
private const string TaskEntityName = "Задача поиска";
/// <summary>
/// 400 create: пустое название после Trim.
/// </summary>
@@ -35,10 +39,12 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
/// Одна задача по id.
/// </summary>
/// <param name="taskId">Id задачи (<c>dt_...</c>).</param>
/// <returns>Задача или null (404 «Задача не найдена» у эндпоинта).</returns>
public Task<DiscoveryTaskDto?> GetAsync(string taskId, CancellationToken ct)
/// <returns>Задача.</returns>
/// <exception cref="NotFoundException">Задача не найдена.</exception>
public async Task<DiscoveryTaskDto> GetAsync(string taskId, CancellationToken ct)
{
return store.GetTaskAsync(taskId, ct);
return await store.GetTaskAsync(taskId, ct).ConfigureAwait(false)
?? throw new NotFoundException(TaskEntityName, taskId);
}
/// <summary>
@@ -102,18 +108,16 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
/// </summary>
/// <param name="taskId">Id задачи (<c>dt_...</c>).</param>
/// <param name="patch">Изменяемые поля (null — не меняется).</param>
/// <returns>Обновлённая задача либо null (404 «Задача не найдена»).</returns>
/// <returns>Обновлённая задача.</returns>
/// <exception cref="NotFoundException">Задача не найдена.</exception>
/// <exception cref="DiscoveryValidationException">Новый план вне границ / бюджет исчерпан.</exception>
public async Task<DiscoveryTaskDto?> PatchAsync(
public async Task<DiscoveryTaskDto> 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);
}
/// <summary>
/// Удаляет задачу вместе с кандидатами и логом.
/// </summary>
/// <param name="taskId">Id задачи (<c>dt_...</c>).</param>
/// <returns>True — задача удалена; false — строки нет (404 у эндпоинта).</returns>
public async Task<bool> DeleteAsync(string taskId, CancellationToken ct)
/// <exception cref="NotFoundException">Задача не найдена.</exception>
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);
}
/// <summary>
/// Запускает поиск
/// </summary>
/// <param name="taskId">Id задачи (<c>dt_...</c>).</param>
/// <returns>Задача в running либо null (404).</returns>
/// <returns>Задача в running.</returns>
/// <exception cref="NotFoundException">Задача не найдена.</exception>
/// <exception cref="DiscoveryValidationException">Ключевых слов нет.</exception>
public async Task<DiscoveryTaskDto?> StartAsync(string taskId, CancellationToken ct)
public async Task<DiscoveryTaskDto> 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);
}
/// <summary>
/// Ставит задачу на паузу.
/// </summary>
/// <param name="taskId">Id задачи (<c>dt_...</c>).</param>
/// <returns>Задача в paused либо null (404).</returns>
public async Task<DiscoveryTaskDto?> PauseAsync(string taskId, CancellationToken ct)
/// <returns>Задача в paused.</returns>
/// <exception cref="NotFoundException">Задача не найдена.</exception>
public async Task<DiscoveryTaskDto> 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);
}
/// <summary>
@@ -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<NotFoundException>(
() => 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<NotFoundException>(
() => 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<NotFoundException>(
() => service.DeleteAsync("dt_missing", CancellationToken.None));
}
[Fact]