Skip to content

Commit e6d530e

Browse files
Code review code and add new icon
1 parent 3d04106 commit e6d530e

8 files changed

Lines changed: 169 additions & 64 deletions

File tree

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ on:
55
branches: [ main, master ]
66
pull_request:
77
branches: [ main, master ]
8-
workflow_dispatch:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
912

1013
jobs:
1114
test:

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,9 @@ appsettings.Development.json
8787
TestResults/
8888
*.sequenced
8989
*.coverage
90-
*.opencoverxml
90+
*.opencoverxml
91+
92+
## =========================================================================
93+
## Claude Code
94+
## =========================================================================
95+
.claude/

OutSystems.YAEmailValidator.UnitTests/OutSystems.YAEmailValidator.UnitTests.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
<IsPackable>false</IsPackable>
99

10-
<LangVersion>10</LangVersion>
1110
</PropertyGroup>
1211

1312
<ItemGroup>
@@ -25,4 +24,8 @@
2524
</PackageReference>
2625
</ItemGroup>
2726

27+
<ItemGroup>
28+
<ProjectReference Include="..\OutSystems.YAEmailValidator\OutSystems.YAEmailValidator.csproj" />
29+
</ItemGroup>
30+
2831
</Project>
Lines changed: 140 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
1-
using NUnit.Framework;
2-
using EmailValidation;
1+
using NUnit.Framework;
32

