Skip to content

Commit b75965a

Browse files
committed
#6: added first tests
1 parent 5e6491f commit b75965a

File tree

8 files changed

+374
-3
lines changed

8 files changed

+374
-3
lines changed

TempMail.Client.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{4591
1212
EndProject
1313
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TempMail.Sample.Console", "samples\TempMail.Sample.Console\TempMail.Sample.Console.csproj", "{96C63A54-708D-481B-AF3F-79EF95687C93}"
1414
EndProject
15+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TempMail.Client.Tests", "tests\TempMail.Client.Tests\TempMail.Client.Tests.csproj", "{A06EA602-D7A9-41F7-A0EF-0B528F42207E}"
16+
EndProject
1517
Global
1618
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1719
Debug|Any CPU = Debug|Any CPU
@@ -30,10 +32,15 @@ Global
3032
{96C63A54-708D-481B-AF3F-79EF95687C93}.Debug|Any CPU.Build.0 = Debug|Any CPU
3133
{96C63A54-708D-481B-AF3F-79EF95687C93}.Release|Any CPU.ActiveCfg = Release|Any CPU
3234
{96C63A54-708D-481B-AF3F-79EF95687C93}.Release|Any CPU.Build.0 = Release|Any CPU
35+
{A06EA602-D7A9-41F7-A0EF-0B528F42207E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36+
{A06EA602-D7A9-41F7-A0EF-0B528F42207E}.Debug|Any CPU.Build.0 = Debug|Any CPU
37+
{A06EA602-D7A9-41F7-A0EF-0B528F42207E}.Release|Any CPU.ActiveCfg = Release|Any CPU
38+
{A06EA602-D7A9-41F7-A0EF-0B528F42207E}.Release|Any CPU.Build.0 = Release|Any CPU
3339
EndGlobalSection
3440
GlobalSection(NestedProjects) = preSolution
3541
{8467F555-F117-4F2A-B378-7B5445A78436} = {35D7467F-42EE-4418-B9E3-05F0D6157B48}
3642
{5B755866-D73F-4E3F-A4EC-E446D9FC7474} = {35D7467F-42EE-4418-B9E3-05F0D6157B48}
3743
{96C63A54-708D-481B-AF3F-79EF95687C93} = {4591572E-9C34-4AED-83DE-1D2361416EAF}
44+
{A06EA602-D7A9-41F7-A0EF-0B528F42207E} = {8BEC36C2-0A03-465F-97F2-14E94F4B827B}
3845
EndGlobalSection
3946
EndGlobal

src/TempMail.Client/Requests/CreateEmailRequest.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ public static CreateEmailRequest ByDomainType(DomainType domainType)
5050

5151
return new CreateEmailByDomainTypeRequest(domainType);
5252
}
53+
/// <summary>
54+
/// Email, only available if the request was created by email
55+
/// </summary>
56+
public string? Email => this is CreateEmailByEmailRequest request ? request.Email : null;
57+
58+
/// <summary>
59+
/// Domain, only available if the request was created by domain
60+
/// </summary>
61+
public string? Domain => this is CreateEmailByDomainRequest request ? request.Domain : null;
62+
63+
/// <summary>
64+
/// Domain type, only available if the request was created by domain type
65+
/// </summary>
66+
public DomainType? DomainType => this is CreateEmailByDomainTypeRequest request ? request.DomainType : null;
5367
}
5468

5569
/// <summary>
@@ -66,7 +80,7 @@ internal CreateEmailByEmailRequest(string email)
6680
/// <summary>
6781
/// Specific e-mail address with which the box will be created
6882
/// </summary>
69-
public string Email { get; }
83+
public new string Email { get; }
7084
}
7185

7286
/// <summary>
@@ -83,7 +97,7 @@ internal CreateEmailByDomainRequest(string domain)
8397
/// <summary>
8498
/// Specific domain on which the box will be created
8599
/// </summary>
86-
public string Domain { get; }
100+
public new string Domain { get; }
87101
}
88102

89103
/// <summary>
@@ -100,5 +114,5 @@ internal CreateEmailByDomainTypeRequest(DomainType domainType)
100114
/// <summary>
101115
/// Specific <see cref="DomainType"/> of which the box will be created
102116
/// </summary>
103-
public DomainType DomainType { get; }
117+
public new DomainType DomainType { get; }
104118
}

