- WordCounterService: pure domain service, 7-step algorithm (null/empty fast path → length check → emoji detection via Rune.EnumerateRunes → count specials before replace → replace specials+hyphens → collapse whitespace → tokenize) - WordCountResult: sealed record with TotalWords + IReadOnlyDictionary<string,int> SpecialCharCounts - 4 domain exceptions extending DomainException: EmojiDetectedException, WordCountValidationException, ChargeableCharConfigInvalidException, ChargeableCharConfigForwardOnlyException - ChargeableCharConfig: rich entity with Create factory (invariants), Rehydrate reconstructor, ScheduleNewPrice (forward-only, returns new entity), Deactivate (idempotent) - ChargeableCharCategories: enum-as-string constants (Currency, Percentage, Exclamation, Question, Other) - DomainTimeProviderExtensions: internal GetArgentinaToday helper (mirrors Application.Common without creating Domain→Application dependency) - 60 new tests: 25 golden cases all GREEN, 12 entity invariant tests, 12 exception tests, 5 WordCountResult tests, 6 ChargeableCharConfig entity tests
329 lines
20 KiB
C#
329 lines
20 KiB
C#
using FluentAssertions;
|
|
using SIGCM2.Domain.Pricing.Exceptions;
|
|
using SIGCM2.Domain.Pricing.WordCounter;
|
|
|
|
namespace SIGCM2.Application.Tests.Domain.Pricing.WordCounter;
|
|
|
|
/// <summary>
|
|
/// PRC-001 — C5 Golden Cases Suite.
|
|
/// 25 canonical test cases that constitute the SPIKE acceptance gate.
|
|
/// Each golden case is a separate [Theory] row or [Fact] — individually identified.
|
|
/// </summary>
|
|
public sealed class WordCounterGoldenCasesTests
|
|
{
|
|
private static readonly WordCounterService _svc = new();
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-01: plain text — 4 words, no specials
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC01_PlainText_FourWords_NoSpecials()
|
|
{
|
|
var result = _svc.Count("vendo auto ford 2005");
|
|
|
|
result.TotalWords.Should().Be(4);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-02: dollar splits a token — 3 words, Currency=1
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC02_DollarSign_SplitsToken_ThreeWords_CurrencyOne()
|
|
{
|
|
var result = _svc.Count("vendo auto $5000");
|
|
|
|
result.TotalWords.Should().Be(3);
|
|
result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(1);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-03: percentage — 2 words, Percentage=1
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC03_Percentage_TwoWords_PercentageOne()
|
|
{
|
|
var result = _svc.Count("descuento 20%");
|
|
|
|
result.TotalWords.Should().Be(2);
|
|
result.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(1);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-04: exclamation mark — 5 words, Exclamation=1
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC04_Exclamation_FiveWords_ExclamationOne()
|
|
{
|
|
var result = _svc.Count("OFERTA! no te la pierdas");
|
|
|
|
result.TotalWords.Should().Be(5);
|
|
result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(1);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-05: inverse + normal exclamation — 1 word, Exclamation=2
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC05_InverseAndNormalExclamation_OneWord_ExclamationTwo()
|
|
{
|
|
var result = _svc.Count("¡Oferta!");
|
|
|
|
result.TotalWords.Should().Be(1);
|
|
result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(2);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-06: anti-fraud pattern P$a$l$a$b$r$a → 7 words, Currency=6
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC06_AntiFraudDollarPattern_SevenWords_CurrencySix()
|
|
{
|
|
var result = _svc.Count("P$a$l$a$b$r$a");
|
|
|
|
result.TotalWords.Should().Be(7);
|
|
result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(6);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-07: mixed specials + hyphen split
|
|
// VENDO | auto | 5000 | usado | 90 | buen | estado = 7
|
|
// Exclamation=1, Currency=1, Percentage=1
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC07_MixedSpecialsAndHyphen_SevenWords_OneEachCategory()
|
|
{
|
|
var result = _svc.Count("VENDO! auto $5000 usado %90 buen-estado");
|
|
|
|
result.TotalWords.Should().Be(7);
|
|
result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(1);
|
|
result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(1);
|
|
result.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(1);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-08: multi-space collapses — 3 words, no specials
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC08_MultiSpace_Collapses_ThreeWords_NoSpecials()
|
|
{
|
|
var result = _svc.Count("vendo auto ford");
|
|
|
|
result.TotalWords.Should().Be(3);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-09: CRLF becomes space — 4 words
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC09_CRLF_BecomesSpace_FourWords()
|
|
{
|
|
var result = _svc.Count("vendo auto\r\nbuen estado");
|
|
|
|
result.TotalWords.Should().Be(4);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-10: leading/trailing whitespace stripped — 2 words
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC10_LeadingTrailingWhitespace_Stripped_TwoWords()
|
|
{
|
|
var result = _svc.Count(" vendo auto ");
|
|
|
|
result.TotalWords.Should().Be(2);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-11: tildes are regular letters — 4 words, no specials
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC11_Tildes_AreRegularLetters_FourWords_NoSpecials()
|
|
{
|
|
var result = _svc.Count("vendo máquina niño año");
|
|
|
|
result.TotalWords.Should().Be(4);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-12: ñ in words — 3 words, no specials
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC12_EnneInWords_ThreeWords_NoSpecials()
|
|
{
|
|
var result = _svc.Count("año mañana niño");
|
|
|
|
result.TotalWords.Should().Be(3);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-13: empty string — 0 words, empty dict
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC13_EmptyString_ZeroWords_EmptyDict()
|
|
{
|
|
var result = _svc.Count("");
|
|
|
|
result.TotalWords.Should().Be(0);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-14: whitespace-only — 0 words, empty dict
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC14_WhitespaceOnly_ZeroWords_EmptyDict()
|
|
{
|
|
var result = _svc.Count(" ");
|
|
|
|
result.TotalWords.Should().Be(0);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-15: single word — 1 word
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC15_SingleWord_OneWord()
|
|
{
|
|
var result = _svc.Count("auto");
|
|
|
|
result.TotalWords.Should().Be(1);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-16: numbers and tilde — 3 words
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC16_NumbersAndTilde_ThreeWords()
|
|
{
|
|
var result = _svc.Count("1978 2005 año");
|
|
|
|
result.TotalWords.Should().Be(3);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-17: URL treated as single token — 2 words
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC17_Url_TreatedAsSingleToken_TwoWords()
|
|
{
|
|
var result = _svc.Count("visita www.example.com");
|
|
|
|
result.TotalWords.Should().Be(2);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-18: hyphenated compounds split — 4 words
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC18_HyphenatedCompounds_SplitIntoFourWords()
|
|
{
|
|
var result = _svc.Count("buen-estado casi-nuevo");
|
|
|
|
result.TotalWords.Should().Be(4);
|
|
result.SpecialCharCounts.Should().BeEmpty();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-19: emoji at end — throws EmojiDetectedException
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC19_EmojiAtEnd_ThrowsEmojiDetectedException()
|
|
{
|
|
var act = () => _svc.Count("vendo 🚗");
|
|
|
|
act.Should().Throw<EmojiDetectedException>();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-20: emoji in middle — throws EmojiDetectedException
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC20_EmojiInMiddle_ThrowsEmojiDetectedException()
|
|
{
|
|
var act = () => _svc.Count("vendo auto 🚗 2005");
|
|
|
|
act.Should().Throw<EmojiDetectedException>();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-21: only emoji — throws EmojiDetectedException
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC21_OnlyEmoji_ThrowsEmojiDetectedException()
|
|
{
|
|
var act = () => _svc.Count("🚗");
|
|
|
|
act.Should().Throw<EmojiDetectedException>();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-22: exactly 2000 chars — passes (no exception), TotalWords >= 1
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC22_Exactly2000Chars_PassesValidation()
|
|
{
|
|
// Build a 2000-char string of "a " repeated (1000 times = 2000 chars)
|
|
var input = string.Concat(Enumerable.Repeat("a ", 1000)).TrimEnd(); // 1999 chars — add one more
|
|
// Ensure exactly 2000: "a " x 999 = 1998 + "aa" = 2000
|
|
input = string.Concat(Enumerable.Repeat("a ", 999)) + "aa"; // 999*2 + 2 = 2000
|
|
|
|
var act = () => _svc.Count(input);
|
|
|
|
act.Should().NotThrow();
|
|
var result = _svc.Count(input);
|
|
result.TotalWords.Should().BeGreaterThanOrEqualTo(1);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-23: 2001 chars — throws WordCountValidationException
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC23_TwoThousandAndOneChars_ThrowsWordCountValidationException()
|
|
{
|
|
var input = new string('a', 2001);
|
|
|
|
var act = () => _svc.Count(input);
|
|
|
|
act.Should().Throw<WordCountValidationException>();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-24: all specials replaced → 0 words
|
|
// "$$ %% !! ¡¡" → Currency=2, Percentage=2, Exclamation=4, TotalWords=0
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC24_AllSpecials_ZeroWordsRemain_CorrectCounts()
|
|
{
|
|
var result = _svc.Count("$$ %% !! ¡¡");
|
|
|
|
result.TotalWords.Should().Be(0);
|
|
result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(2);
|
|
result.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(2);
|
|
result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(4);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GC-25: anti-fraud embedded in sentence — 9 words, Currency=6
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GC25_AntiFraudEmbedded_NineWords_CurrencySix()
|
|
{
|
|
// "vendo P$a$l$a$b$r$a usado"
|
|
// P$a$l$a$b$r$a → P a l a b r a = 7 tokens; + vendo + usado = 9
|
|
var result = _svc.Count("vendo P$a$l$a$b$r$a usado");
|
|
|
|
result.TotalWords.Should().Be(9);
|
|
result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(6);
|
|
}
|
|
}
|