Перевести «не найдено» ML/Pipeline на NotFoundException
MlReviewService.ApplyAsync (исходное сообщение) и PipelineProcessingService.ReturnAsync (запись отсева) бросают NotFoundException; эндпоинты без 404-проверок.
This commit is contained in:
@@ -37,8 +37,6 @@ public static class MlEndpoints
|
||||
|
||||
private const string EnterTextDetail = "Введите текст";
|
||||
|
||||
private const string MessageNotFoundDetail = "Исходное сообщение не найдено";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует группу /api/ml
|
||||
/// </summary>
|
||||
@@ -141,12 +139,7 @@ public static class MlEndpoints
|
||||
}
|
||||
|
||||
MlReviewService review = context.RequestServices.GetRequiredService<MlReviewService>();
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -140,11 +138,7 @@ public static class PipelineEndpoints
|
||||
}
|
||||
|
||||
PipelineProcessingService processing = context.RequestServices.GetRequiredService<PipelineProcessingService>();
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -157,8 +161,9 @@ public sealed class MlReviewService(
|
||||
/// <param name="dialogId">Оригинал источника (OriginRef) записи.</param>
|
||||
/// <param name="msgId">Внешний id записи в источнике.</param>
|
||||
/// <param name="action">Действие: <c>skip</c> | <c>spam</c> | <c>board:<id></c>.</param>
|
||||
/// <returns>Результат решения; null — исходная запись не найдена (404-семантика эндпоинта).</returns>
|
||||
public async Task<MlApplyResult?> ApplyAsync(
|
||||
/// <returns>Результат решения.</returns>
|
||||
/// <exception cref="NotFoundException">Исходная запись не найдена.</exception>
|
||||
public async Task<MlApplyResult> 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)
|
||||
|
||||
@@ -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<string> SpamStages = new(StringComparer.Ordinal)
|
||||
{
|
||||
"spam_ml",
|
||||
@@ -154,17 +158,15 @@ public sealed class PipelineProcessingService(
|
||||
/// </summary>
|
||||
/// <param name="rejectedId">Id записи отсева (<c>r_...</c>).</param>
|
||||
/// <param name="reason">Причина возврата (trim, ≤500; пишется на запись для аудита).</param>
|
||||
/// <returns>null — записи нет (404); иначе результат: Error (400) либо {id, returned:true, returnedAt}.</returns>
|
||||
public async Task<RejectReturnResultDto?> ReturnAsync(
|
||||
/// <returns>Результат: Error (400) либо {id, returned:true, returnedAt}.</returns>
|
||||
/// <exception cref="NotFoundException">Запись отсева не найдена.</exception>
|
||||
public async Task<RejectReturnResultDto> 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)
|
||||
{
|
||||
|
||||
@@ -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<NotFoundException>(
|
||||
() => service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NotFoundException>(
|
||||
() => service.ReturnAsync("r_missing", string.Empty, CancellationToken.None));
|
||||
|
||||
Assert.Null(result); // эндпоинт ответит 404 «Запись не найдена» (Ruling 10)
|
||||
Assert.Empty(store.Queue);
|
||||
Assert.Empty(ml.Pushed);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user