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.
479 lines
13 KiB
C#
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
|
|
}
|