This commit is contained in:
@@ -6,7 +6,6 @@ using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Settings.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
@@ -109,7 +108,7 @@ public sealed class CardComposerTests
|
||||
[Fact]
|
||||
public async Task BuildAsync_BoardWithoutRules_PlacesCardIntoBoardWithEmptyMatchHits()
|
||||
{
|
||||
(CardComposer composer, FakeKanjStore store, _) = Create();
|
||||
(CardComposer composer, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(new ContainerDto { Id = "b_py" });
|
||||
|
||||
CardSnapshot snapshot = await composer.BuildAsync(
|
||||
@@ -125,7 +124,7 @@ public sealed class CardComposerTests
|
||||
[Fact]
|
||||
public async Task BuildAsync_BoardWithNonMatchingRules_FallsBackToInbox()
|
||||
{
|
||||
(CardComposer composer, FakeKanjStore store, _) = Create();
|
||||
(CardComposer composer, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(BoardWithRules("b_wpf", new[] { "wpf" }));
|
||||
|
||||
CardSnapshot snapshot = await composer.BuildAsync(
|
||||
@@ -141,7 +140,7 @@ public sealed class CardComposerTests
|
||||
[Fact]
|
||||
public async Task BuildAsync_BoardWithMatchingRules_PlacesCardAndComputesMatchHits()
|
||||
{
|
||||
(CardComposer composer, FakeKanjStore store, _) = Create();
|
||||
(CardComposer composer, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(BoardWithRules("b_py", new[] { "python" }));
|
||||
|
||||
CardSnapshot snapshot = await composer.BuildAsync(
|
||||
@@ -232,11 +231,11 @@ public sealed class CardComposerTests
|
||||
|
||||
// Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок).
|
||||
// Возвращает: Кортеж (композитор, канбан-хранилище, KV-настройки).
|
||||
private static (CardComposer Composer, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||
private static (CardComposer Composer, TestKanjStore Store, TestSettingsStore Settings) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
return (new CardComposer(store, settings.Store), store, settings);
|
||||
return (new CardComposer(store.Store, settings.Store), store, settings);
|
||||
}
|
||||
|
||||
// Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
||||
|
||||
@@ -3,7 +3,6 @@ using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Contracts;
|
||||
@@ -17,7 +16,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task ListCards_NoCol_ReturnsAllOrderedByReceivedAtDesc()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", receivedAtMs: 100));
|
||||
store.SeedCard(Card("l_2", "b_x", receivedAtMs: 300));
|
||||
|
||||
@@ -30,7 +29,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task ListCards_ByColumn_ReturnsOnlyColumnCards()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox"));
|
||||
store.SeedCard(Card("l_2", KanbanColumns.Trash));
|
||||
|
||||
@@ -43,7 +42,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task GetCard_Existing_ReturnsCard()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", title: "Middle Python"));
|
||||
|
||||
CardDto? card = await service.GetCardAsync("l_1", CancellationToken.None);
|
||||
@@ -66,7 +65,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_ToBoardWithRules_UpdatesColumnAndComputesMatchHits()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Middle Python-разработчик на бота, 1600$."));
|
||||
|
||||
@@ -84,7 +83,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_ToBoard_WritesJournalAndPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
|
||||
|
||||
@@ -105,7 +104,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_ToInbox_WritesJournalWithoutPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", prevCol: "inbox"));
|
||||
|
||||
@@ -124,7 +123,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_ToUnknownBoard_Returns400Text()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
|
||||
|
||||
CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_ghost", CancellationToken.None);
|
||||
@@ -148,7 +147,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_CardMissing_ReturnsNullLead()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
|
||||
CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None);
|
||||
@@ -162,7 +161,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_SameColumn_NoOpWithoutJournalAndPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", isNew: true));
|
||||
|
||||
@@ -179,7 +178,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_ToBoardWithoutRules_MatchHitsEmptyButPushSent()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_free")); // правил нет
|
||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Любой текст"));
|
||||
|
||||
@@ -195,7 +194,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_FromBoardToBoard_PrevColIsOldBoardAndPushNewLabel()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_java", Rules(stack: new[] { "java" })));
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "b_java", sourceMsg: "Нужен Java-разработчик", prevCol: "inbox"));
|
||||
@@ -214,7 +213,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", sourceMsg: " "));
|
||||
|
||||
@@ -228,7 +227,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_EmptySourceAndTitle_JournalWithoutPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
store.SeedCard(Card("l_1", "inbox"));
|
||||
|
||||
@@ -241,7 +240,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_ToBoardWithGradeAndBudget_HitsIncludeGradeWordAndBudget()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedBoard(Board("b_mid", Rules(
|
||||
grade: new[] { "middle" },
|
||||
budget: new BudgetRangeDto(1000, 3000, "USD"))));
|
||||
@@ -263,7 +262,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_FromArchive_Returns400AndWritesNothing()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая вакансия", prevCol: "b_py"));
|
||||
|
||||
@@ -279,7 +278,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Move_FromTrash_Returns400AndWritesNothing()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Спам-текст", prevCol: "b_py"));
|
||||
|
||||
@@ -296,7 +295,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Trash_FromInbox_MovesToTrashWithJournalAndSpamPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Предлагаю услуги продвижения", isNew: true));
|
||||
|
||||
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
||||
@@ -318,7 +317,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Trash_AlreadyInTrash_NoOpWithoutJournalAndPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст"));
|
||||
|
||||
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
||||
@@ -331,7 +330,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Trash_FromArchive_WritesJournalWithoutPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
|
||||
|
||||
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
||||
@@ -358,7 +357,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Restore_FromTrash_ToPrevColBoard_UnlearnsSpam()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Нужен Python-разработчик", prevCol: "b_py"));
|
||||
|
||||
@@ -383,7 +382,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Restore_FromArchive_NoPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
|
||||
|
||||
@@ -399,7 +398,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Restore_FromTrash_PrevColDeletedBoard_FallsBackToInbox()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "b_gone"));
|
||||
|
||||
string? back = await service.RestoreCardAsync("l_1", CancellationToken.None);
|
||||
@@ -414,7 +413,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Restore_FromTrash_PrevColInbox_ReturnsInbox()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "inbox"));
|
||||
|
||||
string? back = await service.RestoreCardAsync("l_1", CancellationToken.None);
|
||||
@@ -439,7 +438,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task DeleteForever_RemovesCardAndComments_KeepsJournal()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash,
|
||||
sourceMsg: "Нужен Python-разработчик",
|
||||
comments: [new CardCommentDto("cm_1", "Вы", "Перезвонить", "5 мин")]));
|
||||
@@ -454,7 +453,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task DeleteForever_JournalRowsSurviveCardDeletion()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
|
||||
|
||||
@@ -480,7 +479,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task ClearCol_Trash_ReturnsClearedCount()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", KanbanColumns.Trash));
|
||||
store.SeedCard(Card("l_2", KanbanColumns.Trash));
|
||||
store.SeedCard(Card("l_3", KanbanColumns.Archive));
|
||||
@@ -507,7 +506,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task ClearCol_Board_Returns400Text()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
store.SeedCard(Card("l_1", "b_py"));
|
||||
|
||||
@@ -531,7 +530,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task MarkSeen_ById_OnlyThatCard()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", isNew: true));
|
||||
store.SeedCard(Card("l_2", "inbox", isNew: true));
|
||||
|
||||
@@ -544,7 +543,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task MarkSeen_ByCol_OnlyColumnCards()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", isNew: true));
|
||||
store.SeedCard(Card("l_2", "inbox", isNew: true));
|
||||
store.SeedCard(Card("l_3", "b_py", isNew: true));
|
||||
@@ -558,7 +557,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task MarkSeen_All_WithoutParameters()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", isNew: true));
|
||||
store.SeedCard(Card("l_2", KanbanColumns.Trash, isNew: true));
|
||||
|
||||
@@ -571,7 +570,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task AddComment_EmptyOrWhitespaceText_Returns400Text()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox"));
|
||||
|
||||
AddCommentResultDto empty = await service.AddCommentAsync("l_1", " ", CancellationToken.None);
|
||||
@@ -585,7 +584,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task AddComment_Valid_AppendsTrimmedCommentWithJournal()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox"));
|
||||
|
||||
AddCommentResultDto result = await service.AddCommentAsync("l_1", " Перезвонить завтра ", CancellationToken.None);
|
||||
@@ -618,7 +617,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Counts_ColumnsNewAndMlStats()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create();
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", isNew: true));
|
||||
store.SeedCard(Card("l_2", "inbox"));
|
||||
store.SeedCard(Card("l_3", "b_py"));
|
||||
@@ -652,7 +651,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Search_QueryShorterThanTwoChars_ReturnsEmptyAndDoesNotCallStore()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", title: "Python-разработчик"));
|
||||
|
||||
IReadOnlyList<CardDto> one = await service.SearchCardsAsync("p", CancellationToken.None);
|
||||
@@ -666,7 +665,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Search_DelegatesToStoreWithQueryAndLimit()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
|
||||
|
||||
IReadOnlyList<CardDto> result = await service.SearchCardsAsync(" pYtHoN ", CancellationToken.None);
|
||||
@@ -681,7 +680,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Search_FindsByTitleSummaryContactSourceText_OrderedByReceivedAtDesc()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_title", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
|
||||
store.SeedCard(Card("l_summary", "b_py", summary: "проект на python", receivedAtMs: 200));
|
||||
store.SeedCard(Card("l_contact", "inbox", contact: "@python_dev", receivedAtMs: 100));
|
||||
@@ -696,7 +695,7 @@ public sealed class CardsServiceTests
|
||||
[Fact]
|
||||
public async Task Search_LimitIsTwelve()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
store.SeedCard(Card($"l_{i:00}", "inbox", title: $"python {i}", receivedAtMs: i));
|
||||
@@ -711,12 +710,12 @@ public sealed class CardsServiceTests
|
||||
|
||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||
|
||||
private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
|
||||
private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
var ml = new TestMlClient();
|
||||
return (new CardsService(store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||
return (new CardsService(store.Store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||
}
|
||||
|
||||
// Доска с правилами (ContainerRulesDto) либо без них.
|
||||
|
||||
@@ -13,7 +13,6 @@ using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Tenants.Abstractions;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Modules.Tenants;
|
||||
@@ -146,7 +145,7 @@ public sealed class IntegrationsDiTests
|
||||
services.AddSingleton<ITenantContext>(tenantContext);
|
||||
services.AddScoped<ISettingsStore>(_ => new TestSettingsStore().Store);
|
||||
services.AddScoped<ISecretCipher>(_ => TestCiphers.New());
|
||||
services.AddScoped<ICardStore>(_ => new FakeKanjStore());
|
||||
services.AddScoped<ICardStore>(_ => new TestKanjStore().Store);
|
||||
services.AddScoped<IMlLearningStore>(_ => new TestMlLearningStore().Store);
|
||||
services.AddScoped<ITenantLimitStore>(_ => new TestTenantLimitStore().Store);
|
||||
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
||||
|
||||
@@ -2,7 +2,6 @@ using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
@@ -18,7 +17,7 @@ public sealed class PipelineCardWriterTests
|
||||
[Fact]
|
||||
public async Task CreateCard_CreatesInboxCardAndLinksDedupClaim()
|
||||
{
|
||||
(PipelineCardWriter writer, FakeKanjStore store, TestPipelineStore pipelineStore) = Create();
|
||||
(PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create();
|
||||
const string hash = "abcdef0123456789abcdef0123456789abcdef01";
|
||||
await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None); // заявка воркера (LeadId=null, Ruling 8)
|
||||
AiParsedCardDto parsed = Parsed(
|
||||
@@ -44,7 +43,7 @@ public sealed class PipelineCardWriterTests
|
||||
[Fact]
|
||||
public async Task CreateCard_AssignedBoardAccepted_WritesCardIntoBoardColumn()
|
||||
{
|
||||
(PipelineCardWriter writer, FakeKanjStore store, TestPipelineStore pipelineStore) = Create();
|
||||
(PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create();
|
||||
store.SeedBoard(new ContainerDto { Id = "b_py" });
|
||||
const string hash = "abcdef0123456789abcdef0123456789abcdef01";
|
||||
await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None);
|
||||
@@ -66,7 +65,7 @@ public sealed class PipelineCardWriterTests
|
||||
[Fact]
|
||||
public async Task CreateCard_AddCardFails_ThrowsAndDoesNotLinkDedup()
|
||||
{
|
||||
(PipelineCardWriter writer, FakeKanjStore store, TestPipelineStore pipelineStore) = Create();
|
||||
(PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create();
|
||||
store.FailAddCard = true; // сбой адаптера записи (например, недоступна БД тенанта)
|
||||
const string hash = "abcdef0123456789abcdef0123456789abcdef01";
|
||||
await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None);
|
||||
@@ -84,12 +83,12 @@ public sealed class PipelineCardWriterTests
|
||||
|
||||
// Создаёт контекст теста: подставки канбана, пайплайна и композитор поверх них.
|
||||
// Возвращает: Кортеж (обёртка, канбан-хранилище, хранилище пайплайна).
|
||||
private static (PipelineCardWriter Writer, FakeKanjStore Store, TestPipelineStore PipelineStore) Create()
|
||||
private static (PipelineCardWriter Writer, TestKanjStore Store, TestPipelineStore PipelineStore) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var pipelineStore = new TestPipelineStore();
|
||||
var composer = new CardComposer(store, new TestSettingsStore().Store);
|
||||
return (new PipelineCardWriter(store, pipelineStore.Store, composer), store, pipelineStore);
|
||||
var composer = new CardComposer(store.Store, new TestSettingsStore().Store);
|
||||
return (new PipelineCardWriter(store.Store, pipelineStore.Store, composer), store, pipelineStore);
|
||||
}
|
||||
|
||||
// Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
||||
|
||||
@@ -15,7 +15,6 @@ using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
using Deal.Tests.Unit.Grpc;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Modules.Tenants;
|
||||
@@ -155,7 +154,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
||||
private sealed record Context(
|
||||
PipelineWorkerService Worker,
|
||||
TestPipelineStore PipelineStore,
|
||||
FakeKanjStore KanjStore,
|
||||
TestKanjStore KanjStore,
|
||||
TestSettingsStore Settings,
|
||||
TestTenantLimitStore Limits);
|
||||
|
||||
@@ -166,7 +165,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
||||
{
|
||||
var settings = new TestSettingsStore();
|
||||
var pipelineStore = new TestPipelineStore();
|
||||
var kanjStore = new FakeKanjStore();
|
||||
var kanjStore = new TestKanjStore();
|
||||
var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
|
||||
var rules = new IncomingRules(settings.Store);
|
||||
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||
@@ -180,7 +179,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
||||
tenantContext,
|
||||
connection,
|
||||
new AiProviderConfigBuilder(settings.Store, TestCiphers.New()),
|
||||
new AiClassifyContextBuilder(settings.Store, kanjStore),
|
||||
new AiClassifyContextBuilder(settings.Store, kanjStore.Store),
|
||||
new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||
NullLogger<GrpcAiClassifier>.Instance);
|
||||
IAiClassifier aiClassifier = budgeted
|
||||
@@ -193,10 +192,10 @@ public sealed class PipelineWorkerGrpcAiTests
|
||||
: grpcClassifier;
|
||||
|
||||
var processing = new PipelineProcessingService(pipelineStore.Store, mlClient.Client, new PipelineIngestService(pipelineStore.Store));
|
||||
var composer = new CardComposer(kanjStore, settings.Store);
|
||||
var writer = new PipelineCardWriter(kanjStore, pipelineStore.Store, composer);
|
||||
var composer = new CardComposer(kanjStore.Store, settings.Store);
|
||||
var writer = new PipelineCardWriter(kanjStore.Store, pipelineStore.Store, composer);
|
||||
var worker = new PipelineWorkerService(
|
||||
pipelineStore.Store, settings.Store, rules, kanjStore, mlClient.Client, aiClassifier, processing, writer, fieldsParser);
|
||||
pipelineStore.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier, processing, writer, fieldsParser);
|
||||
return new Context(worker, pipelineStore, kanjStore, settings, limits);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Infrastructure;
|
||||
@@ -21,7 +20,7 @@ public sealed class CardMoverTests
|
||||
[Fact]
|
||||
public async Task Move_ToStage_RoutesToStageMoveAndResetsReminder()
|
||||
{
|
||||
(CardMover mover, FakeKanjStore store) = Create();
|
||||
(CardMover mover, TestKanjStore store) = Create();
|
||||
store.SeedCard(new CardDto
|
||||
{
|
||||
Id = "c_1",
|
||||
@@ -44,7 +43,7 @@ public sealed class CardMoverTests
|
||||
[Fact]
|
||||
public async Task Move_ToBoard_RoutesToDashboard()
|
||||
{
|
||||
(CardMover mover, FakeKanjStore store) = Create();
|
||||
(CardMover mover, TestKanjStore store) = Create();
|
||||
store.SeedBoard(new ContainerDto { Id = "b_py", Name = "Python" });
|
||||
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Нужен Python" } });
|
||||
|
||||
@@ -59,7 +58,7 @@ public sealed class CardMoverTests
|
||||
[Fact]
|
||||
public async Task Move_UnknownTarget_ReturnsDashboardInvalidTargetError()
|
||||
{
|
||||
(CardMover mover, FakeKanjStore store) = Create();
|
||||
(CardMover mover, TestKanjStore store) = Create();
|
||||
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Текст" } });
|
||||
|
||||
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None);
|
||||
@@ -79,10 +78,10 @@ public sealed class CardMoverTests
|
||||
Assert.False(result.Exists);
|
||||
}
|
||||
|
||||
private static (CardMover Mover, FakeKanjStore Store) Create()
|
||||
private static (CardMover Mover, TestKanjStore Store) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var cardsService = new CardsService(store, new TestSettingsStore().Store, new TestMlClient().Client, new TestFileStorage().Storage);
|
||||
var store = new TestKanjStore();
|
||||
var cardsService = new CardsService(store.Store, new TestSettingsStore().Store, new TestMlClient().Client, new TestFileStorage().Storage);
|
||||
return (new CardMover(cardsService), store);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Settings.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Kanban;
|
||||
@@ -25,11 +24,11 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildFilterPrompt_FillsDomainAndKeywords()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Страж. Сфера: {domain}. Маркеры: {keywords}."));
|
||||
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
|
||||
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
|
||||
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
|
||||
|
||||
@@ -39,9 +38,9 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildFilterPrompt_NoDomain_UsesFallbackTextAndHint()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("[{domain}] {keywords}"));
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
|
||||
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
|
||||
|
||||
@@ -53,12 +52,12 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildClassifySystemPrompt_AppendsCardPromptWhenSet()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
settings.Preload(SettingsKeys.AiPrompt, Json("Классифицируй {domain}."));
|
||||
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке» {keywords}."));
|
||||
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
|
||||
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
|
||||
string prompt = await builder.BuildClassifySystemPromptAsync(CancellationToken.None);
|
||||
|
||||
@@ -70,8 +69,8 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildClassifyUserContext_EmptyBoards_ShowsNoBoardsPhrase()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
|
||||
string context = await builder.BuildClassifyUserContextAsync("Нужен разработчик", CancellationToken.None);
|
||||
|
||||
@@ -83,7 +82,7 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildClassifyUserContext_ListsBoardsWithKeywordsAndRules()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
kanj.SeedBoard(new ContainerDto
|
||||
{
|
||||
Id = "b_py",
|
||||
@@ -114,7 +113,7 @@ public sealed class AiClassifyContextBuilderTests
|
||||
Order = 1,
|
||||
});
|
||||
kanj.SeedBoard(new ContainerDto { Id = "b_sug", Name = "Предложение", Suggested = true, Order = 2 });
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
|
||||
string context = await builder.BuildClassifyUserContextAsync("Ищу Python-разработчика", CancellationToken.None);
|
||||
|
||||
@@ -129,10 +128,10 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildClassifyUserContext_IncludesMarkupExamplesNewestFirst()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
SeedCardWithMove(kanj, "l_1", "b_py", "Пример: middle python (старый)");
|
||||
SeedCardWithMove(kanj, "l_2", "b_py", "Пример: senior go (свежий)");
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
|
||||
string context = await builder.BuildClassifyUserContextAsync("Ищу разработчика", CancellationToken.None);
|
||||
|
||||
@@ -145,8 +144,8 @@ public sealed class AiClassifyContextBuilderTests
|
||||
[Fact]
|
||||
public async Task BuildClassifyUserContext_LongMessage_TruncatesTo5000CodePoints()
|
||||
{
|
||||
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||
TestSettingsStore settings = Context(out TestKanjStore kanj);
|
||||
var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
|
||||
string text = new string('д', 6000);
|
||||
|
||||
string context = await builder.BuildClassifyUserContextAsync(text, CancellationToken.None);
|
||||
@@ -159,9 +158,9 @@ public sealed class AiClassifyContextBuilderTests
|
||||
// Создаёт контекст: пустое KV + пустой канбан-фейк.
|
||||
// kanj: Канбан-фейк (доски/журнал примеров).
|
||||
// Возвращает: KV-хранилище тенанта.
|
||||
private static TestSettingsStore Context(out FakeKanjStore kanj)
|
||||
private static TestSettingsStore Context(out TestKanjStore kanj)
|
||||
{
|
||||
kanj = new FakeKanjStore();
|
||||
kanj = new TestKanjStore();
|
||||
return new TestSettingsStore();
|
||||
}
|
||||
|
||||
@@ -171,12 +170,12 @@ public sealed class AiClassifyContextBuilderTests
|
||||
// board: Колонка-назначение (доска).
|
||||
// text: Исходный текст карточки (source_msg).
|
||||
private static void SeedCardWithMove(
|
||||
FakeKanjStore kanj,
|
||||
TestKanjStore kanj,
|
||||
string cardId,
|
||||
string board,
|
||||
string text)
|
||||
{
|
||||
kanj.SeedCard(new CardDto { Id = cardId, Col = board, Content = new SourceContent { Text = text } });
|
||||
kanj.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult();
|
||||
kanj.Store.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Kanban;
|
||||
@@ -19,7 +18,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Set_RemindersDisabled_Returns400TextAndWritesNothing()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
|
||||
(CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
|
||||
store.SeedCard(Card("c_1", stage: "hold", title: "Отложенный бот"));
|
||||
|
||||
CardResultDto result = await service.SetReminderAsync("c_1", NowMs() + DayMs, CancellationToken.None);
|
||||
@@ -32,7 +31,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Set_Enabled_SetsReminderAndReturnsCard()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "hold", title: "Отложенный бот", updatedAtMs: 1));
|
||||
long atMs = NowMs() + DayMs;
|
||||
|
||||
@@ -61,7 +60,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Set_StageNotHold_Allowed()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "work", title: "В работе"));
|
||||
long atMs = NowMs() + DayMs;
|
||||
|
||||
@@ -76,7 +75,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Clear_WithReminder_ClearsItAndReturnsTrue()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(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);
|
||||
@@ -98,7 +97,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Clear_RemindersDisabled_StillClears()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
|
||||
(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);
|
||||
@@ -111,7 +110,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Snooze_MovesReminderToNowPlus24h()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
|
||||
long beforeMs = NowMs();
|
||||
|
||||
@@ -136,7 +135,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task Snooze_RemindersDisabled_StillSnoozes()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
|
||||
(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);
|
||||
@@ -149,7 +148,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task CheckDue_Disabled_ClearsExpiredAndReturnsEmpty()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
|
||||
(CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
|
||||
long pastMs = NowMs() - 1;
|
||||
store.SeedCard(Card("c_hold_past", stage: "hold") with { Reminder = new CardReminderDto(pastMs) });
|
||||
store.SeedCard(Card("c_work_past", stage: "work") with { Reminder = new CardReminderDto(pastMs) });
|
||||
@@ -167,7 +166,7 @@ public sealed class CardsServiceRemindersTests
|
||||
[Fact]
|
||||
public async Task CheckDue_Enabled_ReturnsDueAndMarksFired()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_d1", stage: "hold", title: "Ранний") with { Reminder = new CardReminderDto(NowMs() - 2 * DayMs) });
|
||||
store.SeedCard(Card("c_d2", stage: "hold", title: "Поздний") with { Reminder = new CardReminderDto(NowMs() - 1) });
|
||||
store.SeedCard(Card("c_future", stage: "hold", title: "Будущий") with { Reminder = new CardReminderDto(NowMs() + DayMs) });
|
||||
@@ -181,21 +180,21 @@ public sealed class CardsServiceRemindersTests
|
||||
due.Select(item => (item.Id, item.Title)));
|
||||
Assert.All(due, item => Assert.Equal("hold", item.ContainerId));
|
||||
// «Выстрелившие» помечены fired: повторная выборка due пуста (признак держит строка БД).
|
||||
Assert.Empty(await store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
Assert.Empty(await store.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
}
|
||||
|
||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||
|
||||
private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings) Create(bool remindersEnabled = true)
|
||||
private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings) Create(bool remindersEnabled = true)
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
if (!remindersEnabled)
|
||||
{
|
||||
settings.Preload(SettingsKeys.RemindersEnabled, "false");
|
||||
}
|
||||
|
||||
return (new CardsService(store, settings.Store, new TestMlClient().Client, new TestFileStorage().Storage), store, settings);
|
||||
return (new CardsService(store.Store, settings.Store, new TestMlClient().Client, new TestFileStorage().Storage), store, settings);
|
||||
}
|
||||
|
||||
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
||||
|
||||
@@ -5,7 +5,6 @@ using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
@@ -20,14 +19,14 @@ public sealed class MlReviewServiceTests
|
||||
|
||||
private static MlReviewService Create(
|
||||
TestPipelineStore pipeline,
|
||||
FakeKanjStore kanj,
|
||||
TestKanjStore kanj,
|
||||
TestMlClient ml,
|
||||
out CardsService cards)
|
||||
{
|
||||
var settings = new TestSettingsStore();
|
||||
cards = new CardsService(kanj, settings.Store, ml.Client, new TestFileStorage().Storage);
|
||||
cards = new CardsService(kanj.Store, settings.Store, ml.Client, new TestFileStorage().Storage);
|
||||
var processing = new PipelineProcessingService(pipeline.Store, ml.Client, new PipelineIngestService(pipeline.Store));
|
||||
return new MlReviewService(pipeline.Store, kanj, cards, processing, ml.Client);
|
||||
return new MlReviewService(pipeline.Store, kanj.Store, cards, processing, ml.Client);
|
||||
}
|
||||
|
||||
private static QueueItemDto QueueRow(
|
||||
@@ -76,7 +75,7 @@ public sealed class MlReviewServiceTests
|
||||
var pipeline = new TestPipelineStore();
|
||||
pipeline.SeedQueue(QueueRow(101, "из очереди"));
|
||||
pipeline.SeedRejected(RejectedRow(102, "из отсева"));
|
||||
var kanj = new FakeKanjStore();
|
||||
var kanj = new TestKanjStore();
|
||||
kanj.SeedCard(Card(103, "из карточки"));
|
||||
MlReviewService service = Create(pipeline, kanj, new TestMlClient(), out _);
|
||||
|
||||
@@ -97,7 +96,7 @@ public sealed class MlReviewServiceTests
|
||||
var pipeline = new TestPipelineStore();
|
||||
pipeline.SeedQueue(QueueRow(101, "нужный", Dialog));
|
||||
pipeline.SeedQueue(QueueRow(201, "другой", "d_2"));
|
||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _);
|
||||
MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
|
||||
|
||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
||||
|
||||
@@ -111,7 +110,7 @@ public sealed class MlReviewServiceTests
|
||||
var pipeline = new TestPipelineStore();
|
||||
pipeline.SeedQueue(QueueRow(101, "a", "d_1"));
|
||||
pipeline.SeedQueue(QueueRow(201, "b", "d_2"));
|
||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _);
|
||||
MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
|
||||
|
||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(null, 10, CancellationToken.None);
|
||||
|
||||
@@ -127,7 +126,7 @@ public sealed class MlReviewServiceTests
|
||||
pipeline.SeedQueue(QueueRow(100 + i, $"текст {i}"));
|
||||
}
|
||||
|
||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _);
|
||||
MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
|
||||
|
||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 2, CancellationToken.None);
|
||||
|
||||
@@ -140,7 +139,7 @@ public sealed class MlReviewServiceTests
|
||||
var pipeline = new TestPipelineStore();
|
||||
pipeline.SeedQueue(QueueRow(101, "текст"));
|
||||
var ml = new TestMlClient();
|
||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
|
||||
MlReviewService service = Create(pipeline, new TestKanjStore(), ml, out _);
|
||||
|
||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSkip, CancellationToken.None);
|
||||
|
||||
@@ -154,7 +153,7 @@ public sealed class MlReviewServiceTests
|
||||
[Fact]
|
||||
public async Task Apply_Spam_WithCard_TrashesAndLearns()
|
||||
{
|
||||
var kanj = new FakeKanjStore();
|
||||
var kanj = new TestKanjStore();
|
||||
kanj.SeedCard(Card(101, "спамный текст"));
|
||||
var ml = new TestMlClient();
|
||||
MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
|
||||
@@ -175,7 +174,7 @@ public sealed class MlReviewServiceTests
|
||||
var pipeline = new TestPipelineStore();
|
||||
pipeline.SeedQueue(QueueRow(101, "рекламный текст"));
|
||||
var ml = new TestMlClient();
|
||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
|
||||
MlReviewService service = Create(pipeline, new TestKanjStore(), ml, out _);
|
||||
|
||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
|
||||
|
||||
@@ -192,7 +191,7 @@ public sealed class MlReviewServiceTests
|
||||
[Fact]
|
||||
public async Task Apply_Board_MovesCardAndLearns()
|
||||
{
|
||||
var kanj = new FakeKanjStore();
|
||||
var kanj = new TestKanjStore();
|
||||
kanj.SeedBoard(new ContainerDto
|
||||
{
|
||||
Id = "b_py",
|
||||
@@ -216,7 +215,7 @@ public sealed class MlReviewServiceTests
|
||||
[Fact]
|
||||
public async Task Apply_UnknownBoard_ReturnsError()
|
||||
{
|
||||
var kanj = new FakeKanjStore();
|
||||
var kanj = new TestKanjStore();
|
||||
kanj.SeedCard(Card(101, "текст"));
|
||||
MlReviewService service = Create(new TestPipelineStore(), kanj, new TestMlClient(), out _);
|
||||
|
||||
@@ -232,7 +231,7 @@ public sealed class MlReviewServiceTests
|
||||
{
|
||||
var pipeline = new TestPipelineStore();
|
||||
pipeline.SeedQueue(QueueRow(101, "текст"));
|
||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _);
|
||||
MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
|
||||
|
||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "что-то", CancellationToken.None);
|
||||
|
||||
@@ -244,7 +243,7 @@ public sealed class MlReviewServiceTests
|
||||
[Fact]
|
||||
public async Task Apply_MessageNotFound_ReturnsNull()
|
||||
{
|
||||
MlReviewService service = Create(new TestPipelineStore(), new FakeKanjStore(), new TestMlClient(), out _);
|
||||
MlReviewService service = Create(new TestPipelineStore(), new TestKanjStore(), new TestMlClient(), out _);
|
||||
|
||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Kanban;
|
||||
@@ -16,7 +15,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_DefaultSettings_ArchivesExpiredBoardAndInboxCards()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, _) = Create();
|
||||
(StorageTickService service, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(Board("b_1"));
|
||||
store.SeedCard(Card("l_board_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(16))));
|
||||
store.SeedCard(Card("l_board_young", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(13)))); // < 14 дн. — не кандидат
|
||||
@@ -48,7 +47,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_AutoArchiveSettingFalse_KeepsExpiredCardsInPlace()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.AutoArchive, "false");
|
||||
store.SeedBoard(Board("b_1"));
|
||||
store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(30))));
|
||||
@@ -63,7 +62,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_ArchiveAfterDays30_ArchivesOnlyCardsOlderThanConfiguredMonth()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.ArchiveAfterDays, "30");
|
||||
store.SeedBoard(Board("b_1"));
|
||||
store.SeedCard(Card("l_older_month", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(32))));
|
||||
@@ -79,7 +78,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_ArchiveAfterDays1_ArchivesTwoDayOldCard()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.ArchiveAfterDays, "1");
|
||||
store.SeedCard(Card("l_inbox_old", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromDays(2))));
|
||||
|
||||
@@ -92,7 +91,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_SecondRun_DoesNotReArchiveNorPurgeJustArchivedCards()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, _) = Create();
|
||||
(StorageTickService service, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(Board("b_1"));
|
||||
store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(20))));
|
||||
|
||||
@@ -110,7 +109,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_PurgesOnlyArchiveCardsWithArchivedAtOlderThanArchiveClearDays()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, _) = Create();
|
||||
(StorageTickService service, TestKanjStore store, _) = Create();
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(30))));
|
||||
store.SetArchivedAt("l_expired", now.AddDays(-100)); // старше 90 дн.
|
||||
@@ -130,7 +129,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_ArchiveClearDaysFromSettings_ControlsArchivePurgeBoundary()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.ArchiveClearDays, "30");
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(10))));
|
||||
@@ -149,7 +148,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_PurgesOnlyTrashCardsWithReceivedAtOlderThanTrashClearDays()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, _) = Create();
|
||||
(StorageTickService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(10)))); // старше 7 дн.
|
||||
store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromHours(6))));
|
||||
|
||||
@@ -166,7 +165,7 @@ public sealed class StorageTickServiceTests
|
||||
[Fact]
|
||||
public async Task Tick_TrashClearDaysFromSettings_ControlsTrashPurgeBoundary()
|
||||
{
|
||||
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.TrashClearDays, "3");
|
||||
store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(5)))); // старше 3 дн.
|
||||
store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(1))));
|
||||
@@ -195,11 +194,11 @@ public sealed class StorageTickServiceTests
|
||||
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
private static (StorageTickService Service, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||
private static (StorageTickService Service, TestKanjStore Store, TestSettingsStore Settings) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
return (new StorageTickService(store, settings.Store), store, settings);
|
||||
return (new StorageTickService(store.Store, settings.Store), store, settings);
|
||||
}
|
||||
|
||||
// Доска-колонка минимально (id/имя достаточно для списка кандидатов фейка).
|
||||
@@ -216,13 +215,13 @@ public sealed class StorageTickServiceTests
|
||||
private static long ReceivedAtMsAgo(TimeSpan age) => DateTimeOffset.UtcNow.Subtract(age).ToUnixTimeMilliseconds();
|
||||
|
||||
// Колонка карточки в фейке после тика.
|
||||
private static string ColOf(FakeKanjStore store, string cardId) =>
|
||||
private static string ColOf(TestKanjStore store, string cardId) =>
|
||||
store.CardDtos.Single(card => card.Id == cardId).Col;
|
||||
|
||||
// Флаг is_new карточки после тика.
|
||||
private static bool IsNewOf(FakeKanjStore store, string cardId) =>
|
||||
private static bool IsNewOf(TestKanjStore store, string cardId) =>
|
||||
store.CardDtos.Single(card => card.Id == cardId).IsNew;
|
||||
|
||||
private static IReadOnlyList<MatchHitDto> MatchHitsOf(FakeKanjStore store, string cardId) =>
|
||||
private static IReadOnlyList<MatchHitDto> MatchHitsOf(TestKanjStore store, string cardId) =>
|
||||
store.CardDtos.Single(card => card.Id == cardId).MatchHits;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using Deal.Modules.Pipeline.Application.Parse;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using NSubstitute;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
@@ -31,7 +30,7 @@ public sealed class AdminTickOrchestratorTests
|
||||
private sealed record Context(
|
||||
AdminTickOrchestrator Orchestrator,
|
||||
TestPipelineStore PipelineStore,
|
||||
FakeKanjStore KanjStore,
|
||||
TestKanjStore KanjStore,
|
||||
TestSettingsStore Settings,
|
||||
TestAiClassifier AiClassifier,
|
||||
PipelinePumpGate PumpGate,
|
||||
@@ -160,7 +159,7 @@ public sealed class AdminTickOrchestratorTests
|
||||
Assert.Equal("hold", payload.RootElement.GetProperty("containerId").GetString());
|
||||
|
||||
// «Выстрелившее» помечено fired: повторная выборка due пуста (признак держит строка БД).
|
||||
Assert.Empty(await ctx.KanjStore.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
Assert.Empty(await ctx.KanjStore.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -211,24 +210,24 @@ public sealed class AdminTickOrchestratorTests
|
||||
.Returns<IReadOnlyList<QueueItemDto>>(_ =>
|
||||
throw new InvalidOperationException("Тестовый сбой чтения очереди (ListAsync)."));
|
||||
}
|
||||
var kanjStore = new FakeKanjStore();
|
||||
var kanjStore = new TestKanjStore();
|
||||
var mlClient = new TestMlClient();
|
||||
var aiClassifier = new TestAiClassifier();
|
||||
var rules = new IncomingRules(settings.Store);
|
||||
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||
var ingest = new PipelineIngestService(store.Store);
|
||||
var processing = new PipelineProcessingService(store.Store, mlClient.Client, ingest);
|
||||
var composer = new CardComposer(kanjStore, settings.Store);
|
||||
var writer = new PipelineCardWriter(kanjStore, store.Store, composer);
|
||||
var composer = new CardComposer(kanjStore.Store, settings.Store);
|
||||
var writer = new PipelineCardWriter(kanjStore.Store, store.Store, composer);
|
||||
var worker = new PipelineWorkerService(
|
||||
store.Store, settings.Store, rules, kanjStore, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser);
|
||||
store.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser);
|
||||
|
||||
var broker = new SseBroker();
|
||||
var tickService = new StorageTickService(kanjStore, settings.Store);
|
||||
var tickService = new StorageTickService(kanjStore.Store, settings.Store);
|
||||
var toastPublisher = new StorageToastPublisher(broker);
|
||||
var pumpGate = new PipelinePumpGate();
|
||||
FakeKanjStore reminderStore = withThrowingReminderCheck ? new ThrowingDueKanjStore() : kanjStore;
|
||||
var cardsService = new CardsService(reminderStore, settings.Store, mlClient.Client, new TestFileStorage().Storage);
|
||||
TestKanjStore reminderStore = withThrowingReminderCheck ? new TestKanjStore(throwOnDueReminders: true) : kanjStore;
|
||||
var cardsService = new CardsService(reminderStore.Store, settings.Store, mlClient.Client, new TestFileStorage().Storage);
|
||||
var orchestrator = new AdminTickOrchestrator(
|
||||
tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate,
|
||||
NullLogger<AdminTickOrchestrator>.Instance);
|
||||
@@ -237,16 +236,7 @@ public sealed class AdminTickOrchestratorTests
|
||||
broker.Subscribe(TenantA));
|
||||
}
|
||||
|
||||
// Хранилище карточек со сбоем выборки due-напоминаний: ListDueRemindersAsync бросает (сценарий
|
||||
// «БД/схема недоступны» на проверке напоминаний — тик продолжается, reminders ответа пуст).
|
||||
private sealed class ThrowingDueKanjStore : FakeKanjStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<CardReminderDueDto>> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct)
|
||||
{
|
||||
throw new InvalidOperationException("Тестовый сбой выборки due-напоминаний (ListDueRemindersAsync).");
|
||||
}
|
||||
}
|
||||
// Сбой выборки due-напоминаний задаётся флагом throwOnDueReminders в TestKanjStore.
|
||||
|
||||
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
||||
private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
@@ -9,7 +9,6 @@ using Deal.Modules.Pipeline.Application.Parse;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
@@ -22,7 +21,7 @@ public sealed class CardReclassifierTests
|
||||
// Контекст теста: сервис поверх in-memory фейков.
|
||||
private sealed record Context(
|
||||
CardReclassifier Reclassifier,
|
||||
FakeKanjStore Store,
|
||||
TestKanjStore Store,
|
||||
TestSettingsStore Settings,
|
||||
TestMlClient MlClient,
|
||||
TestAiClassifier AiClassifier,
|
||||
@@ -33,15 +32,15 @@ public sealed class CardReclassifierTests
|
||||
private static Context CreateContext()
|
||||
{
|
||||
var settings = new TestSettingsStore();
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var mlClient = new TestMlClient();
|
||||
var aiClassifier = new TestAiClassifier();
|
||||
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||
var composer = new CardComposer(store, settings.Store);
|
||||
var cardsService = new CardsService(store, settings.Store, mlClient.Client, new TestFileStorage().Storage);
|
||||
var composer = new CardComposer(store.Store, settings.Store);
|
||||
var cardsService = new CardsService(store.Store, settings.Store, mlClient.Client, new TestFileStorage().Storage);
|
||||
var gate = new ReclassifyGate();
|
||||
var reclassifier = new CardReclassifier(
|
||||
store, settings.Store, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient.Client, gate);
|
||||
store.Store, settings.Store, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient.Client, gate);
|
||||
return new Context(reclassifier, store, settings, mlClient, aiClassifier, gate);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Text;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
@@ -16,7 +15,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_ImageMime_DetectsKindByMimeWritesObjectAndMeta()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
store.SeedCard(Card("c_1", updatedAtMs: 1));
|
||||
byte[] content = Encoding.UTF8.GetBytes("данные-картинки");
|
||||
using MemoryStream stream = new(content);
|
||||
@@ -42,7 +41,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_PdfExtensionWithoutMime_DetectsKindByExtension()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1"));
|
||||
|
||||
CardFileDto? entry = await service.AddFileAsync(
|
||||
@@ -57,7 +56,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_TwoFiles_AppendsPreservingOrderAndBothObjects()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
store.SeedCard(Card("c_1"));
|
||||
|
||||
CardFileDto? first = await service.AddFileAsync(
|
||||
@@ -76,7 +75,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_CardMissing_ReturnsNullAndDoesNotWriteObject()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
|
||||
CardFileDto? entry = await service.AddFileAsync(
|
||||
"c_missing", "photo.png", "image/png", new MemoryStream("x"u8.ToArray()), 1, CancellationToken.None);
|
||||
@@ -89,7 +88,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_EmptyOrWhitespaceFileName_DefaultsToPrototypeFile()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1"));
|
||||
|
||||
CardFileDto? entry = await service.AddFileAsync(
|
||||
@@ -105,7 +104,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_FileNameWithPathAndQuoteChars_SanitizesObjectKeyButKeepsMetaName()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
store.SeedCard(Card("c_1"));
|
||||
string rawName = "..\\файл\"отчёта v2.pdf";
|
||||
|
||||
@@ -123,7 +122,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Add_StreamPositionNotZero_StoresWholeContent()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
store.SeedCard(Card("c_1"));
|
||||
byte[] content = Encoding.UTF8.GetBytes("полное-содержимое-файла");
|
||||
using MemoryStream stream = new(content) { Position = 5 }; // эндпоинт мог прочитать поток раньше
|
||||
@@ -141,7 +140,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task GetEntry_ExistingFile_ReturnsEntryMeta()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
var file = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000000_tz.pdf");
|
||||
store.SeedCard(Card("c_1") with { Files = new[] { file } });
|
||||
|
||||
@@ -164,7 +163,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task GetEntry_FileNotInMetadata_ReturnsNull()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1")
|
||||
with { Files = new[] { new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "k") } });
|
||||
|
||||
@@ -177,7 +176,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Remove_Existing_DeletesObjectRemovesMetaAndReturnsCard()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
var first = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf");
|
||||
var second = new CardFileDto("pf_2", "photo.png", 200, "image", "Изображение", "projects/c_1/1710000000002_photo.png");
|
||||
store.SeedCard(Card("c_1", updatedAtMs: 1) with { Files = new[] { first, second } });
|
||||
@@ -199,7 +198,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Remove_UnknownFileId_ReturnsCardUnchangedWithoutStorageDelete()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
var file = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf");
|
||||
store.SeedCard(Card("c_1") with { Files = new[] { file } });
|
||||
|
||||
@@ -214,7 +213,7 @@ public sealed class CardsServiceFilesTests
|
||||
[Fact]
|
||||
public async Task Remove_EntryWithEmptyObjectKey_SkipsStorageDelete()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||
(CardsService service, TestKanjStore store, TestFileStorage storage) = Create();
|
||||
store.SeedCard(Card("c_1")
|
||||
with { Files = new[] { new CardFileDto("pf_mock", "meta-only.pdf", 10, "document", "Документ", ObjectKey: string.Empty) } });
|
||||
|
||||
@@ -238,11 +237,11 @@ public sealed class CardsServiceFilesTests
|
||||
|
||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||
|
||||
private static (CardsService Service, FakeKanjStore Store, TestFileStorage Storage) Create()
|
||||
private static (CardsService Service, TestKanjStore Store, TestFileStorage Storage) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var storage = new TestFileStorage();
|
||||
return (new CardsService(store, new TestSettingsStore().Store, new TestMlClient().Client, storage.Storage), store, storage);
|
||||
return (new CardsService(store.Store, new TestSettingsStore().Store, new TestMlClient().Client, storage.Storage), store, storage);
|
||||
}
|
||||
|
||||
// Карточка с полями по умолчанию (planned, CreatedAtMs=1, UpdatedAtMs=1; Files пуст).
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Text.Json;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
@@ -16,7 +15,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task List_NoStage_ReturnsCardsOrderedByUpdatedAtDesc()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", updatedAtMs: 100));
|
||||
store.SeedCard(Card("c_2", updatedAtMs: 300));
|
||||
store.SeedCard(Card("c_3", stage: "work", updatedAtMs: 200));
|
||||
@@ -29,7 +28,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task List_ByStage_ReturnsOnlyStageCards()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1"));
|
||||
store.SeedCard(Card("c_2", stage: "work"));
|
||||
|
||||
@@ -53,7 +52,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task CreateLocal_WithStage_TrimsTitleAndWritesCreatedLocalHistory()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
CardDto card = await service.CreateLocalCardAsync(
|
||||
new CardLocalCreateDto(Title: " Задача на бота ", ContainerId: "work"),
|
||||
@@ -102,7 +101,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task TakeCard_CardMissing_ReturnsNullAndCreatesNothing()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
CardDto? card = await service.TakeCardAsync("c_missing", CancellationToken.None);
|
||||
|
||||
@@ -113,7 +112,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task TakeCard_MovesCardToPlannedKeepingFields()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card(
|
||||
"c_1",
|
||||
stage: "inbox",
|
||||
@@ -150,7 +149,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task TakeCard_AlreadyInStage_ReturnsCardUnchanged()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "work", title: "Уже взята"));
|
||||
|
||||
CardDto? card = await service.TakeCardAsync("c_1", CancellationToken.None);
|
||||
@@ -167,7 +166,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Patch_PresentKeys_UpdateFieldsAndBumpUpdatedAt()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card(
|
||||
"c_1",
|
||||
title: "Старый заголовок",
|
||||
@@ -198,7 +197,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Patch_BudgetNull_ClearsBudget()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "С бюджетом", updatedAtMs: 1)
|
||||
with { Budget = new CardBudgetDto(1000, 2000, "USD") });
|
||||
|
||||
@@ -213,7 +212,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Patch_StackNull_ClearsStack()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "С стеком", updatedAtMs: 1)
|
||||
with { Stack = new[] { "Python" } });
|
||||
|
||||
@@ -226,7 +225,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Patch_UnknownKeys_AreIgnored()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Старый"));
|
||||
|
||||
CardDto? card = await service.PatchCardAsync(
|
||||
@@ -242,7 +241,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Patch_NullTextKey_IsIgnored()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Старый", summary: "Описание", updatedAtMs: 1));
|
||||
|
||||
CardDto? card = await service.PatchCardAsync(
|
||||
@@ -272,7 +271,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Move_TwoMoves_AppendHistoryResetReminderAndBumpUpdatedAt()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
long seededAt = 5_000;
|
||||
CardDto before = Card("c_1", stage: "hold", title: "Отложенная", updatedAtMs: seededAt)
|
||||
with
|
||||
@@ -310,7 +309,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task Move_UnknownStage_Returns400ErrorAndLeavesCardUntouched()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "planned"));
|
||||
|
||||
CardResultDto result = await service.MoveStageCardAsync("c_1", "stuck", CancellationToken.None);
|
||||
@@ -337,7 +336,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task ClearRejected_RemovesOnlyRejectedAndReturnsCount()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "rejected"));
|
||||
store.SeedCard(Card("c_2", stage: "rejected"));
|
||||
store.SeedCard(Card("c_3", stage: "finished"));
|
||||
@@ -363,7 +362,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddComment_Valid_AppendsTrimmedCommentWithWireForm()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка", updatedAtMs: 1));
|
||||
|
||||
AddCommentResultDto result = await service.AddCommentAsync("c_1", " Перезвонить завтра ", CancellationToken.None);
|
||||
@@ -381,7 +380,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddComment_TwoComments_AppendsPreservingOrder()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка")
|
||||
with { Comments = new[] { new CardCommentDto("cm_1", "Вы", "Первый", "5 мин") } });
|
||||
|
||||
@@ -396,7 +395,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddComment_EmptyOrWhitespaceText_Returns400DetailAndWritesNothing()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка"));
|
||||
|
||||
AddCommentResultDto empty = await service.AddCommentAsync("c_1", string.Empty, CancellationToken.None);
|
||||
@@ -411,7 +410,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddComment_CardMissing_ReturnsNullComments()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
AddCommentResultDto result = await service.AddCommentAsync("c_missing", "Текст", CancellationToken.None);
|
||||
|
||||
@@ -424,7 +423,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddLink_NoScheme_PrefixesHttpsAndDefaultsNameToUrl()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка", updatedAtMs: 1)
|
||||
with { Links = new[] { new CardLinkDto("pl_1", "Сайт", "https://example.com") } });
|
||||
|
||||
@@ -444,7 +443,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddLink_HasHttpScheme_KeepsSchemeAndUsesTrimmedName()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка"));
|
||||
|
||||
CardResultDto result = await service.AddLinkAsync("c_1", " Сайт ", "http://site.ru/abc", CancellationToken.None);
|
||||
@@ -458,7 +457,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddLink_EmptyUrl_Returns400DetailAndWritesNothing()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка"));
|
||||
|
||||
CardResultDto result = await service.AddLinkAsync("c_1", "Имя", " ", CancellationToken.None);
|
||||
@@ -471,7 +470,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task AddLink_CardMissing_ReturnsNullCardBeforeUrlValidation()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
CardResultDto result = await service.AddLinkAsync("c_missing", string.Empty, "example.com", CancellationToken.None);
|
||||
|
||||
@@ -483,7 +482,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task RemoveLink_ById_RemovesOnlyTargetAndReturnsCard()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка", updatedAtMs: 1)
|
||||
with
|
||||
{
|
||||
@@ -507,7 +506,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task RemoveLink_UnknownLinkId_LeavesLinksUnchangedWithoutError()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("c_1", title: "Карточка")
|
||||
with { Links = new[] { new CardLinkDto("pl_1", "Сайт", "https://a.b") } });
|
||||
|
||||
@@ -520,7 +519,7 @@ public sealed class CardsServiceSelectedTests
|
||||
[Fact]
|
||||
public async Task RemoveLink_CardMissing_ReturnsNullCard()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
CardResultDto result = await service.RemoveLinkAsync("c_missing", "pl_1", CancellationToken.None);
|
||||
|
||||
@@ -531,12 +530,12 @@ public sealed class CardsServiceSelectedTests
|
||||
|
||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||
|
||||
private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
|
||||
private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
var ml = new TestMlClient();
|
||||
return (new CardsService(store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||
return (new CardsService(store.Store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||
}
|
||||
|
||||
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
@@ -29,7 +28,7 @@ public sealed class ContainersServiceTests
|
||||
[Fact]
|
||||
public async Task Create_ExistingContainers_OrderIsMaxPlusOne()
|
||||
{
|
||||
(ContainersService service, FakeKanjStore store, _) = Create();
|
||||
(ContainersService service, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(Container("b_2", 2));
|
||||
store.SeedBoard(Container("b_5", 5));
|
||||
|
||||
@@ -43,7 +42,7 @@ public sealed class ContainersServiceTests
|
||||
[Fact]
|
||||
public async Task Create_OrderEight_CyclesPaletteToFirst()
|
||||
{
|
||||
(ContainersService service, FakeKanjStore store, _) = Create();
|
||||
(ContainersService service, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(Container("b_7", 7));
|
||||
|
||||
ContainerDto container = await service.CreateAsync(new ContainerCreateDto("Middle"), CancellationToken.None);
|
||||
@@ -157,7 +156,7 @@ public sealed class ContainersServiceTests
|
||||
[Fact]
|
||||
public async Task List_FillsCountsFromCards()
|
||||
{
|
||||
(ContainersService service, FakeKanjStore store, _) = Create();
|
||||
(ContainersService service, TestKanjStore store, _) = Create();
|
||||
ContainerDto container = await service.CreateAsync(new ContainerCreateDto("Middle"), CancellationToken.None);
|
||||
store.SeedCard(new CardDto { Id = "c_1", Col = container.Id, IsNew = true });
|
||||
store.SeedCard(new CardDto { Id = "c_2", Col = container.Id, IsNew = true });
|
||||
@@ -283,16 +282,16 @@ public sealed class ContainersServiceTests
|
||||
[Fact]
|
||||
public async Task Reorder_AssignsOrdersInGivenOrder()
|
||||
{
|
||||
(ContainersService service, FakeKanjStore store, _) = Create();
|
||||
(ContainersService service, TestKanjStore store, _) = Create();
|
||||
store.SeedBoard(Container("b_1", 0));
|
||||
store.SeedBoard(Container("b_2", 1));
|
||||
store.SeedBoard(Container("b_3", 2));
|
||||
|
||||
await service.ReorderAsync(ContainerSpaces.Dashboard, ["b_3", "b_1", "b_2"], CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, (await store.GetContainerAsync("b_3", CancellationToken.None))!.Order);
|
||||
Assert.Equal(1, (await store.GetContainerAsync("b_1", CancellationToken.None))!.Order);
|
||||
Assert.Equal(2, (await store.GetContainerAsync("b_2", CancellationToken.None))!.Order);
|
||||
Assert.Equal(0, (await store.Store.GetContainerAsync("b_3", CancellationToken.None))!.Order);
|
||||
Assert.Equal(1, (await store.Store.GetContainerAsync("b_1", CancellationToken.None))!.Order);
|
||||
Assert.Equal(2, (await store.Store.GetContainerAsync("b_2", CancellationToken.None))!.Order);
|
||||
}
|
||||
|
||||
// ─── Удаление контейнера ──────────────────────────────────────────────
|
||||
@@ -300,7 +299,7 @@ public sealed class ContainersServiceTests
|
||||
[Fact]
|
||||
public async Task Delete_MovesCardsToInbox_ReturnsMovedCount()
|
||||
{
|
||||
(ContainersService service, FakeKanjStore store, _) = Create();
|
||||
(ContainersService service, TestKanjStore store, _) = Create();
|
||||
ContainerDto container = await service.CreateAsync(new ContainerCreateDto("Middle"), CancellationToken.None);
|
||||
store.AddCard("c_1", container.Id);
|
||||
store.AddCard("c_2", container.Id);
|
||||
@@ -309,7 +308,7 @@ public sealed class ContainersServiceTests
|
||||
int moved = await service.DeleteAsync(container.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, moved);
|
||||
Assert.Null(await store.GetContainerAsync(container.Id, CancellationToken.None)); // контейнер удалён
|
||||
Assert.Null(await store.Store.GetContainerAsync(container.Id, CancellationToken.None)); // контейнер удалён
|
||||
Assert.Contains(store.Cards, card => card.CardId == "c_1" && card.Col == "inbox" && card.IsNew);
|
||||
Assert.Contains(store.Cards, card => card.CardId == "c_2" && card.Col == "inbox" && card.IsNew);
|
||||
Assert.Contains(store.Cards, card => card.CardId == "c_inbox" && card.Col == "inbox"); // inbox не тронут
|
||||
@@ -418,11 +417,11 @@ public sealed class ContainersServiceTests
|
||||
|
||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||
|
||||
private static (ContainersService Service, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||
private static (ContainersService Service, TestKanjStore Store, TestSettingsStore Settings) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
return (new ContainersService(store, settings.Store), store, settings);
|
||||
return (new ContainersService(store.Store, settings.Store), store, settings);
|
||||
}
|
||||
|
||||
// Патч только с нужными полями: остальные параметры null = «не менять».
|
||||
|
||||
@@ -5,7 +5,6 @@ using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Settings.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
@@ -20,7 +19,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_MockRates_ConvertsUsdBudgetToRub()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
store.SeedCard(Card("l_inbox", KanbanColumns.Inbox, from: 50, to: 200, cur: "USD"));
|
||||
@@ -35,7 +34,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_TargetCurrencyFromSettings_ConvertsToConfiguredCurrency()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
settings.Preload(SettingsKeys.TargetCurrency, "\"EUR\""); // PATCH пишет Upper (ApplyStringKey)
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
@@ -50,7 +49,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_NoTargetCurrencySetting_DefaultsToRub()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
|
||||
@@ -63,7 +62,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_UsdtBudget_TreatedAsUsdRate()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
var rates = new Dictionary<string, double> { ["RUB"] = 1.0, ["USD"] = 100.0, ["USDT"] = 90.0 };
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(rates));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USDT"));
|
||||
@@ -79,7 +78,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_BudgetWithoutUpperBound_UsesLowerBoundForConvTo()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: null, cur: "USD"));
|
||||
|
||||
@@ -94,7 +93,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_ConversionOff_ReturnsZeroAndKeepsCardsUntouched()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.ConversionOn, "false");
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
@@ -110,7 +109,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_ExcludesArchiveAndTrashCards()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
store.SeedCard(Card("l_archive", KanbanColumns.Archive, from: 100, to: 100, cur: "USD"));
|
||||
@@ -129,7 +128,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_MissingCurrencyInRates_SkipsCardKeepingOldConversion()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(new Dictionary<string, double> { ["RUB"] = 1.0, ["USD"] = 92.5 }));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "XXX") with
|
||||
{
|
||||
@@ -148,7 +147,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_NoRatesCache_ReturnsZeroAndKeepsCardsUntouched()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, _) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
|
||||
int updated = await service.RecomputeAsync(CancellationToken.None);
|
||||
@@ -160,7 +159,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_CorruptedRatesCache_ReturnsZero()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, "{это не JSON");
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
|
||||
@@ -175,7 +174,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task OnRatesChangedAsync_PerformsFullRecompute()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
|
||||
@@ -190,7 +189,7 @@ public sealed class ConversionRecomputerTests
|
||||
[Fact]
|
||||
public async Task Recompute_SecondRun_RecomputesSameValues()
|
||||
{
|
||||
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||
(ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create();
|
||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||
|
||||
@@ -204,11 +203,11 @@ public sealed class ConversionRecomputerTests
|
||||
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
private static (ConversionRecomputer Service, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||
private static (ConversionRecomputer Service, TestKanjStore Store, TestSettingsStore Settings) Create()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
return (new ConversionRecomputer(settings.Store, store), store, settings);
|
||||
return (new ConversionRecomputer(settings.Store, store.Store), store, settings);
|
||||
}
|
||||
|
||||
// Карточка с бюджетом в колонке (BudgetCur задан → Budget не null, как маппинг адаптера).
|
||||
@@ -238,7 +237,7 @@ public sealed class ConversionRecomputerTests
|
||||
// Конверсия (Converted) карточки в фейке после пересчёта.
|
||||
// store: Фейк-хранилище.
|
||||
// cardId: Id карточки.
|
||||
private static CardBudgetDto? ConvertedOf(FakeKanjStore store, string cardId) =>
|
||||
private static CardBudgetDto? ConvertedOf(TestKanjStore store, string cardId) =>
|
||||
store.CardDtos.Single(card => card.Id == cardId).Converted;
|
||||
|
||||
// Проверяет conv-поля карточки (From/To с допуском 0.001 и валюту).
|
||||
@@ -248,7 +247,7 @@ public sealed class ConversionRecomputerTests
|
||||
// convTo: Ожидаемая верхняя граница.
|
||||
// convCur: Ожидаемая валюта конверсии.
|
||||
private static void AssertConverted(
|
||||
FakeKanjStore store,
|
||||
TestKanjStore store,
|
||||
string cardId,
|
||||
double convFrom,
|
||||
double convTo,
|
||||
|
||||
@@ -15,7 +15,6 @@ using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
using Deal.Tests.Unit.Grpc;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Modules.Tenants;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -52,7 +51,7 @@ public sealed class GrpcAiClassifierTests
|
||||
Reason = "реклама",
|
||||
Usage = new Usage { Prompt = 500, Completion = 40, Total = 540 },
|
||||
};
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Фильтруй. {domain} | {keywords}."));
|
||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
||||
@@ -88,7 +87,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.FilterUnavailable = true;
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||
@@ -102,7 +101,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
string text = new string('а', 5000);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await classifier.FilterAsync(text, CancellationToken.None);
|
||||
@@ -131,7 +130,7 @@ public sealed class GrpcAiClassifierTests
|
||||
""",
|
||||
Usage = new Usage { Prompt = 3000, Completion = 700, Total = 3700 },
|
||||
};
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
settings.Preload(SettingsKeys.AiPrompt, Json("Разбор: {domain}."));
|
||||
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке»."));
|
||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||
@@ -191,7 +190,7 @@ public sealed class GrpcAiClassifierTests
|
||||
Ok = false,
|
||||
Usage = new Usage { Prompt = 900, Completion = 0, Total = 900 },
|
||||
};
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
|
||||
TestTenantLimitStore limits = new();
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||
@@ -212,7 +211,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.ClassifyUnavailable = true;
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||
@@ -226,7 +225,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Заголовок","stack":[],"is_spam":false}""" };
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
settings.Preload(SettingsKeys.AiProvider, Json("anthropic"));
|
||||
settings.Preload(
|
||||
SettingsKeys.AiConfigs,
|
||||
@@ -263,7 +262,7 @@ public sealed class GrpcAiClassifierTests
|
||||
{
|
||||
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" };
|
||||
string text = new string('б', 6000);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await classifier.ClassifyAsync(text, CancellationToken.None);
|
||||
@@ -286,7 +285,7 @@ public sealed class GrpcAiClassifierTests
|
||||
int port,
|
||||
TestSettingsStore settings,
|
||||
ISecretCipher cipher,
|
||||
FakeKanjStore kanjStore,
|
||||
TestKanjStore kanjStore,
|
||||
TestTenantLimitStore? limits = null)
|
||||
{
|
||||
limits ??= new TestTenantLimitStore();
|
||||
@@ -297,7 +296,7 @@ public sealed class GrpcAiClassifierTests
|
||||
tenantContext,
|
||||
connection,
|
||||
new AiProviderConfigBuilder(settings.Store, cipher),
|
||||
new AiClassifyContextBuilder(settings.Store, kanjStore),
|
||||
new AiClassifyContextBuilder(settings.Store, kanjStore.Store),
|
||||
new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||
NullLogger<GrpcAiClassifier>.Instance);
|
||||
}
|
||||
@@ -305,8 +304,8 @@ public sealed class GrpcAiClassifierTests
|
||||
// Создаёт контекст сценария (пустые фейки; сценарий переопределяет настройки/доски).
|
||||
// port: Порт хоста (не используется контекстом — единый вид хелперов).
|
||||
// Возвращает: Кортеж фейков (настройки, шифр, канбан).
|
||||
private static (TestSettingsStore Settings, ISecretCipher Cipher, FakeKanjStore Kanj) Context(int port)
|
||||
=> (new TestSettingsStore(), TestCiphers.New(), new FakeKanjStore());
|
||||
private static (TestSettingsStore Settings, ISecretCipher Cipher, TestKanjStore Kanj) Context(int port)
|
||||
=> (new TestSettingsStore(), TestCiphers.New(), new TestKanjStore());
|
||||
|
||||
// Сериализует значение настройки в JSON-строку (как пишет SettingsStore).
|
||||
// value: Значение (строка/список).
|
||||
|
||||
@@ -6,7 +6,6 @@ using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
@@ -18,18 +17,18 @@ public sealed class LocalColumnSuggesterTests
|
||||
{
|
||||
// Создаёт контекст теста: in-memory хранилища + адаптер поверх сервиса досок модуля.
|
||||
// Возвращает: Кортеж (хранилище канбана, KV-настройки, адаптер).
|
||||
private static (FakeKanjStore Store, TestSettingsStore Settings, LocalColumnSuggester Suggester) CreateContext()
|
||||
private static (TestKanjStore Store, TestSettingsStore Settings, LocalColumnSuggester Suggester) CreateContext()
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
var suggester = new LocalColumnSuggester(store, settings.Store, new ContainersService(store, settings.Store));
|
||||
var suggester = new LocalColumnSuggester(store.Store, settings.Store, new ContainersService(store.Store, settings.Store));
|
||||
return (store, settings, suggester);
|
||||
}
|
||||
|
||||
private static void SeedInbox(
|
||||
string id,
|
||||
string text,
|
||||
FakeKanjStore store,
|
||||
TestKanjStore store,
|
||||
long receivedAtMs = 0) =>
|
||||
store.SeedCard(new CardDto
|
||||
{
|
||||
@@ -58,7 +57,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestColumns_TooFewCards_ReturnsTooFewCardsReason()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
for (int i = 1; i < SuggestHeuristics.MinInbox; i++)
|
||||
{
|
||||
SeedInbox($"l_{i}", "нужен python", store);
|
||||
@@ -73,7 +72,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestColumns_CooldownActive_ReturnsCooldownReason()
|
||||
{
|
||||
(FakeKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
||||
settings.Preload(SettingsKeys.LastSuggestAt,
|
||||
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
||||
SeedInbox("l_1", "нужен python", store);
|
||||
@@ -88,7 +87,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestColumns_NoThemes_ReturnsNothingGroupedReason()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
string[] uniqueWords = ["python", "vue", "crm", "sql", "php", "mvp"];
|
||||
for (int i = 0; i < uniqueWords.Length; i++)
|
||||
{
|
||||
@@ -104,7 +103,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestColumns_SimilarExistingBoard_ReturnsNothingGroupedReason()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
store.SeedBoard(new ContainerDto
|
||||
{
|
||||
Id = "b_python",
|
||||
@@ -129,7 +128,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestColumns_Success_CreatesSuggestedBoardsAndPlacesCards()
|
||||
{
|
||||
(FakeKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
||||
SeedInbox("l_p1", "нужен python", store);
|
||||
SeedInbox("l_p2", "нужен python", store);
|
||||
SeedInbox("l_p3", "нужен python", store);
|
||||
@@ -170,7 +169,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestColumns_SuccessThenImmediateRepeat_BlockedByCooldown()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
SeedInbox("l_p1", "нужен python", store);
|
||||
SeedInbox("l_p2", "нужен python", store);
|
||||
SeedInbox("l_p3", "нужен python", store);
|
||||
@@ -192,7 +191,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestKeywords_Success_ReturnsRepeatedWordMarkers()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
SeedInbox("l_p1", "нужен python", store);
|
||||
SeedInbox("l_p2", "нужен python", store);
|
||||
SeedInbox("l_p3", "нужен python", store);
|
||||
@@ -210,7 +209,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestKeywords_TooFewCards_ReturnsReason()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
SeedInbox("l_1", "нужен python", store);
|
||||
SeedInbox("l_2", "нужен python", store);
|
||||
|
||||
@@ -224,7 +223,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestKeywords_TrashAndArchiveCards_AreNotCounted()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
SeedInbox("l_1", "нужен python", store);
|
||||
SeedInbox("l_2", "нужен vue", store);
|
||||
store.SeedCard(new CardDto { Id = "l_t1", Col = KanbanColumns.Trash, Content = new SourceContent { Text = "ищу такси" } });
|
||||
@@ -240,7 +239,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
[Fact]
|
||||
public async Task SuggestKeywords_NoRepeatedMarkers_ReturnsReason()
|
||||
{
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
(TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
string[] uniqueWords = ["python", "vue", "crm", "sql", "php", "mvp"];
|
||||
for (int i = 0; i < uniqueWords.Length; i++)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@ using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.SharedKernel.Tenants.Abstractions;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Modules.Tenants;
|
||||
@@ -42,10 +41,10 @@ public sealed class PipelineWorkerSchedulerTests
|
||||
private sealed record Context(
|
||||
PipelineWorkerScheduler Scheduler,
|
||||
TestPipelineStore PipelineA,
|
||||
FakeKanjStore KanjA,
|
||||
TestKanjStore KanjA,
|
||||
SseSubscription SubscriptionA,
|
||||
TestPipelineStore PipelineB,
|
||||
FakeKanjStore KanjB,
|
||||
TestKanjStore KanjB,
|
||||
SseSubscription SubscriptionB,
|
||||
PipelinePumpGate PumpGate,
|
||||
TenantContext TenantContext,
|
||||
@@ -153,8 +152,8 @@ public sealed class PipelineWorkerSchedulerTests
|
||||
var tenantContext = new TenantContext();
|
||||
var pipelineA = new TestPipelineStore(throwOnList: withThrowingQueueReadA);
|
||||
var pipelineB = new TestPipelineStore();
|
||||
var kanjA = new FakeKanjStore();
|
||||
var kanjB = new FakeKanjStore();
|
||||
var kanjA = new TestKanjStore();
|
||||
var kanjB = new TestKanjStore();
|
||||
var settingsA = new TestSettingsStore();
|
||||
var settingsB = new TestSettingsStore();
|
||||
var pumpGate = new PipelinePumpGate();
|
||||
@@ -172,7 +171,7 @@ public sealed class PipelineWorkerSchedulerTests
|
||||
services.AddSingleton<IAiClassifier>(aiClassifier.Classifier);
|
||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||
// (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||
services.AddScoped<ICardStore>(provider => TenantOf(provider) == TenantA ? kanjA : kanjB);
|
||||
services.AddScoped<ICardStore>(provider => TenantOf(provider) == TenantA ? kanjA.Store : kanjB.Store);
|
||||
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA.Store : settingsB.Store);
|
||||
services.AddScoped<IPipelineStore>(provider => TenantOf(provider) == TenantA ? pipelineA.Store : pipelineB.Store);
|
||||
// Реальные сервисы модуля Pipeline — как AddPipelineModule в Program.cs: цикл резолвит их в tenant-scope.
|
||||
|
||||
@@ -7,7 +7,6 @@ using Deal.Modules.Pipeline.Application.Parse;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
|
||||
@@ -26,7 +25,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
private sealed record Context(
|
||||
PipelineWorkerService Worker,
|
||||
TestPipelineStore PipelineStore,
|
||||
FakeKanjStore KanjStore,
|
||||
TestKanjStore KanjStore,
|
||||
TestSettingsStore Settings,
|
||||
TestMlClient MlClient,
|
||||
TestAiClassifier AiClassifier);
|
||||
@@ -38,16 +37,16 @@ public sealed class PipelineWorkerServiceTests
|
||||
{
|
||||
var settings = new TestSettingsStore();
|
||||
var store = pipelineStore ?? new TestPipelineStore();
|
||||
var kanjStore = new FakeKanjStore();
|
||||
var kanjStore = new TestKanjStore();
|
||||
var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
|
||||
var aiClassifier = new TestAiClassifier();
|
||||
var rules = new IncomingRules(settings.Store);
|
||||
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||
var processing = new PipelineProcessingService(store.Store, mlClient.Client, new PipelineIngestService(store.Store));
|
||||
var composer = new CardComposer(kanjStore, settings.Store);
|
||||
var writer = new PipelineCardWriter(kanjStore, store.Store, composer);
|
||||
var composer = new CardComposer(kanjStore.Store, settings.Store);
|
||||
var writer = new PipelineCardWriter(kanjStore.Store, store.Store, composer);
|
||||
var worker = new PipelineWorkerService(
|
||||
store.Store, settings.Store, rules, kanjStore, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser);
|
||||
store.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser);
|
||||
return new Context(worker, store, kanjStore, settings, mlClient, aiClassifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.SharedKernel.Tenants.Abstractions;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Modules.Tenants;
|
||||
@@ -43,8 +42,8 @@ public sealed class StorageTickSchedulerTests
|
||||
[Fact]
|
||||
public async Task RunCycle_TicksEveryTenantInOwnScopeAndPublishesToastToEachTenantChannel()
|
||||
{
|
||||
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
||||
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||
TestKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
||||
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||
var settings = new TestSettingsStore();
|
||||
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
||||
var tenantContext = new TenantContext();
|
||||
@@ -71,8 +70,8 @@ public sealed class StorageTickSchedulerTests
|
||||
[Fact]
|
||||
public async Task RunCycle_ZeroCountersTenant_PublishesToastOnlyToTenantWithChanges()
|
||||
{
|
||||
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
||||
var storeB = new FakeKanjStore();
|
||||
TestKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
||||
var storeB = new TestKanjStore();
|
||||
storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1))));
|
||||
var settings = new TestSettingsStore();
|
||||
var tenantContext = new TenantContext();
|
||||
@@ -100,16 +99,16 @@ public sealed class StorageTickSchedulerTests
|
||||
public async Task RunCycle_TenantTickFailure_DoesNotAbortOtherTenants()
|
||||
{
|
||||
// У тенанта A настройки падают (имитация сбоя схемы/БД) — тик A логирует ошибку, B обрабатывается.
|
||||
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||
var tenantContext = new TenantContext();
|
||||
var settingsByTenant = new Dictionary<Guid, ISettingsStore>
|
||||
{
|
||||
[TenantA] = new ThrowingSettingsStore(),
|
||||
[TenantB] = new TestSettingsStore().Store,
|
||||
};
|
||||
var storesByTenant = new Dictionary<Guid, FakeKanjStore>
|
||||
var storesByTenant = new Dictionary<Guid, TestKanjStore>
|
||||
{
|
||||
[TenantA] = new FakeKanjStore(),
|
||||
[TenantA] = new TestKanjStore(),
|
||||
[TenantB] = storeB,
|
||||
};
|
||||
await using ServiceProvider provider = BuildProvider(
|
||||
@@ -138,7 +137,7 @@ public sealed class StorageTickSchedulerTests
|
||||
await using ServiceProvider provider = BuildProvider(
|
||||
new ThrowingTenantRepository(),
|
||||
tenantContext,
|
||||
new Dictionary<Guid, FakeKanjStore>(),
|
||||
new Dictionary<Guid, TestKanjStore>(),
|
||||
new Dictionary<Guid, ISettingsStore>());
|
||||
StorageTickScheduler scheduler = CreateScheduler(provider);
|
||||
|
||||
@@ -151,7 +150,7 @@ public sealed class StorageTickSchedulerTests
|
||||
[Fact]
|
||||
public async Task RunCycle_PurgesExpiredRejectedRowsAndPublishesRejectedPurgeToast()
|
||||
{
|
||||
var kanjStore = new FakeKanjStore();
|
||||
var kanjStore = new TestKanjStore();
|
||||
var settings = new TestSettingsStore();
|
||||
var tenantContext = new TenantContext();
|
||||
var pipelineStoreA = new TestPipelineStore();
|
||||
@@ -161,7 +160,7 @@ public sealed class StorageTickSchedulerTests
|
||||
await using ServiceProvider provider = BuildProvider(
|
||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||
tenantContext,
|
||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = kanjStore, [TenantB] = new FakeKanjStore() },
|
||||
new Dictionary<Guid, TestKanjStore> { [TenantA] = kanjStore, [TenantB] = new TestKanjStore() },
|
||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings.Store, [TenantB] = settings.Store },
|
||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = pipelineStoreA, [TenantB] = new TestPipelineStore() });
|
||||
|
||||
@@ -182,16 +181,16 @@ public sealed class StorageTickSchedulerTests
|
||||
[Fact]
|
||||
public async Task RunCycle_DueReminder_PublishesReminderDueToTenantChannelAndMarksFired()
|
||||
{
|
||||
var cardStoreA = new FakeKanjStore();
|
||||
var cardStoreA = new TestKanjStore();
|
||||
cardStoreA.SeedCard(HoldCard("c_a_past", title: "Отложенный бот", reminderAtMs: NowMs() - 60_000));
|
||||
cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000));
|
||||
cardStoreA.SeedCard(HoldCard("c_a_work", stage: "work", title: "В работе", reminderAtMs: NowMs() - 60_000));
|
||||
var cardStoreB = new FakeKanjStore();
|
||||
var cardStoreB = new TestKanjStore();
|
||||
var tenantContext = new TenantContext();
|
||||
await using ServiceProvider provider = BuildProvider(
|
||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||
tenantContext,
|
||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = cardStoreB },
|
||||
new Dictionary<Guid, TestKanjStore> { [TenantA] = cardStoreA, [TenantB] = cardStoreB },
|
||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = new TestSettingsStore().Store, [TenantB] = new TestSettingsStore().Store },
|
||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new(), [TenantB] = new() });
|
||||
|
||||
@@ -211,14 +210,14 @@ public sealed class StorageTickSchedulerTests
|
||||
Assert.False(subscriptionB.Events.TryRead(out _));
|
||||
|
||||
// «Выстрелившее» помечено fired: повторная выборка due пуста (признак держит строка БД).
|
||||
Assert.Empty(await cardStoreA.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
Assert.Empty(await cardStoreA.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
Assert.False(tenantContext.HasTenant);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunCycle_RemindersDisabled_ClearsExpiredAndPublishesNoReminderDue()
|
||||
{
|
||||
var cardStoreA = new FakeKanjStore();
|
||||
var cardStoreA = new TestKanjStore();
|
||||
cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000));
|
||||
var settingsA = new TestSettingsStore();
|
||||
settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
|
||||
@@ -226,7 +225,7 @@ public sealed class StorageTickSchedulerTests
|
||||
await using ServiceProvider provider = BuildProvider(
|
||||
new TestTenantRepository(Tenant(TenantA)).Repository,
|
||||
tenantContext,
|
||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA },
|
||||
new Dictionary<Guid, TestKanjStore> { [TenantA] = cardStoreA },
|
||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = settingsA.Store },
|
||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new() });
|
||||
|
||||
@@ -246,13 +245,13 @@ public sealed class StorageTickSchedulerTests
|
||||
{
|
||||
// У тенанта A проверка напоминаний падает (имитация сбоя схемы/БД на ListDueAsync) — ветка логируется
|
||||
// и тик A завершается, тенант B обрабатывается (автоархив + тост) — проход жив.
|
||||
var cardStoreA = new ThrowingDueKanjStore();
|
||||
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||
var cardStoreA = new TestKanjStore(throwOnDueReminders: true);
|
||||
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||
var tenantContext = new TenantContext();
|
||||
await using ServiceProvider provider = BuildProvider(
|
||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||
tenantContext,
|
||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = storeB },
|
||||
new Dictionary<Guid, TestKanjStore> { [TenantA] = cardStoreA, [TenantB] = storeB },
|
||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = new TestSettingsStore().Store, [TenantB] = new TestSettingsStore().Store },
|
||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new(), [TenantB] = new() });
|
||||
|
||||
@@ -282,14 +281,14 @@ public sealed class StorageTickSchedulerTests
|
||||
private static ServiceProvider BuildProvider(
|
||||
TestTenantRepository tenants,
|
||||
TenantContext tenantContext,
|
||||
FakeKanjStore storeA,
|
||||
FakeKanjStore storeB,
|
||||
TestKanjStore storeA,
|
||||
TestKanjStore storeB,
|
||||
TestSettingsStore settings)
|
||||
{
|
||||
return BuildProvider(
|
||||
tenants.Repository,
|
||||
tenantContext,
|
||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = storeA, [TenantB] = storeB },
|
||||
new Dictionary<Guid, TestKanjStore> { [TenantA] = storeA, [TenantB] = storeB },
|
||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings.Store, [TenantB] = settings.Store });
|
||||
}
|
||||
|
||||
@@ -303,7 +302,7 @@ public sealed class StorageTickSchedulerTests
|
||||
private static ServiceProvider BuildProvider(
|
||||
ITenantRepository tenants,
|
||||
TenantContext tenantContext,
|
||||
Dictionary<Guid, FakeKanjStore> storesByTenant,
|
||||
Dictionary<Guid, TestKanjStore> storesByTenant,
|
||||
Dictionary<Guid, ISettingsStore> settingsByTenant,
|
||||
Dictionary<Guid, TestPipelineStore>? pipelineStoresByTenant = null)
|
||||
{
|
||||
@@ -320,7 +319,7 @@ public sealed class StorageTickSchedulerTests
|
||||
services.AddSingleton(tenants);
|
||||
// Тенант-scoped адаптеры: реальные строят TenantDbContext по схеме текущего тенанта — фейк
|
||||
// выбирает хранилище по тому же ITenantContext, который планировщик заполняет SetTenant.
|
||||
services.AddScoped<ICardStore>(provider => storesByTenant[TenantOf(provider)]);
|
||||
services.AddScoped<ICardStore>(provider => storesByTenant[TenantOf(provider)].Store);
|
||||
services.AddScoped<ISettingsStore>(provider => settingsByTenant[TenantOf(provider)]);
|
||||
services.AddScoped<IPipelineStore>(provider => pipelineStoresByTenant[TenantOf(provider)].Store);
|
||||
services.AddSingleton<IMlClient>(new TestMlClient().Client);
|
||||
@@ -360,17 +359,7 @@ public sealed class StorageTickSchedulerTests
|
||||
private static TenantRecordDto Tenant(Guid id) =>
|
||||
new(id, Name: "tenant", Status: "active", CreatedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
// Хранилище карточек со сбоем выборки due-напоминаний: ListDueRemindersAsync бросает (сценарий
|
||||
// «БД/схема недоступны» на проверке напоминаний — ветка логируется, тик тенанта/проход живы, как в
|
||||
// AdminTickOrchestratorTests).
|
||||
private sealed class ThrowingDueKanjStore : FakeKanjStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<CardReminderDueDto>> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct)
|
||||
{
|
||||
throw new InvalidOperationException("Тестовый сбой выборки due-напоминаний (ListDueRemindersAsync).");
|
||||
}
|
||||
}
|
||||
// Сбой выборки due-напоминаний задаётся флагом throwOnDueReminders в TestKanjStore.
|
||||
|
||||
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
||||
private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
@@ -395,9 +384,9 @@ public sealed class StorageTickSchedulerTests
|
||||
// Хранилище с просроченной карточкой «Неразобранного» — кандидатом автоархива дефолтного тика.
|
||||
// cardId: Id карточки.
|
||||
// Возвращает: Фейк-хранилище с одной старой карточкой inbox.
|
||||
private static FakeKanjStore StoreWithExpiredInboxCard(string cardId)
|
||||
private static TestKanjStore StoreWithExpiredInboxCard(string cardId)
|
||||
{
|
||||
var store = new FakeKanjStore();
|
||||
var store = new TestKanjStore();
|
||||
store.SeedCard(Card(cardId, KanbanColumns.Inbox, ReceivedAtMsAgo(ExpiredAge)));
|
||||
return store;
|
||||
}
|
||||
|
||||
+265
-293
@@ -2,120 +2,262 @@ using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Abstractions;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Cards;
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory реализация <see cref="ICardStore"/> для unit-тестов сервисов единого домена карточки.
|
||||
/// Подставка <see cref="ICardStore"/> на списках: сервисы получают NSubstitute-подставку
|
||||
/// (<see cref="Store"/>), тесты сеют/проверяют состояние через <see cref="SeedBoard"/>,
|
||||
/// <see cref="SeedCard"/>, <see cref="AddCard"/>, <see cref="Boards"/>, <see cref="CardDtos"/>,
|
||||
/// <see cref="Moves"/>, <see cref="SearchCalls"/>. Сбой записи задаётся <see cref="FailAddCard"/>.
|
||||
/// </summary>
|
||||
public class FakeKanjStore : ICardStore
|
||||
public sealed class TestKanjStore
|
||||
{
|
||||
private readonly List<ContainerDto> _boards = [];
|
||||
private readonly List<CardDto> _cards = [];
|
||||
private readonly List<CardMoveDto> _moves = [];
|
||||
private readonly List<(string Query, int Limit)> _searchCalls = [];
|
||||
|
||||
private readonly HashSet<string> _firedById = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly Dictionary<string, DateTimeOffset> _archivedAtById = new(StringComparer.Ordinal);
|
||||
private readonly bool _throwOnDueReminders;
|
||||
|
||||
/// <summary>
|
||||
/// Доски фейка (копия на момент обращения) — проверки BoardsServiceTests и LocalColumnSuggesterTests.
|
||||
/// Подставка порта единого домена карточки (создаётся в конструкторе).
|
||||
/// </summary>
|
||||
public ICardStore Store { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Доски хранилища (копия на момент обращения).
|
||||
/// </summary>
|
||||
public IReadOnlyList<ContainerDto> Boards => _boards.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Карточки фейка как тройки
|
||||
/// Карточки хранилища как тройки (id, колонка, флаг «новая»).
|
||||
/// </summary>
|
||||
public IReadOnlyList<(string CardId, string Col, bool IsNew)> Cards =>
|
||||
_cards.Select(card => (card.Id, card.Col, card.IsNew)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Полные карточки фейка
|
||||
/// Полные карточки хранилища (копия на момент обращения).
|
||||
/// </summary>
|
||||
public IReadOnlyList<CardDto> CardDtos => _cards.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Флаг «сбой записи карточки»
|
||||
/// Флаг «сбой записи карточки».
|
||||
/// </summary>
|
||||
public bool FailAddCard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Записи журнала CardMoves
|
||||
/// Записи журнала CardMoves.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CardMoveDto> Moves => _moves.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SearchCardsAsync как пары
|
||||
/// Вызовы SearchCardsAsync как пары (запрос, лимит).
|
||||
/// </summary>
|
||||
public IReadOnlyList<(string Query, int Limit)> SearchCalls => _searchCalls.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт подставку с пустыми списками.
|
||||
/// </summary>
|
||||
/// <param name="throwOnDueReminders">Сценарий сбоя выборки due-напоминаний: ListDueRemindersAsync бросает.</param>
|
||||
public TestKanjStore(bool throwOnDueReminders = false)
|
||||
{
|
||||
_throwOnDueReminders = throwOnDueReminders;
|
||||
Store = Substitute.For<ICardStore>();
|
||||
ConfigureContainers();
|
||||
ConfigureCards();
|
||||
ConfigureComments();
|
||||
ConfigureMoves();
|
||||
ConfigureStorage();
|
||||
ConfigureSelected();
|
||||
ConfigureReminders();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Кладёт доску напрямую
|
||||
/// </summary>
|
||||
/// <param name="board">Доска как если бы была сохранена в БД.</param>
|
||||
public void SeedBoard(ContainerDto board)
|
||||
{
|
||||
_boards.Add(board);
|
||||
}
|
||||
public void SeedBoard(ContainerDto board) => _boards.Add(board);
|
||||
|
||||
/// <summary>
|
||||
/// Кладёт карточку в колонку.
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки.</param>
|
||||
/// <param name="col">Колонка (inbox/доска).</param>
|
||||
public void AddCard(string cardId, string col)
|
||||
{
|
||||
SeedCard(new CardDto { Id = cardId, Col = col });
|
||||
}
|
||||
public void AddCard(string cardId, string col) => SeedCard(new CardDto { Id = cardId, Col = col });
|
||||
|
||||
/// <summary>
|
||||
/// Кладёт полную карточку.
|
||||
/// </summary>
|
||||
/// <param name="card">Карточка как если бы была сохранена в БД (комментарии — приложенным массивом).</param>
|
||||
public void SeedCard(CardDto card)
|
||||
public void SeedCard(CardDto card) => _cards.Add(card);
|
||||
|
||||
/// <summary>
|
||||
/// Задаёт метку архивации карточки
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки.</param>
|
||||
/// <param name="archivedAt">Метка архивации (когда карточка ушла в архив).</param>
|
||||
public void SetArchivedAt(string cardId, DateTimeOffset archivedAt) => _archivedAtById[cardId] = archivedAt;
|
||||
|
||||
/// <summary>
|
||||
/// Метка архивации карточки — проверка archived_at после автоархива тика.
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки.</param>
|
||||
/// <returns>Метка архивации либо null — карточки нет/не архивирована/метка сброшена.</returns>
|
||||
public DateTimeOffset? ArchivedAtOf(string cardId) =>
|
||||
_archivedAtById.TryGetValue(cardId, out DateTimeOffset archivedAt) ? archivedAt : null;
|
||||
|
||||
private void ConfigureContainers()
|
||||
{
|
||||
_cards.Add(card);
|
||||
Store.ListContainersAsync(Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListContainers(ci.ArgAt<string?>(0)));
|
||||
Store.GetContainerAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => _boards.FirstOrDefault(board => board.Id == ci.ArgAt<string>(0)));
|
||||
Store.When(s => s.CreateContainerAsync(Arg.Any<ContainerDto>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => _boards.Add(ci.Arg<ContainerDto>()));
|
||||
Store.When(s => s.UpdateContainerAsync(Arg.Any<ContainerDto>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => UpdateContainer(ci.Arg<ContainerDto>()));
|
||||
Store.DeleteContainerAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => DeleteContainer(ci.ArgAt<string>(0)));
|
||||
Store.When(s => s.ReorderContainersAsync(
|
||||
Arg.Any<string>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => ReorderContainers(ci.ArgAt<string>(0), ci.ArgAt<IReadOnlyList<string>>(1)));
|
||||
}
|
||||
|
||||
// ── Контейнеры (колонки/стадии/зоны) ─────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ContainerDto>> ListContainersAsync(string? space, CancellationToken ct)
|
||||
private void ConfigureCards()
|
||||
{
|
||||
IReadOnlyList<ContainerDto> ordered = _boards
|
||||
Store.ListCardsAsync(Arg.Any<CardsQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListCards(ci.Arg<CardsQuery>()));
|
||||
Store.SearchCardsAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => SearchCards(ci.ArgAt<string>(0), ci.ArgAt<int>(1)));
|
||||
Store.GetCardAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => _cards.FirstOrDefault(card => card.Id == ci.ArgAt<string>(0)));
|
||||
Store.GetCardBySourceAsync(Arg.Any<SourceRef>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => GetCardBySource(ci.Arg<SourceRef>()));
|
||||
Store.When(s => s.AddCardAsync(Arg.Any<CardSnapshot>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => AddCard(ci.Arg<CardSnapshot>()));
|
||||
Store.When(s => s.UpdateColumnAsync(Arg.Any<CardColumnUpdateDto>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => UpdateColumn(ci.Arg<CardColumnUpdateDto>()));
|
||||
Store.ApplyReclassificationAsync(Arg.Any<CardReclassificationDto>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ApplyReclassification(ci.Arg<CardReclassificationDto>()));
|
||||
Store.When(s => s.UpdateSeenAsync(Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => UpdateSeen(ci.ArgAt<string?>(0), ci.ArgAt<string?>(1)));
|
||||
Store.When(s => s.DeleteForeverAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => DeleteForever(ci.ArgAt<string>(0)));
|
||||
Store.ClearColAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ClearCol(ci.ArgAt<string>(0)));
|
||||
Store.CountCardsByColAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(_ => CountCardsByCol());
|
||||
}
|
||||
|
||||
private void ConfigureComments()
|
||||
{
|
||||
Store.ListCommentsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => _cards.FirstOrDefault(card => card.Id == ci.ArgAt<string>(0))?.Comments
|
||||
?? Array.Empty<CardCommentDto>());
|
||||
Store.When(s => s.AddCommentAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => AddComment(
|
||||
ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<string>(2), ci.ArgAt<string>(3)));
|
||||
}
|
||||
|
||||
private void ConfigureMoves()
|
||||
{
|
||||
Store.When(s => s.AddMoveAsync(Arg.Any<CardMoveDto>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => _moves.Add(ci.Arg<CardMoveDto>()));
|
||||
Store.CountMovesAsync(Arg.Any<CancellationToken>()).Returns(_ => _moves.Count);
|
||||
Store.GetAiMarkupExamplesAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => GetAiMarkupExamples(ci.ArgAt<int>(0)));
|
||||
}
|
||||
|
||||
private void ConfigureStorage()
|
||||
{
|
||||
Store.ListArchiveCandidatesAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListArchiveCandidates(ci.ArgAt<DateTimeOffset>(0)));
|
||||
Store.ArchiveAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => Archive(ci.ArgAt<IReadOnlyList<string>>(0), ci.ArgAt<DateTimeOffset>(1)));
|
||||
Store.ListExpiredArchiveCandidatesAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListExpiredArchiveCandidates(ci.ArgAt<DateTimeOffset>(0)));
|
||||
Store.ListTrashCandidatesAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListTrashCandidates(ci.ArgAt<DateTimeOffset>(0)));
|
||||
Store.PurgeAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => Purge(ci.ArgAt<IReadOnlyList<string>>(0)));
|
||||
Store.ListCardsForConversionAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(_ => ListCardsForConversion());
|
||||
Store.When(s => s.UpdateConversionAsync(
|
||||
Arg.Any<string>(), Arg.Any<double?>(), Arg.Any<double?>(), Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => UpdateConversion(
|
||||
ci.ArgAt<string>(0), ci.ArgAt<double?>(1), ci.ArgAt<double?>(2), ci.ArgAt<string>(3)));
|
||||
}
|
||||
|
||||
private void ConfigureSelected()
|
||||
{
|
||||
Store.ListSelectedCardsAsync(Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListSelectedCards(ci.ArgAt<string?>(0)));
|
||||
Store.PatchCardAsync(Arg.Any<string>(), Arg.Any<CardPatch>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => PatchCard(ci.ArgAt<string>(0), ci.Arg<CardPatch>()));
|
||||
Store.AddLinkAsync(Arg.Any<string>(), Arg.Any<CardLinkDto>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => AddLink(ci.ArgAt<string>(0), ci.Arg<CardLinkDto>()));
|
||||
Store.RemoveLinkAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => RemoveLink(ci.ArgAt<string>(0), ci.ArgAt<string>(1)));
|
||||
Store.AddFileAsync(Arg.Any<string>(), Arg.Any<CardFileDto>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => AddFile(ci.ArgAt<string>(0), ci.Arg<CardFileDto>()));
|
||||
Store.RemoveFileAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => RemoveFile(ci.ArgAt<string>(0), ci.ArgAt<string>(1)));
|
||||
Store.MoveCardStageAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CardHistoryDto>(), Arg.Any<long>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => MoveCardStage(
|
||||
ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.Arg<CardHistoryDto>(), ci.ArgAt<long>(3)));
|
||||
}
|
||||
|
||||
private void ConfigureReminders()
|
||||
{
|
||||
Store.When(s => s.SetReminderAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SetReminder(ci.ArgAt<string>(0), ci.ArgAt<long>(1)));
|
||||
Store.When(s => s.ClearReminderAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => ClearReminder(ci.ArgAt<string>(0)));
|
||||
Store.ClearStageAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ClearStage(ci.ArgAt<string>(0)));
|
||||
if (_throwOnDueReminders)
|
||||
{
|
||||
Store.ListDueRemindersAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task<IReadOnlyList<CardReminderDueDto>>>(_ =>
|
||||
throw new InvalidOperationException("Тестовый сбой выборки due-напоминаний (ListDueRemindersAsync)."));
|
||||
}
|
||||
else
|
||||
{
|
||||
Store.ListDueRemindersAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListDueReminders(ci.ArgAt<DateTimeOffset>(0)));
|
||||
}
|
||||
|
||||
Store.When(s => s.MarkRemindersFiredAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => MarkRemindersFired(ci.ArgAt<IReadOnlyList<string>>(0)));
|
||||
Store.ClearExpiredRemindersAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ClearExpiredReminders(ci.ArgAt<DateTimeOffset>(0)));
|
||||
Store.ListInboxWithSourceAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(_ => ListInboxWithSource());
|
||||
}
|
||||
|
||||
private IReadOnlyList<ContainerDto> ListContainers(string? space)
|
||||
{
|
||||
return _boards
|
||||
.Where(board => space is null || board.Space == space)
|
||||
.OrderBy(board => board.Suggested)
|
||||
.ThenBy(board => board.Order)
|
||||
.ToList();
|
||||
return Task.FromResult(ordered);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ContainerDto?> GetContainerAsync(string containerId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(_boards.FirstOrDefault(board => board.Id == containerId));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateContainerAsync(ContainerDto container, CancellationToken ct)
|
||||
{
|
||||
_boards.Add(container);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateContainerAsync(ContainerDto container, CancellationToken ct)
|
||||
private void UpdateContainer(ContainerDto container)
|
||||
{
|
||||
int index = _boards.FindIndex(item => item.Id == container.Id);
|
||||
_boards[index] = container;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> DeleteContainerAsync(string containerId, CancellationToken ct)
|
||||
private int DeleteContainer(string containerId)
|
||||
{
|
||||
List<CardDto> moved = _cards.Where(card => card.Col == containerId).ToList();
|
||||
int moved = _cards.Count(card => card.Col == containerId);
|
||||
for (int i = 0; i < _cards.Count; i++)
|
||||
{
|
||||
if (_cards[i].Col == containerId)
|
||||
@@ -125,54 +267,39 @@ public class FakeKanjStore : ICardStore
|
||||
}
|
||||
|
||||
_boards.RemoveAll(board => board.Id == containerId);
|
||||
return Task.FromResult(moved.Count);
|
||||
return moved;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ReorderContainersAsync(
|
||||
string space,
|
||||
IReadOnlyList<string> containerIds,
|
||||
CancellationToken ct)
|
||||
private void ReorderContainers(string space, IReadOnlyList<string> containerIds)
|
||||
{
|
||||
for (int i = 0; i < containerIds.Count; i++)
|
||||
{
|
||||
string containerId = containerIds[i];
|
||||
int index = _boards.FindIndex(board => board.Id == containerId && board.Space == space);
|
||||
int index = _boards.FindIndex(board => board.Id == containerIds[i] && board.Space == space);
|
||||
if (index >= 0)
|
||||
{
|
||||
_boards[index] = _boards[index] with { Order = i };
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── Карточки ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CardDto>> ListCardsAsync(CardsQuery query, CancellationToken ct)
|
||||
private IReadOnlyList<CardDto> ListCards(CardsQuery query)
|
||||
{
|
||||
IEnumerable<CardDto> result = query.Col is null
|
||||
? _cards
|
||||
: _cards.Where(card => card.Col == query.Col);
|
||||
return Task.FromResult<IReadOnlyList<CardDto>>(
|
||||
result.OrderByDescending(card => card.ReceivedAtMs).ToList());
|
||||
return result.OrderByDescending(card => card.ReceivedAtMs).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CardDto>> SearchCardsAsync(
|
||||
string q,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
private IReadOnlyList<CardDto> SearchCards(string q, int limit)
|
||||
{
|
||||
_searchCalls.Add((q, limit));
|
||||
string lowered = q.Trim().ToLowerInvariant();
|
||||
if (lowered.Length == 0)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<CardDto>>(Array.Empty<CardDto>());
|
||||
return Array.Empty<CardDto>();
|
||||
}
|
||||
|
||||
IReadOnlyList<CardDto> result = _cards
|
||||
return _cards
|
||||
.Where(card => card.Title.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||
|| card.Summary.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||
|| card.Contact.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||
@@ -180,38 +307,26 @@ public class FakeKanjStore : ICardStore
|
||||
.OrderByDescending(card => card.ReceivedAtMs)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<CardDto>>(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CardDto?> GetCardAsync(string cardId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(_cards.FirstOrDefault(card => card.Id == cardId));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CardDto?> GetCardBySourceAsync(
|
||||
SourceRef source,
|
||||
CancellationToken ct)
|
||||
private CardDto? GetCardBySource(SourceRef source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source.Kind))
|
||||
{
|
||||
return Task.FromResult<CardDto?>(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
string externalId = source.ExternalId ?? string.Empty;
|
||||
string originRef = source.OriginRef ?? string.Empty;
|
||||
CardDto? match = _cards
|
||||
return _cards
|
||||
.Where(card => card.Source.Kind == source.Kind
|
||||
&& (card.Source.ExternalId ?? string.Empty) == externalId
|
||||
&& (card.Source.OriginRef ?? string.Empty) == originRef)
|
||||
.OrderByDescending(card => card.ReceivedAtMs)
|
||||
.FirstOrDefault();
|
||||
return Task.FromResult(match);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddCardAsync(CardSnapshot snapshot, CancellationToken ct)
|
||||
private void AddCard(CardSnapshot snapshot)
|
||||
{
|
||||
if (FailAddCard)
|
||||
{
|
||||
@@ -249,16 +364,14 @@ public class FakeKanjStore : ICardStore
|
||||
CreatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct)
|
||||
private void UpdateColumn(CardColumnUpdateDto update)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == update.CardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
_cards[index] = _cards[index] with
|
||||
@@ -276,17 +389,14 @@ public class FakeKanjStore : ICardStore
|
||||
{
|
||||
_archivedAtById.Remove(update.CardId);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct)
|
||||
private bool ApplyReclassification(CardReclassificationDto update)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == update.CardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
_cards[index] = _cards[index] with
|
||||
@@ -304,40 +414,28 @@ public class FakeKanjStore : ICardStore
|
||||
Contacts = update.Contacts,
|
||||
MatchHits = update.MatchHits,
|
||||
};
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateSeenAsync(
|
||||
string? cardId,
|
||||
string? col,
|
||||
CancellationToken ct)
|
||||
private void UpdateSeen(string? cardId, string? col)
|
||||
{
|
||||
for (int i = 0; i < _cards.Count; i++)
|
||||
{
|
||||
bool matches = cardId is not null
|
||||
? _cards[i].Id == cardId
|
||||
: col is null || _cards[i].Col == col;
|
||||
bool matches = cardId is not null ? _cards[i].Id == cardId : col is null || _cards[i].Col == col;
|
||||
if (matches && _cards[i].IsNew)
|
||||
{
|
||||
_cards[i] = _cards[i] with { IsNew = false };
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteForeverAsync(string cardId, CancellationToken ct)
|
||||
private void DeleteForever(string cardId)
|
||||
{
|
||||
// Удаляется карточка (с комментариями — они часть карточки в фейке); журнал CardMoves не трогаем.
|
||||
_cards.RemoveAll(card => card.Id == cardId);
|
||||
_archivedAtById.Remove(cardId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> ClearColAsync(string col, CancellationToken ct)
|
||||
private int ClearCol(string col)
|
||||
{
|
||||
List<string> removedIds = _cards.Where(card => card.Col == col).Select(card => card.Id).ToList();
|
||||
_cards.RemoveAll(card => card.Col == col);
|
||||
@@ -346,64 +444,33 @@ public class FakeKanjStore : ICardStore
|
||||
_archivedAtById.Remove(cardId);
|
||||
}
|
||||
|
||||
return Task.FromResult(removedIds.Count);
|
||||
return removedIds.Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyDictionary<string, CardColumnCountDto>> CountCardsByColAsync(CancellationToken ct)
|
||||
private IReadOnlyDictionary<string, CardColumnCountDto> CountCardsByCol()
|
||||
{
|
||||
IReadOnlyDictionary<string, CardColumnCountDto> result = _cards
|
||||
return _cards
|
||||
.GroupBy(card => card.Col)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => new CardColumnCountDto(group.Count(), group.Count(card => card.IsNew)));
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
// ── Комментарии (LeadComments) ────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CardCommentDto>> ListCommentsAsync(string cardId, CancellationToken ct)
|
||||
{
|
||||
CardDto? card = _cards.FirstOrDefault(item => item.Id == cardId);
|
||||
return Task.FromResult(card?.Comments ?? Array.Empty<CardCommentDto>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddCommentAsync(
|
||||
string commentId,
|
||||
string cardId,
|
||||
string by,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
private void AddComment(string commentId, string cardId, string by, string text)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Метка времени свежего комментария — «только что» (адаптер считает HumanAge от CreatedAt = UtcNow).
|
||||
CardCommentDto comment = new(commentId, by, text, "только что");
|
||||
IReadOnlyList<CardCommentDto> updated = _cards[index].Comments.Concat(new[] { comment }).ToList();
|
||||
_cards[index] = _cards[index] with { Comments = updated };
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── Журнал CardMoves (learning_log) ───────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddMoveAsync(CardMoveDto move, CancellationToken ct)
|
||||
{
|
||||
_moves.Add(move);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CountMovesAsync(CancellationToken ct) => Task.FromResult(_moves.Count);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AiMarkupExampleDto>> GetAiMarkupExamplesAsync(int limit, CancellationToken ct)
|
||||
private IReadOnlyList<AiMarkupExampleDto> GetAiMarkupExamples(int limit)
|
||||
{
|
||||
var examples = new List<AiMarkupExampleDto>();
|
||||
for (int index = _moves.Count - 1; index >= 0 && examples.Count < limit; index--)
|
||||
@@ -429,31 +496,24 @@ public class FakeKanjStore : ICardStore
|
||||
examples.Add(new AiMarkupExampleDto(sourceText, move.ToCol));
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AiMarkupExampleDto>>(examples);
|
||||
return examples;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<string>> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
|
||||
private IReadOnlyList<string> ListArchiveCandidates(DateTimeOffset receivedBeforeUtc)
|
||||
{
|
||||
var boardIds = new HashSet<string>(_boards.Select(board => board.Id), StringComparer.Ordinal);
|
||||
IReadOnlyList<string> result = _cards
|
||||
return _cards
|
||||
.Where(card => (card.Col == KanbanColumns.Inbox || boardIds.Contains(card.Col))
|
||||
&& ReceivedAtOf(card) < receivedBeforeUtc)
|
||||
.Select(card => card.Id)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> ArchiveAsync(
|
||||
IReadOnlyList<string> cardIds,
|
||||
DateTimeOffset archivedAt,
|
||||
CancellationToken ct)
|
||||
private int Archive(IReadOnlyList<string> cardIds, DateTimeOffset archivedAt)
|
||||
{
|
||||
if (cardIds.Count == 0)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ids = new HashSet<string>(cardIds, StringComparer.Ordinal);
|
||||
@@ -475,38 +535,32 @@ public class FakeKanjStore : ICardStore
|
||||
archived++;
|
||||
}
|
||||
|
||||
return Task.FromResult(archived);
|
||||
return archived;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<string>> ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct)
|
||||
private IReadOnlyList<string> ListExpiredArchiveCandidates(DateTimeOffset archivedBeforeUtc)
|
||||
{
|
||||
IReadOnlyList<string> result = _cards
|
||||
return _cards
|
||||
.Where(card => card.Col == KanbanColumns.Archive
|
||||
&& _archivedAtById.TryGetValue(card.Id, out DateTimeOffset archivedAt)
|
||||
&& archivedAt < archivedBeforeUtc)
|
||||
.Select(card => card.Id)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<string>> ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
|
||||
private IReadOnlyList<string> ListTrashCandidates(DateTimeOffset receivedBeforeUtc)
|
||||
{
|
||||
IReadOnlyList<string> result = _cards
|
||||
return _cards
|
||||
.Where(card => card.Col == KanbanColumns.Trash && ReceivedAtOf(card) < receivedBeforeUtc)
|
||||
.Select(card => card.Id)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> PurgeAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
|
||||
private int Purge(IReadOnlyList<string> cardIds)
|
||||
{
|
||||
// Жёсткое удаление пачки (как KanbanStore.PurgeAsync): комментарии — часть карточки, журнал не трогаем.
|
||||
if (cardIds.Count == 0)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ids = new HashSet<string>(cardIds, StringComparer.Ordinal);
|
||||
@@ -516,88 +570,45 @@ public class FakeKanjStore : ICardStore
|
||||
_archivedAtById.Remove(cardId);
|
||||
}
|
||||
|
||||
return Task.FromResult(removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Задаёт метку архивации карточки
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки.</param>
|
||||
/// <param name="archivedAt">Метка архивации (когда карточка ушла в архив).</param>
|
||||
public void SetArchivedAt(string cardId, DateTimeOffset archivedAt) => _archivedAtById[cardId] = archivedAt;
|
||||
|
||||
/// <summary>
|
||||
/// Метка архивации карточки — проверка archived_at после автоархива тика.
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки.</param>
|
||||
/// <returns>Метка архивации либо null — карточки нет/не архивирована/метка сброшена.</returns>
|
||||
public DateTimeOffset? ArchivedAtOf(string cardId)
|
||||
private IReadOnlyList<CardDto> ListCardsForConversion()
|
||||
{
|
||||
return _archivedAtById.TryGetValue(cardId, out DateTimeOffset archivedAt) ? archivedAt : null;
|
||||
}
|
||||
|
||||
// Момент получения карточки из её epoch-ms (как маппинг адаптера ReceivedAt → ReceivedAtMs).
|
||||
// card: Карточка.
|
||||
// Возвращает: ReceivedAt карточки как UTC-момент.
|
||||
private static DateTimeOffset ReceivedAtOf(CardDto card) => DateTimeOffset.FromUnixTimeMilliseconds(card.ReceivedAtMs);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CardDto>> ListCardsForConversionAsync(CancellationToken ct)
|
||||
{
|
||||
IReadOnlyList<CardDto> result = _cards
|
||||
return _cards
|
||||
.Where(card => card.Budget is not null
|
||||
&& card.Col != KanbanColumns.Archive
|
||||
&& card.Col != KanbanColumns.Trash)
|
||||
.OrderByDescending(card => card.ReceivedAtMs)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateConversionAsync(
|
||||
string cardId,
|
||||
double? convFrom,
|
||||
double? convTo,
|
||||
string convCur,
|
||||
CancellationToken ct)
|
||||
private void UpdateConversion(string cardId, double? convFrom, double? convTo, string convCur)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index >= 0)
|
||||
{
|
||||
_cards[index] = _cards[index] with
|
||||
{
|
||||
Converted = convCur.Length == 0
|
||||
? null
|
||||
: new CardBudgetDto(convFrom, convTo, convCur),
|
||||
Converted = convCur.Length == 0 ? null : new CardBudgetDto(convFrom, convTo, convCur),
|
||||
};
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CardDto>> ListSelectedCardsAsync(string? containerId, CancellationToken ct)
|
||||
private IReadOnlyList<CardDto> ListSelectedCards(string? containerId)
|
||||
{
|
||||
IEnumerable<CardDto> query = containerId is null
|
||||
? _cards.Where(card => CardsDefaultContainers.Contains(card.Col))
|
||||
: _cards.Where(card => card.Col == containerId);
|
||||
return Task.FromResult<IReadOnlyList<CardDto>>(
|
||||
query.OrderByDescending(card => card.UpdatedAtMs).ToList());
|
||||
return query.OrderByDescending(card => card.UpdatedAtMs).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> PatchCardAsync(
|
||||
string cardId,
|
||||
CardPatch patch,
|
||||
CancellationToken ct)
|
||||
private bool PatchCard(string cardId, CardPatch patch)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardDto card = _cards[index];
|
||||
@@ -647,19 +658,15 @@ public class FakeKanjStore : ICardStore
|
||||
}
|
||||
|
||||
_cards[index] = card with { UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() };
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> AddLinkAsync(
|
||||
string cardId,
|
||||
CardLinkDto link,
|
||||
CancellationToken ct)
|
||||
private bool AddLink(string cardId, CardLinkDto link)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
_cards[index] = _cards[index] with
|
||||
@@ -667,19 +674,15 @@ public class FakeKanjStore : ICardStore
|
||||
Links = [.. _cards[index].Links, link],
|
||||
UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
};
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> RemoveLinkAsync(
|
||||
string cardId,
|
||||
string linkId,
|
||||
CancellationToken ct)
|
||||
private bool RemoveLink(string cardId, string linkId)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
_cards[index] = _cards[index] with
|
||||
@@ -687,19 +690,15 @@ public class FakeKanjStore : ICardStore
|
||||
Links = _cards[index].Links.Where(link => link.Id != linkId).ToList(),
|
||||
UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
};
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> AddFileAsync(
|
||||
string cardId,
|
||||
CardFileDto file,
|
||||
CancellationToken ct)
|
||||
private bool AddFile(string cardId, CardFileDto file)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
_cards[index] = _cards[index] with
|
||||
@@ -707,19 +706,15 @@ public class FakeKanjStore : ICardStore
|
||||
Files = [.. _cards[index].Files, file],
|
||||
UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
};
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> RemoveFileAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
CancellationToken ct)
|
||||
private bool RemoveFile(string cardId, string fileId)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
_cards[index] = _cards[index] with
|
||||
@@ -727,21 +722,15 @@ public class FakeKanjStore : ICardStore
|
||||
Files = _cards[index].Files.Where(file => file.Id != fileId).ToList(),
|
||||
UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
};
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> MoveCardStageAsync(
|
||||
string cardId,
|
||||
string containerId,
|
||||
CardHistoryDto historyEntry,
|
||||
long atMs,
|
||||
CancellationToken ct)
|
||||
private bool MoveCardStage(string cardId, string containerId, CardHistoryDto historyEntry, long atMs)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardDto card = _cards[index];
|
||||
@@ -753,19 +742,15 @@ public class FakeKanjStore : ICardStore
|
||||
UpdatedAtMs = atMs,
|
||||
History = [.. card.History, historyEntry],
|
||||
};
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetReminderAsync(
|
||||
string cardId,
|
||||
long atMs,
|
||||
CancellationToken ct)
|
||||
private void SetReminder(string cardId, long atMs)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index < 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
_firedById.Remove(cardId);
|
||||
@@ -774,11 +759,9 @@ public class FakeKanjStore : ICardStore
|
||||
Reminder = new CardReminderDto(atMs),
|
||||
UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
};
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ClearReminderAsync(string cardId, CancellationToken ct)
|
||||
private void ClearReminder(string cardId)
|
||||
{
|
||||
int index = _cards.FindIndex(card => card.Id == cardId);
|
||||
if (index >= 0)
|
||||
@@ -787,20 +770,14 @@ public class FakeKanjStore : ICardStore
|
||||
}
|
||||
|
||||
_firedById.Remove(cardId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> ClearStageAsync(string containerId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(_cards.RemoveAll(card => card.Col == containerId));
|
||||
}
|
||||
private int ClearStage(string containerId) => _cards.RemoveAll(card => card.Col == containerId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IReadOnlyList<CardReminderDueDto>> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct)
|
||||
private IReadOnlyList<CardReminderDueDto> ListDueReminders(DateTimeOffset now)
|
||||
{
|
||||
long nowMs = now.ToUnixTimeMilliseconds();
|
||||
IReadOnlyList<CardReminderDueDto> due = _cards
|
||||
return _cards
|
||||
.Where(card => card.Col == CardsDefaultContainers.Hold
|
||||
&& card.Reminder is not null
|
||||
&& !_firedById.Contains(card.Id)
|
||||
@@ -808,11 +785,9 @@ public class FakeKanjStore : ICardStore
|
||||
.OrderBy(card => card.Reminder!.At)
|
||||
.Select(card => new CardReminderDueDto(card.Id, card.Title, card.Col))
|
||||
.ToList();
|
||||
return Task.FromResult(due);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task MarkRemindersFiredAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
|
||||
private void MarkRemindersFired(IReadOnlyList<string> cardIds)
|
||||
{
|
||||
foreach (string cardId in cardIds)
|
||||
{
|
||||
@@ -821,12 +796,9 @@ public class FakeKanjStore : ICardStore
|
||||
_firedById.Add(cardId);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct)
|
||||
private int ClearExpiredReminders(DateTimeOffset now)
|
||||
{
|
||||
long nowMs = now.ToUnixTimeMilliseconds();
|
||||
int cleared = 0;
|
||||
@@ -840,16 +812,16 @@ public class FakeKanjStore : ICardStore
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(cleared);
|
||||
return cleared;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct)
|
||||
private IReadOnlyList<CardDto> ListInboxWithSource()
|
||||
{
|
||||
IReadOnlyList<CardDto> result = _cards
|
||||
return _cards
|
||||
.Where(card => card.Col == KanbanColumns.Inbox && (card.Content.Text ?? string.Empty) != string.Empty)
|
||||
.OrderByDescending(card => card.ReceivedAtMs)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static DateTimeOffset ReceivedAtOf(CardDto card) => DateTimeOffset.FromUnixTimeMilliseconds(card.ReceivedAtMs);
|
||||
}
|
||||
Reference in New Issue
Block a user