4-
namespace OutSystems.YAEmailValidalitor.Tests
3+
namespace OutSystems.YAEmailValidator.UnitTests
54
{
5+
/// <summary>
6+
/// Unit tests for <see cref="YAEmailValidator.EmailValidate"/>.
7+
/// All tests exercise the wrapper (not the underlying EmailValidation library directly).
8+
///
9+
/// TEST INDEX
10+
/// ──────────────────────────────────────────────────────────────────
11+
/// # Method Cases Line
12+
/// ──────────────────────────────────────────────────────────────────
13+
/// 1. Validate_ValidEmails_ReturnsTrue 8 30
14+
/// 2. Validate_InvalidEmails_ReturnsFalse 10 45
15+
/// 3. Validate_EmptyOrWhitespace_ReturnsFalse 2 62
16+
/// 4. Validate_NullEmail_ThrowsArgumentNullException 1 69
17+
/// 5. Validate_LeadingTrailingWhitespace_WhenNotAllowed 3 80
18+
/// 6. Validate_LeadingTrailingWhitespace_WhenAllowed 3 89
19+
/// 7. Validate_InternationalEmails_WhenAllowed_ReturnsTrue 2 99
20+
/// 8. Validate_InternationalEmails_WhenNotAllowed_ReturnsFalse 1 108
21+
/// 9. Validate_TopLevelDomain_WhenAllowed_ReturnsTrue 1 116
22+
/// 10. Validate_TopLevelDomain_WhenNotAllowed_ReturnsFalse 1 123
23+
/// 11. Validate_ShouldRejectDisplayNamesAndComments 2 131
24+
/// 12. Validate_LocalPartBoundary 2 142
25+
/// 13. Validate_TotalLengthBoundary 3 157
26+
/// 14. Validate_ShouldRejectInvalidDotPlacement 3 179
27+
/// ──────────────────────────────────────────────────────────────────
28+
/// Total: 39
29+
///
30+
/// COVERAGE BY FEATURE
31+
/// ──────────────────────────────────────────────────────────────────
32+
/// Feature / Flag Tests
33+
/// ──────────────────────────────────────────────────────────────────
34+
/// Basic valid/invalid emails #1, #2
35+
/// Empty/whitespace/null input #3, #4
36+
/// allowLeadingTrailingWhitespace flag #5, #6
37+
/// allowInternational flag (RFC 6531) #7, #8
38+
/// allowTopLevelDomains flag #9, #10
39+
/// RFC 5321 compliance #11 (display names), #12 (local part 64-char),
40+
/// #13 (total 254-char), #14 (dot placement)
41+
/// ──────────────────────────────────────────────────────────────────
42+
/// </summary>
643
[TestFixture]
7-
public class EmailValidatorTests
44+
public class YAEmailValidatorTests
845
{
9-
// Tests for valid email addresses
46+
private readonly YAEmailValidator _validator = new();
47+
48+
private bool Validate(string email,
49+
bool allowWhitespace = false,
50+
bool allowInternational = false,
51+
bool allowTopLevelDomains = false)
52+
{
53+
_validator.EmailValidate(email, allowWhitespace, allowInternational, allowTopLevelDomains, out bool result);
54+
return result;
55+
}
56+
57+
// --- Valid emails ---
58+
1059
[TestCase("test@example.com")]
1160
[TestCase("firstname.lastname@domain.com")]
1261
[TestCase("email@subdomain.domain.com")]
@@ -17,11 +66,11 @@ public class EmailValidatorTests
1766
[TestCase("email@domain.co.jp")]
1867
public void Validate_ValidEmails_ReturnsTrue(string email)
1968
{
20-
bool result = EmailValidator.Validate(email);
21-
Assert.That(result, Is.True, $"Expected '{email}' to be valid.");
69+
Assert.That(Validate(email), Is.True, $"Expected '{email}' to be valid.");
2270
}
2371

24-
// Tests for invalid email addresses
72+
// --- Invalid emails ---
73+
2574
[TestCase("plainaddress")] // No @ or domain
2675
[TestCase("#@%^%#$@#$@#.com")] // Garbage characters
2776
[TestCase("@domain.com")] // Missing username
@@ -34,81 +83,136 @@ public void Validate_ValidEmails_ReturnsTrue(string email)
3483
[TestCase("email@domain..com")] // Double dots in domain
3584
public void Validate_InvalidEmails_ReturnsFalse(string email)
3685
{
37-
bool result = EmailValidator.Validate(email);
38-
Assert.That(result, Is.False, $"Expected '{email}' to be invalid.");
86+
Assert.That(Validate(email), Is.False, $"Expected '{email}' to be invalid.");
3987
}
4088

41-
// Tests for whitespace
89+
// --- Empty and whitespace ---
90+
4291
[TestCase("")]
4392
[TestCase(" ")]
44-
public void Validate_EmptyOrNull_ReturnsFalse(string email)
93+
public void Validate_EmptyOrWhitespace_ReturnsFalse(string email)
94+
{
95+
Assert.That(Validate(email), Is.False);
96+
}
97+
98+
// --- Null handling ---
99+
100+
[Test]
101+
public void Validate_NullEmail_ThrowsArgumentNullException()
102+
{
103+
Assert.Throws<ArgumentNullException>(() =>
104+
_validator.EmailValidate(null!, false, false, false, out _));
105+
}
106+
107+
// --- Whitespace flag ---
108+
109+
[TestCase(" test@example.com")]
110+
[TestCase("test@example.com ")]
111+
[TestCase(" test@example.com ")]
112+
public void Validate_LeadingTrailingWhitespace_WhenNotAllowed_ReturnsFalse(string email)
113+
{
114+
Assert.That(Validate(email, allowWhitespace: false), Is.False,
115+
$"Expected '{email}' to be invalid when whitespace is not allowed.");
116+
}
117+
118+
[TestCase(" test@example.com")]
119+
[TestCase("test@example.com ")]
120+
[TestCase(" test@example.com ")]
121+
public void Validate_LeadingTrailingWhitespace_WhenAllowed_ReturnsTrue(string email)
122+
{
123+
Assert.That(Validate(email, allowWhitespace: true), Is.True,
124+
$"Expected '{email}' to be valid when whitespace is allowed (trimmed before validation).");
125+
}
126+
127+
// --- International support (RFC 6531) ---
128+
129+
[TestCase("t\u00e9st@domain.com")]
130+
[TestCase("\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a")]
131+
public void Validate_InternationalEmails_WhenAllowed_ReturnsTrue(string email)
132+
{
133+
Assert.That(Validate(email, allowInternational: true), Is.True,
134+
$"Expected international email '{email}' to be valid.");
135+
}
136+
137+
[TestCase("t\u00e9st@domain.com")]
138+
public void Validate_InternationalEmails_WhenNotAllowed_ReturnsFalse(string email)
45139
{
46-
bool result = EmailValidator.Validate(email);
47-
Assert.That(result, Is.False);
140+
Assert.That(Validate(email, allowInternational: false), Is.False,
141+
$"Expected international email '{email}' to be invalid when international is not allowed.");
48142
}
49143

50-
// To truly validate that your library is adhering to RFC 5321, you should use these specific test cases.
51-
// These are designed to catch "loose" validators that fail to enforce the technical limits of the SMTP protocol.
144+
// --- Top-level domain flag ---
145+
146+
[Test]
147+
public void Validate_TopLevelDomain_WhenAllowed_ReturnsTrue()
148+
{
149+
Assert.That(Validate("user@localhost", allowTopLevelDomains: true), Is.True,
150+
"Expected 'user@localhost' to be valid when TLDs are allowed.");
151+
}
52152

53-
// INTERNATIONALIZATION (RFC 6531)
54-
[TestCase("tést@domain.com")]
55-
[TestCase("用户@例子.广告")]
56-
public void Validate_InternationalEmails(string email)
153+
[Test]
154+
public void Validate_TopLevelDomain_WhenNotAllowed_ReturnsFalse()
57155
{
58-
// Jeffrey's lib supports this if the overload is called correctly
59-
Assert.That(EmailValidator.Validate(email, allowInternational: true), Is.True);
156+
Assert.That(Validate("user@localhost", allowTopLevelDomains: false), Is.False,
157+
"Expected 'user@localhost' to be invalid when TLDs are not allowed.");
60158
}
61159

62-
// 1. THE "DISPLAY NAME" TRAP
63-
// MailAddress passes this; EmailValidation correctly fails it.
160+
// --- Display name rejection (RFC 5321) ---
161+
64162
[TestCase("Jeffrey Stedfast <jestedfa@microsoft.com>")]
65163
[TestCase("jestedfa@microsoft.com (Jeffrey Stedfast)")]
66164
public void Validate_ShouldRejectDisplayNamesAndComments(string email)
67165
{
68-
Assert.That(EmailValidator.Validate(email), Is.False,
166+
Assert.That(Validate(email), Is.False,
69167
"RFC 5321 Address literals should not include display names or comments.");
70168
}
71169

72-
// 2. LOCAL PART LENGTH (Exactly 64 chars is allowed, 65 is not)
170+
// --- Local part length boundary (64 chars max) ---
171+
73172
[Test]
74173
public void Validate_LocalPartBoundary()
75174
{
76175
string sixtyFourChars = new string('a', 64);
77176
string sixtyFiveChars = new string('a', 65);
78177

79-
Assert.That(EmailValidator.Validate($"{sixtyFourChars}@domain.com"), Is.True, "64 chars should pass.");
80-
Assert.That(EmailValidator.Validate($"{sixtyFiveChars}@domain.com"), Is.False, "65 chars must fail.");
178+
Assert.Multiple(() =>
179+
{
180+
Assert.That(Validate($"{sixtyFourChars}@domain.com"), Is.True, "64 chars should pass.");
181+
Assert.That(Validate($"{sixtyFiveChars}@domain.com"), Is.False, "65 chars must fail.");
182+
});
81183
}
82184

83-
// 3. TOTAL LENGTH (Maximum 254 characters)
185+
// --- Total length boundary (254 chars max) ---
186+
84187
[Test]
85188
public void Validate_TotalLengthBoundary()
86-
{// A valid domain must have labels no longer than 63 characters
189+
{
190+
// A valid domain must have labels no longer than 63 characters
87191
string label63 = new string('b', 63);
88192
string domain = $"{label63}.{label63}.{label63}.com"; // 63*3 + 3 dots + 3 'com' = 195 chars
89193

90194
// 254 - 1 (@) - 195 (domain) = 58
91195
string local = new string('a', 58);
92-
93196
string valid254 = $"{local}@{domain}";
94197

95198
Assert.Multiple(() =>
96199
{
97200
Assert.That(valid254.Length, Is.EqualTo(254), "Manual check that string is 254");
98-
Assert.That(EmailValidator.Validate(valid254), Is.True, "254 chars with valid labels should pass.");
201+
Assert.That(Validate(valid254), Is.True, "254 chars with valid labels should pass.");
99202

100203
string invalid255 = "a" + valid254;
101-
Assert.That(EmailValidator.Validate(invalid255), Is.False, "255 chars must fail.");
204+
Assert.That(Validate(invalid255), Is.False, "255 chars must fail.");
102205
});
103206
}
104207

105-
// 4. THE "DOUBLE DOT" AND PURE SYNTAX
208+
// --- Invalid dot placement ---
209+
106210
[TestCase("user..name@domain.com")] // Consecutive dots
107211
[TestCase(".user@domain.com")] // Leading dot
108212
[TestCase("user.@domain.com")] // Trailing dot in local part
109213
public void Validate_ShouldRejectInvalidDotPlacement(string email)
110214
{
111-
Assert.That(EmailValidator.Validate(email), Is.False);
215+
Assert.That(Validate(email), Is.False);
112216
}
113217
}
114-
}
218+
}

OutSystems.YAEmailValidator/IYAEmailValidator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public interface IYAEmailValidator
2020
/// When false, the method returns false if the provided email contains any leading or trailing whitespace.
2121
/// </param>
2222
/// <param name="allowInternational">If true, non-ASCII international addresses are allowed.</param>
23-
/// <param name="allowTopLevelDomains">If true, top-level domains are allowed (prevents local-only addresses like "user@localhost").
24-
/// True when the email is valid; otherwise false.</returns>
23+
/// <param name="allowTopLevelDomains">If true, top-level domains are allowed (prevents local-only addresses like "user@localhost").</param>
24+
/// <param name="isValidEmail">True when the email is valid according to RFC 5321; otherwise false.</param>
2525
[OSAction(
2626
Description = "Validates the specified email address (using the RFC 5321) with optional flags for trimming and international/TLD support.",
2727
IconResourceName = "OutSystems.YAEmailValidator.resources.YAEmailValidator_icon.png"
@@ -33,7 +33,7 @@ void EmailValidate(
3333
bool allowLeadingTrailingWhitespace,
3434
[OSParameterAttribute(Description = "If true, non-ASCII international addresses are allowed.")]
3535
bool allowInternational,
36-
[OSParameterAttribute(Description = "If true, top-level domains are allowed (prevents local-only addresses like 'user@localhost').Returns True when the email is valid; otherwise false.")]
36+
[OSParameterAttribute(Description = "If true, top-level domains are allowed (prevents local-only addresses like 'user@localhost').")]
3737
bool allowTopLevelDomains,
3838
[OSParameterAttribute(Description = "True, if email is valid according to RFC 5321. Otherwise, false.")]
3939
out bool isValidEmail

OutSystems.YAEmailValidator/YAEmailValidator.cs

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ public class YAEmailValidator : IYAEmailValidator
1212
/// Validates the specified email address with optional flags for trimming and international/TLD support.
1313
/// </summary>
1414
/// <param name="emailToValidate">The email to validate.</param>
15-
/// <param name="allowLeadingTrailingWhitespace_optional">
15+
/// <param name="allowLeadingTrailingWhitespace">
1616
/// When true, leading/trailing whitespace will be ignored (the email is trimmed before validation).
1717
/// When false, the method returns false if the provided email contains any leading or trailing whitespace.
1818
/// </param>
19-
/// <param name="allowInternational_optional">If true, non-ASCII international addresses are allowed.</param>
20-
/// <param name="allowTopLevelDomains_optional">If true, top-level domains are allowed (prevents local-only addresses like "user@localhost").</param>
21-
/// <returns>True when the email is valid; otherwise false.</returns>
19+
/// <param name="allowInternational">If true, non-ASCII international addresses are allowed.</param>
20+
/// <param name="allowTopLevelDomains">If true, top-level domains are allowed (prevents local-only addresses like "user@localhost").</param>
21+
/// <param name="isValidEmail">True when the email is valid according to RFC 5321; otherwise false.</param>
2222
public void EmailValidate(
2323
string emailToValidate,
2424
bool allowLeadingTrailingWhitespace,
@@ -28,29 +28,19 @@ public void EmailValidate(
2828
{
2929
if (emailToValidate is null) throw new ArgumentNullException(nameof(emailToValidate));
3030

31-
// If trimming is not allowed and the provided email contains leading/trailing whitespace,
32-
// treat it as invalid (return false).
33-
if (!allowLeadingTrailingWhitespace)
34-
{
35-
if (!string.Equals(emailToValidate, emailToValidate.Trim(), StringComparison.Ordinal))
36-
{
37-
isValidEmail = false;
38-
return;
39-
}
31+
var trimmed = emailToValidate.Trim();
4032

41-
// No trimming allowed, validate the exact provided value.
42-
isValidEmail = EmailValidator.Validate(emailToValidate,
43-
allowInternational: allowInternational,
44-
allowTopLevelDomains: allowTopLevelDomains);
33+
// If trimming is not allowed and whitespace was present, reject immediately.
34+
if (!allowLeadingTrailingWhitespace && !string.Equals(trimmed, emailToValidate, StringComparison.Ordinal))
35+
{
36+
isValidEmail = false;
4537
return;
4638
}
4739

48-
// Trimming allowed: remove leading/trailing whitespace and validate the trimmed value.
49-
var email = emailToValidate.Trim();
50-
isValidEmail = EmailValidator.Validate(email,
40+
isValidEmail = EmailValidator.Validate(
41+
allowLeadingTrailingWhitespace ? trimmed : emailToValidate,
5142
allowInternational: allowInternational,
5243
allowTopLevelDomains: allowTopLevelDomains);
53-
return;
5444
}
5545
}
5646
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Set-ExecutionPolicy -Scope CurrentUser Unrestricted
1+
Set-ExecutionPolicy -Scope Process Bypass
22
dotnet publish -c Release -r linux-x64 --self-contained false
33
Compress-Archive -Path .\bin\Release\net8.0\linux-x64\publish\* -DestinationPath YAEmailValidator_Asset.zip
-722 Bytes
Loading

0 commit comments

Comments
 (0)