src/TempMail.Client/TempMailClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class TempMailClient : ITempMailClient
2323
private static readonly JsonSerializerOptions JsonOptions = new ()
2424
{
2525
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
26+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
2627
Converters =
2728
{
2829
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using TempMail.Client.Models;
4+
using TempMail.Client.Requests;
5+
6+
namespace TempMail.Client.Tests;
7+
8+
internal class CreateEmailRequestJsonConverter : JsonConverter<CreateEmailRequest>
9+
{
10+
public override CreateEmailRequest? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11+
{
12+
if (reader.TokenType != JsonTokenType.StartObject)
13+
{
14+
throw new JsonException();
15+
}
16+
17+
if (!reader.Read())
18+
{
19+
throw new JsonException();
20+
}
21+
22+
if (reader.TokenType == JsonTokenType.EndObject)
23+
{
24+
return null;
25+
}
26+
27+
if (reader.TokenType != JsonTokenType.PropertyName)
28+
{
29+
throw new JsonException();
30+
}
31+
32+
var propertyName = reader.GetString();
33+
34+
if (!reader.Read())
35+
{
36+
throw new JsonException();
37+
}
38+
39+
var propertyValue = reader.GetString() ?? throw new JsonException();
40+
41+
CreateEmailRequest result;
42+
switch (propertyName)
43+
{
44+
case "email":
45+
result = CreateEmailRequest.ByEmail(propertyValue);
46+
break;
47+
case "domain":
48+
result = CreateEmailRequest.ByDomain(propertyValue);
49+
break;
50+
case "domain_type":
51+
if (!Enum.TryParse<DomainType>(propertyValue, ignoreCase: true, out var domainType))
52+
{
53+
throw new JsonException();
54+
}
55+
result = CreateEmailRequest.ByDomainType(domainType);
56+
break;
57+
58+
default:
59+
throw new JsonException();
60+
}
61+
62+
while (reader.TokenType != JsonTokenType.EndObject)
63+
{
64+
reader.Read();
65+
}
66+
67+
return result ?? throw new JsonException();
68+
}
69+
70+
public override void Write(Utf8JsonWriter writer, CreateEmailRequest value, JsonSerializerOptions options)
71+
{
72+
switch (value)
73+
{
74+
case CreateEmailByEmailRequest emailRequest:
75+
JsonSerializer.Serialize(writer, emailRequest, options);
76+
break;
77+
case CreateEmailByDomainRequest emailRequest:
78+
JsonSerializer.Serialize(writer, emailRequest, options);
79+
break;
80+
case CreateEmailByDomainTypeRequest emailRequest:
81+
JsonSerializer.Serialize(writer, emailRequest, options);
82+
break;
83+
default:
84+
throw new JsonException("Invalid CreateEmailRequest");
85+
}
86+
}
87+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.RegularExpressions;
3+
4+
namespace TempMail.Client.Tests;
5+
6+
[AttributeUsage(AttributeTargets.Method)]
7+
internal class HandlerAttribute(
8+
[StringSyntax(StringSyntaxAttribute.Regex)] string route,
9+
string httpMethod)
10+
: Attribute
11+
{
12+
private readonly Regex _regex = new(route, RegexOptions.Compiled);
13+
14+
public string Route { get; } = route;
15+
16+
public string? Matches(HttpRequestMessage request)
17+
{
18+
var match = _regex.Match(request.RequestUri?.ToString() ?? string.Empty);
19+
if (!match.Success ||
20+
!request.Method.Method.Equals(httpMethod, StringComparison.InvariantCultureIgnoreCase))
21+
{
22+
return null;
23+
}
24+
25+
return match.Groups.Count > 1
26+
? match.Groups[1].Value
27+
: string.Empty;
28+
}
29+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.Net;
2+
using System.Reflection;
3+
using System.Text.Json;
4+
using TempMail.Client.Requests;
5+
using TempMail.Client.Responses;
6+
7+
namespace TempMail.Client.Tests;
8+
9+
internal class MockingHttpMessageHandler : HttpMessageHandler
10+
{
11+
private bool _returnErrors;
12+
13+
private static MockingHttpMessageHandler? _instance;
14+
15+
public MockingHttpMessageHandler()
16+
{
17+
if (_instance != null)
18+
{
19+
throw new InvalidOperationException("MockingHttpMessageHandler instance already exists");
20+
}
21+
22+
_instance = this;
23+
}
24+
25+
26+
private static readonly IReadOnlyCollection<(HandlerAttribute Handler, Func<HttpRequestMessage, Task<HttpResponseMessage>> Method)>
27+
Handlers =
28+
typeof(MockingHttpMessageHandler)
29+
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
30+
.Select(x =>
31+
(x.GetCustomAttribute<HandlerAttribute>()!, CreateMethod(x)))
32+
.Where(x => x.Item1 != null)
33+
.ToList();
34+
35+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
36+
{
37+
foreach (var handler in Handlers)
38+
{
39+
if (handler.Handler.Matches(request) == null)
40+
{
41+
continue;
42+
}
43+
44+
return handler.Method(request);
45+
}
46+
47+
throw new Exception($"No handler found for {request.RequestUri}");
48+
}
49+
50+
private HttpResponseMessage ReturnError<TResponse>()
51+
{
52+
var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest);
53+
54+
httpResponseMessage.Content = new StringContent(JsonSerializer.Serialize(
55+
new ErrorResponse(
56+
new Error(ErrorType.ApiError, "api_error", "An error occured during request handling"),
57+
new ErrorMeta(Guid.NewGuid().ToString())),
58+
JsonOptions));
59+
60+
return httpResponseMessage;
61+
}
62+
63+
[Handler(".*/v1/emails", "POST")]
64+
private async Task<HttpResponseMessage> HandleCreateEmail(HttpRequestMessage request)
65+
{
66+
if (_returnErrors)
67+
{
68+
return ReturnError<HttpResponseMessage>();
69+
}
70+
var requestBody = JsonSerializer.Deserialize<CreateEmailRequest>(
71+
await request.Content.ReadAsStringAsync(),
72+
JsonOptions);
73+
74+
var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK);
75+
var resp = requestBody switch
76+
{
77+
CreateEmailByEmailRequest { Email: var email } => new CreateEmailResponse(email, int.MaxValue),
78+
CreateEmailByDomainRequest { Domain: var domain } => new CreateEmailResponse($"random@{domain}", int.MaxValue),
79+
CreateEmailByDomainTypeRequest { DomainType: var domainType } => new CreateEmailResponse($"random@{domainType.ToString().ToLowerInvariant()}.io", int.MaxValue),
80+
_ => throw new Exception($"Unknown request body type: {requestBody?.GetType().FullName}")
81+
};
82+
httpResponseMessage.Content = new StringContent(JsonSerializer.Serialize(resp, new JsonSerializerOptions
83+
{
84+
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
85+
Converters = { new CreateEmailRequestJsonConverter() }
86+
}));
87+
return httpResponseMessage;
88+
}
89+
90+
private static JsonSerializerOptions JsonOptions =>
91+
new()
92+
{
93+
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
94+
Converters = { new CreateEmailRequestJsonConverter() }
95+
};
96+
97+
private static Func<HttpRequestMessage, Task<HttpResponseMessage>> CreateMethod(MethodInfo methodInfo) => request =>
98+
{
99+
var handler = methodInfo.GetCustomAttribute<HandlerAttribute>()!;
100+
var match = handler.Matches(request)!;
101+
102+
if (match == string.Empty)
103+
{
104+
return (Task<HttpResponseMessage>)methodInfo.Invoke(_instance, [request])!;
105+
}
106+
107+
return (Task<HttpResponseMessage>)methodInfo.Invoke(_instance, [request, match])!;
108+
};
109+
110+
public void ReturnErrors(bool returnErrors = true) => _returnErrors = returnErrors;
111+
112+
public new void Dispose()
113+
{
114+
_instance = null!;
115+
base.Dispose();
116+
}
117+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
15+
<PackageReference Include="NUnit" Version="3.14.0"/>
16+
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
17+
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<Using Include="NUnit.Framework"/>
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\src\TempMail.Client\TempMail.Client.csproj" />
26+
</ItemGroup>
27+
28+
</Project>

0 commit comments

Comments
 (0)