Перевести «не найдено» кандидатов Discovery на исключения
DiscoveryCandidatesService (get/set/status/mark_joined/mark_rejected/add при отсутствии задачи) бросает NotFoundException; skip-ветки add оставлены null. Эндпоинты без локальных 404-проверок.
This commit is contained in:
@@ -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<DiscoveryCandidatesService>();
|
||||
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<DiscoveryCandidatesService>();
|
||||
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)
|
||||
{
|
||||
|
||||
+46
-40
@@ -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 = "Задача поиска";
|
||||
|
||||
/// <summary>
|
||||
/// 400 mark_rejected
|
||||
/// </summary>
|
||||
@@ -53,10 +58,12 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// Кандидат по dialog_id.
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <returns>Кандидат или null (404 «Кандидат не найден»).</returns>
|
||||
public Task<DiscoveryCandidateDto?> GetAsync(string dialogId, CancellationToken ct)
|
||||
/// <returns>Кандидат.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
public async Task<DiscoveryCandidateDto> GetAsync(string dialogId, CancellationToken ct)
|
||||
{
|
||||
return store.GetCandidateAsync(dialogId, ct);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -134,18 +139,21 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="taskId">Id задачи.</param>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="patch">Изменяемые поля (null — не менять).</param>
|
||||
/// <returns>Обновлённый кандидат либо null — задачи/кандидата нет (или кандидат другой задачи).</returns>
|
||||
public async Task<DiscoveryCandidateDto?> SetAsync(
|
||||
/// <returns>Обновлённый кандидат.</returns>
|
||||
/// <exception cref="NotFoundException">Задача или кандидат не найдены.</exception>
|
||||
public async Task<DiscoveryCandidateDto> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,9 +176,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="status">Новый статус: new|review.</param>
|
||||
/// <returns>Обновлённый кандидат либо null (кандидата нет).</returns>
|
||||
/// <returns>Обновлённый кандидат.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
/// <exception cref="DiscoveryValidationException">Статус не new/review.</exception>
|
||||
public async Task<DiscoveryCandidateDto?> SetStatusAsync(
|
||||
public async Task<DiscoveryCandidateDto> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -203,17 +211,15 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="auto">True — авто-вступление воркера; false — ручное.</param>
|
||||
/// <returns>Кандидат в joined либо null (кандидата нет). Повторный вызов для joined — идемпотентен.</returns>
|
||||
public async Task<DiscoveryCandidateDto?> MarkJoinedAsync(
|
||||
/// <returns>Кандидат в joined. Повторный вызов для joined — идемпотентен.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
public async Task<DiscoveryCandidateDto> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -232,18 +239,16 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="reason">Причина отклонения (в лог/чёрный список; «отклонено вручную» — эндпоинт reject).</param>
|
||||
/// <returns>Кандидат в rejected либо null (кандидата нет). Повторный вызов для rejected — идемпотентен.</returns>
|
||||
/// <returns>Кандидат в rejected. Повторный вызов для rejected — идемпотентен.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
/// <exception cref="DiscoveryValidationException">Источник уже joined — отклонить нельзя.</exception>
|
||||
public async Task<DiscoveryCandidateDto?> MarkRejectedAsync(
|
||||
public async Task<DiscoveryCandidateDto> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+14
-19
@@ -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<NotFoundException>(() => 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<NotFoundException>(
|
||||
() => 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<NotFoundException>(
|
||||
() => 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<NotFoundException>(() => service.SetAsync(
|
||||
"dt_2", "-1001", new DiscoveryCandidatePatch { Participants = 10 }, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user