Files
EnvelopeGenerator/EnvelopeGenerator.Tests/Infrastructure/ReplacePlaceholderExtensionsTests.cs
TekH d913b949e0 Add DTOs and tests for placeholder replacement
Added new record types `CreateEnvelopeDocumentDto`, `CreateEnvelopeFieldDto`, and `CreateEnvelopeReceiverDto` with XML documentation in `CreateEnvelopeCommand.cs`. Marked `ConfigService` as obsolete in `CreateEnvelopeCommandHandler.cs`. Modified `CertificationType` usage in `Fake.cs` to use the enum directly. Updated `EnvelopeGenerator.Tests.csproj` with a new project reference and compile directives. Introduced `MailTemplatePlaceholderDbTests` and `ReplacePlaceholderExtensionsTests` to verify placeholder replacement functionality. Added supporting model classes for testing. These changes improve code functionality and test coverage.
2026-10-29 09:53:52 +01:00

479 lines
13 KiB
C#

using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
using EnvelopeGenerator.Infrastructure.Notification;
namespace EnvelopeGenerator.Tests.Infrastructure;
[TestFixture]
public class ReplacePlaceholderExtensionsTests
{
#region Naming
private static IEnumerable<TestCaseData> NamingCases()
{
yield return Naming<PersonModel>("[FIRST_NAME]");
yield return Naming<PersonModel>("[LAST_NAME]");
yield return Naming<IdModel>("[ID]");
yield return Naming<UpperIdModel>("[ID]");
yield return Naming<AcronymModel>("[URL_VALUE]");
yield return Naming<AcronymModel>("[HTTP_STATUS_CODE]");
yield return Naming<AcronymModel>("[XML_HTTP_REQUEST]");
yield return Naming<AcronymModel>("[OTP]");
yield return Naming<DigitModel>("[FIELD1]");
yield return Naming<DigitModel>("[VALUE2_TEXT]");
yield return Naming<DigitModel>("[A1_B2]");
yield return Naming<DigitModel>("[NAME2]");
yield return Naming<MyUrl2ValueModel>("[MY_URL2_VALUE]");
yield return Naming<SingleLetterModel>("[X]");
yield return Naming<LowerCaseModel>("[FIRST_NAME]");
yield return Naming<SnakeCaseModel>("[FIRST_NAME]");
yield return Naming<TfaModel>("[TFA_EXPIRATION]");
}
private static TestCaseData Naming<T>(string placeholder) where T : new()
=> new(new T(), placeholder)
{
TestName = $"Placeholder_{typeof(T).Name}_{placeholder.Trim('[', ']')}"
};
[TestCaseSource(nameof(NamingCases))]
public void Property_names_are_converted_to_upper_snake_case(object data, string placeholder)
{
var result = placeholder.ReplacePlaceholders(data);
Assert.That(result, Is.EqualTo("v"));
}
[Test]
[SetCulture("tr-TR")]
public void Placeholder_names_are_culture_invariant()
{
var result = "[INFO]".ReplacePlaceholders(new LowerCaseInfoModel());
Assert.That(result, Is.EqualTo("v"), "Turkish casing would produce [\u0130NFO]");
}
#endregion
#region Basic replacement
[Test]
public void Replaces_single_placeholder()
{
var result = "[FIRST_NAME]".ReplacePlaceholders(new PersonModel { FirstName = "Ada" });
Assert.That(result, Is.EqualTo("Ada"));
}
[Test]
public void Replaces_multiple_placeholders()
{
var data = new PersonModel { FirstName = "Ada", LastName = "Lovelace" };
var result = "[FIRST_NAME] [LAST_NAME]".ReplacePlaceholders(data);
Assert.That(result, Is.EqualTo("Ada Lovelace"));
}
[Test]
public void Replaces_all_repeated_occurrences()
{
var data = new PersonModel { FirstName = "Ada" };
var result = "[FIRST_NAME]-[FIRST_NAME]-[FIRST_NAME]".ReplacePlaceholders(data);
Assert.That(result, Is.EqualTo("Ada-Ada-Ada"));
}
[Test]
public void Leaves_unknown_placeholder_untouched()
{
var result = "[UNKNOWN_TOKEN]".ReplacePlaceholders(new PersonModel());
Assert.That(result, Is.EqualTo("[UNKNOWN_TOKEN]"));
}
[Test]
public void Returns_template_unchanged_when_it_has_no_placeholders()
{
var result = "Hello world".ReplacePlaceholders(new PersonModel());
Assert.That(result, Is.EqualTo("Hello world"));
}
[Test]
public void Returns_empty_string_for_empty_template()
{
var result = string.Empty.ReplacePlaceholders(new PersonModel());
Assert.That(result, Is.Empty);
}
[Test]
public void Returns_template_unchanged_when_data_has_no_readable_properties()
{
var result = "[ANYTHING]".ReplacePlaceholders(new object());
Assert.That(result, Is.EqualTo("[ANYTHING]"));
}
[Test]
public void Matching_is_case_sensitive()
{
var result = "[name]|[Name]".ReplacePlaceholders(new PersonNameModel { Name = "v" });
Assert.That(result, Is.EqualTo("[name]|[Name]"));
}
[Test]
public void Placeholder_without_brackets_is_untouched()
{
var result = "NAME Receiver".ReplacePlaceholders(new PersonNameModel { Name = "v" });
Assert.That(result, Is.EqualTo("NAME Receiver"));
}
[Test]
public void Double_brackets_leave_the_outer_brackets()
{
var result = "[[NAME]]".ReplacePlaceholders(new PersonNameModel { Name = "v" });
Assert.That(result, Is.EqualTo("[v]"));
}
#endregion
#region Values
[Test]
public void Null_value_is_replaced_with_empty_string()
{
var result = "[NAME]".ReplacePlaceholders(new { Name = (string?)null });
Assert.That(result, Is.Empty);
}
[Test]
public void Custom_ToString_is_used()
{
var result = "[VALUE]".ReplacePlaceholders(new { Value = new CustomValue() });
Assert.That(result, Is.EqualTo("custom"));
}
[Test]
public void Primitive_values_use_ToString()
{
var result = "[COUNT]|[ENABLED]".ReplacePlaceholders(new { Count = 3, Enabled = true });
Assert.That(result, Is.EqualTo("3|True"));
}
[Test]
public void Regex_like_characters_are_inserted_literally()
{
var result = "[PATTERN]".ReplacePlaceholders(new { Pattern = @"$1 {0} \ $&" });
Assert.That(result, Is.EqualTo(@"$1 {0} \ $&"));
}
[Test]
public void Value_containing_its_own_placeholder_is_not_expanded_recursively()
{
var result = "[NAME]".ReplacePlaceholders(new EchoNameModel());
Assert.That(result, Is.EqualTo("[NAME]"));
}
[Test]
public void Unicode_value_is_preserved()
{
var result = "[NAME]".ReplacePlaceholders(new PersonNameModel { Name = "Z\u00fcrich \ud83d\ude00" });
Assert.That(result, Is.EqualTo("Z\u00fcrich \ud83d\ude00"));
}
#endregion
#region Property discovery
[Test]
public void Public_readable_property_is_replaced()
{
var result = "[SHOWN]".ReplacePlaceholders(new DiscoveryModel());
Assert.That(result, Is.EqualTo("shown"));
}
[Test]
public void Get_only_property_is_replaced()
{
var result = "[READ_ONLY]".ReplacePlaceholders(new DiscoveryModel());
Assert.That(result, Is.EqualTo("computed"));
}
[Test]
public void Static_private_write_only_and_indexer_properties_are_ignored()
{
const string template = "[HIDDEN]|[STATIC]|[WRITE_ONLY]|[ITEM]";
var result = template.ReplacePlaceholders(new DiscoveryModel());
Assert.That(result, Is.EqualTo(template));
}
[Test]
public void Inherited_public_property_is_replaced()
{
var result = "[ID]|[NAME]".ReplacePlaceholders(new DerivedModel());
Assert.That(result, Is.EqualTo("base|derived"));
}
[Test]
public void Record_properties_are_replaced()
{
var result = "[FIRST_NAME]/[AGE]".ReplacePlaceholders(new PersonRecord("Ada", 36));
Assert.That(result, Is.EqualTo("Ada/36"));
}
[Test]
public void Struct_data_is_supported()
{
var result = "[NAME]".ReplacePlaceholders(new StructModel { Name = "struct" });
Assert.That(result, Is.EqualTo("struct"));
}
[Test]
public void Anonymous_type_is_supported()
{
var data = new { Name = "anon", Age = 42 };
var result = "[NAME]|[AGE]".ReplacePlaceholders(data);
Assert.That(result, Is.EqualTo("anon|42"));
}
[Test]
public void Generic_type_is_supported_for_reference_type()
{
var result = "[VALUE]".ReplacePlaceholders(new Box<string> { Value = "boxed" });
Assert.That(result, Is.EqualTo("boxed"));
}
[Test]
public void Generic_type_is_supported_for_value_type()
{
var result = "[VALUE]".ReplacePlaceholders(new Box<int> { Value = 5 });
Assert.That(result, Is.EqualTo("5"));
}
#endregion
#region Cache and thread safety
[Test]
public void Cached_metadata_uses_values_of_current_instance()
{
var first = "[FIRST_NAME]".ReplacePlaceholders(new PersonModel { FirstName = "first" });
var second = "[FIRST_NAME]".ReplacePlaceholders(new PersonModel { FirstName = "second" });
Assert.That(new[] { first, second }, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Two_types_with_same_property_name_are_resolved_independently()
{
var a = "[NAME]".ReplacePlaceholders(new TypeA());
var b = "[NAME]".ReplacePlaceholders(new TypeB());
Assert.That(new[] { a, b }, Is.EqualTo(new[] { "a", "b" }));
}
[Test]
public void Concurrent_first_calls_are_thread_safe()
{
var failures = 0;
Parallel.For(0, 500, i =>
{
var expected = $"n{i}";
var actual = "[NAME]".ReplacePlaceholders(new ConcurrentModel { Name = expected });
if (actual != expected)
Interlocked.Increment(ref failures);
});
Assert.That(failures, Is.Zero);
}
#endregion
#region Real DTO
[Test]
public void Document_received_notification_is_fully_replaced()
{
var data = new DocumentReceivedNotification
{
DocumentTitle = "Vertrag",
NameReceiver = "Max",
NameSender = "Erika",
IsReadAndConfirm = true,
AdvancedSignature = false,
LinkToDocument = "https://x.io/a",
Message = "Bitte",
NamePortal = "Portal"
};
const string template =
"[NAME_RECEIVER]|[NAME_SENDER]|[SIGNATURE_TYPE]|[DOCUMENT_TITLE]|[MESSAGE]|[NAME_PORTAL]|[LINK_TO_DOCUMENT]|[LINK_TO_DOCUMENT_TEXT]";
var result = template.ReplacePlaceholders(data);
Assert.That(result, Is.EqualTo("Max|Erika|Signieren|Vertrag|Bitte|Portal|https://x.io/a|https://x.io/a"));
}
#endregion
#region Models
private sealed class PersonModel
{
public string FirstName { get; set; } = "v";
public string LastName { get; set; } = "v";
}
private sealed class PersonNameModel
{
public string Name { get; set; } = "v";
}
private sealed class IdModel
{
public string Id { get; set; } = "v";
}
private sealed class UpperIdModel
{
public string ID { get; set; } = "v";
}
private sealed class AcronymModel
{
public string URLValue { get; set; } = "v";
public string HTTPStatusCode { get; set; } = "v";
public string XMLHttpRequest { get; set; } = "v";
public string OTP { get; set; } = "v";
}
private sealed class DigitModel
{
public string Field1 { get; set; } = "v";
public string Value2Text { get; set; } = "v";
public string A1B2 { get; set; } = "v";
public string Name2 { get; set; } = "v";
}
private sealed class MyUrl2ValueModel
{
public string MyURL2Value { get; set; } = "v";
}
private sealed class SingleLetterModel
{
public string X { get; set; } = "v";
}
private sealed class LowerCaseModel
{
public string firstName { get; set; } = "v";
}
private sealed class LowerCaseInfoModel
{
public string info { get; set; } = "v";
}
private sealed class SnakeCaseModel
{
public string First_Name { get; set; } = "v";
}
private sealed class TfaModel
{
public string TFAExpiration { get; set; } = "v";
}
private sealed class CustomValue
{
public override string ToString() => "custom";
}
private sealed class EchoNameModel
{
public string Name { get; set; } = "[NAME]";
}
private sealed class DiscoveryModel
{
public static string Static { get; set; } = "static";
public string ReadOnly => "computed";
public string WriteOnly
{
set { }
}
public string this[int index] => "indexer";
public string Shown { get; set; } = "shown";
private string Hidden { get; set; } = "hidden";
}
private class BaseModel
{
public string Id { get; set; } = "base";
}
private sealed class DerivedModel : BaseModel
{
public string Name { get; set; } = "derived";
}
private sealed record PersonRecord(string FirstName, int Age);
private struct StructModel
{
public string Name { get; set; }
}
private sealed class Box<T>
{
public T Value { get; set; } = default!;
}
private sealed class TypeA
{
public string Name { get; set; } = "a";
}
private sealed class TypeB
{
public string Name { get; set; } = "b";
}
private sealed class ConcurrentModel
{
public string Name { get; set; } = string.Empty;
}
#endregion
}