Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/mailgun entry point #94

Open
wants to merge 10 commits into
base: milestone/mailgun-support
Choose a base branch
from
21 changes: 21 additions & 0 deletions src/Sinch/Core/UrlResolver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Sinch.Conversation;
using Sinch.Mailgun;
using Sinch.SMS;
using Sinch.Voice;

Expand Down Expand Up @@ -83,5 +84,25 @@ public Uri ResolveSmsServicePlanIdUrl(SmsServicePlanIdRegion smsServicePlanIdReg
return new Uri(string.Format(smsApiServicePlanIdUrlTemplate,
smsServicePlanIdRegion.Value.ToLowerInvariant()));
}

public Uri ResolveMailgunUrl(MailgunRegion mailgunRegion)
{
if (!string.IsNullOrEmpty(_apiUrlOverrides?.MailgunUrl)) return new Uri(_apiUrlOverrides.MailgunUrl);

string? mailgunUrl;
switch (mailgunRegion)
{
case MailgunRegion.Us:
mailgunUrl = "https://api.mailgun.net";
break;
case MailgunRegion.Eu:
mailgunUrl = "https://api.eu.mailgun.net";
break;
default:
throw new ArgumentOutOfRangeException(nameof(mailgunRegion), mailgunRegion, "Unreachable");
}

return new Uri(mailgunUrl);
}
}
}
18 changes: 18 additions & 0 deletions src/Sinch/Mailgun/MailgunRegion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Sinch.Mailgun
{
/// <summary>
/// Mailgun allows the ability to send and receive email in both US and EU regions.
/// Be sure to use the appropriate region on which you have created your domain.
/// </summary>
public enum MailgunRegion
{
/// <summary>
/// United States region
/// </summary>
Us,
/// <summary>
/// Europe region
/// </summary>
Eu
}
}
23 changes: 23 additions & 0 deletions src/Sinch/Mailgun/SinchMailgunClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using Sinch.Core;
using Sinch.Logger;

namespace Sinch.Mailgun
{
/// <summary>
/// The Mailgun API is part of the Sinch family and enables you to send, track, and receive email effortlessly.
/// </summary>
public interface ISinchMailgunClient
{
// TBD: add domains
}

/// <inheritdoc />
internal class SinchMailgunClient : ISinchMailgunClient
{
public SinchMailgunClient(Uri baseUrl, Http http, LoggerFactory? loggerFactory = null)
{
// TBD: implement domains
}
}
}
27 changes: 27 additions & 0 deletions src/Sinch/SinchClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Sinch.Conversation;
using Sinch.Core;
using Sinch.Logger;
using Sinch.Mailgun;
using Sinch.Numbers;
using Sinch.SMS;
using Sinch.Verification;
Expand Down Expand Up @@ -111,6 +112,17 @@ public ISinchVerificationClient Verification(string appKey, string appSecret,
/// <param name="voiceRegion">See <see cref="VoiceRegion" />. Defaults to <see cref="VoiceRegion.Global" /></param>
/// <returns></returns>
public ISinchVoiceClient Voice(string appKey, string appSecret, VoiceRegion? voiceRegion = null);

/// <summary>
/// APIs are at the heart of Mailgun.
/// Our goal is to provide developers worldwide with an accessible and straightforward way to send,
/// receive, and track emails effortlessly.
/// </summary>
/// <param name="apiKey">When you sign up for Mailgun, a primary account API key is generated.
/// This key allows you to perform all CRUD operations via our various API endpoints and for any of your sending domains. </param>
/// <param name="region"><see cref="MailgunRegion"/></param>
/// <returns></returns>
public ISinchMailgunClient Mailgun(string apiKey, MailgunRegion region);
}

public class SinchClient : ISinchClient
Expand Down Expand Up @@ -274,6 +286,21 @@ public ISinchVoiceClient Voice(string appKey, string appSecret,
_urlResolver.ResolveVoiceApplicationManagementUrl());
}

/// <inheritdoc />
public ISinchMailgunClient Mailgun(string apiKey, MailgunRegion region)
{
if (string.IsNullOrEmpty(apiKey))
{
throw new ArgumentNullException(nameof(apiKey), "apiKey shouldn't be null or empty");
}
var baseUrl = _urlResolver.ResolveMailgunUrl(region);
var mailgunAuth = new BasicAuth(appKey: "api", appSecret: apiKey);
// NOTE: jsonNamingPolicy will not play a role here as property naming of mailgun is inconsistent
// meaning, all lifting will be done through JsonPropertyNamingAttribute
var http = new Http(mailgunAuth, _httpClient, _loggerFactory?.Create<IHttp>(), JsonNamingPolicy.CamelCase);
return new SinchMailgunClient(baseUrl, http, _loggerFactory);
}

private void ValidateCommonCredentials()
{
var exceptions = new List<Exception>();
Expand Down
5 changes: 5 additions & 0 deletions src/Sinch/SinchOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,10 @@ public sealed class ApiUrlOverrides
/// Overrides Numbers api base url
/// </summary>
public string? NumbersUrl { get; init; }

/// <summary>
/// Overrides Mailgun api base url
/// </summary>
public string? MailgunUrl { get; init; }
}
}
56 changes: 56 additions & 0 deletions tests/Sinch.Tests/Core/UrlResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using FluentAssertions;
using Sinch.Conversation;
using Sinch.Core;
using Sinch.Mailgun;
using Sinch.SMS;
using Sinch.Voice;
using Xunit;
Expand Down Expand Up @@ -314,5 +315,60 @@ public void ResolveSmsServicePlanIdUrl(SmsServicePlanIdRegion smsServicePlanIdRe
: new Uri(apiUrlOverrides.SmsUrl);
smsUrl.Should().BeEquivalentTo(expectedUrl);
}


public static IEnumerable<object[]> MailgunUrlData => new List<object[]>
{
new object[]
{
MailgunRegion.Us,
null,
},
new object[]
{
MailgunRegion.Eu,
null,
},
new object[]
{
MailgunRegion.Eu,
new ApiUrlOverrides()
{
MailgunUrl = null
}
},
new object[]
{
MailgunRegion.Eu,
new ApiUrlOverrides()
{
MailgunUrl = "https://my-mailgun-proxy.net"
}
},
};

[Theory]
[MemberData(nameof(MailgunUrlData))]
public void ResolveMailgunUrl(MailgunRegion region, ApiUrlOverrides apiUrlOverrides)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There shouldn't be any logic in this test function: here you are recoding the logic from the UrlResolver. It would be better to provide the expected value as part of the test data. And also adding a test name to improve the readability, such as:

public static IEnumerable<object[]> ResolveMailgunUrlTestCases => new List<object[]>
{
    // Format: TestName, Region, Override, ExpectedUrl
    new object[] { "US Region Default URL", MailgunRegion.Us, null, "https://api.mailgun.net" },
    new object[] { "EU Region Default URL", MailgunRegion.Eu, null, "https://api.eu.mailgun.net" },
    new object[] { "EU Region with Null Override", MailgunRegion.Eu, new ApiUrlOverrides { MailgunUrl = null }, "https://api.eu.mailgun.net" },
    new object[] { "EU Region with Custom Override", MailgunRegion.Eu, new ApiUrlOverrides { MailgunUrl = "https://my-mailgun-proxy.net" }, "https://my-mailgun-proxy.net" },
};

The test would be more straightforward:

[Theory]
[MemberData(nameof(ResolveMailgunUrlTestCases))]
public void ResolveMailgunUrl(string testName, MailgunRegion region, ApiUrlOverrides apiUrlOverrides, string expectedUrl)
{
    var actual = new UrlResolver(apiUrlOverrides).ResolveMailgunUrl(region);
    actual.Should().BeEquivalentTo(new Uri(expectedUrl));
}

And in order to test all the branches from the UrlResolver method, you can consider adding another test for an unexpected Region value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, refactored the test

And in order to test all the branches from the UrlResolver method, you can consider adding another test for an unexpected Region value

for a note, MailgunRegion is a enum, it's impossible to add pass unexpected region value

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even with ResolveMailgunUrl((MailgunRegion)(-1)) ?
I thought this was why you added a default clause in the switch statement in the ResolveMailgunUrl method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible with enum casting, yes, even if it's very unlikely, but totally possible, someone going to do that, I'll add that to the test case.
default is for any unexpectancy, yes

{
var actual = new UrlResolver(apiUrlOverrides).ResolveMailgunUrl(region);

if (apiUrlOverrides is { MailgunUrl: not null })
{
actual.Should().BeEquivalentTo(new Uri(apiUrlOverrides.MailgunUrl));
}
else
{
if (region == MailgunRegion.Eu)
{
actual.Should().BeEquivalentTo(new Uri("https://api.eu.mailgun.net"));
}
else
{
actual.Should().BeEquivalentTo(new Uri("https://api.mailgun.net"));
}
}

}
}
}
28 changes: 28 additions & 0 deletions tests/Sinch.Tests/Mailgun/MailgunClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using FluentAssertions;
using Sinch.Mailgun;
using Xunit;

namespace Sinch.Tests.Mailgun
{
public class MailgunClientTests
{
[Fact]
public void InitMailgunClient()
{
var mailgun = new SinchClient(default, default, default).Mailgun("apikey", MailgunRegion.Eu);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's following the same design as Verification and Voice which are not using the same kind of credentials, but it still seems very weird to have to initialize a SinchClient with a triplet of undefined parameters to be able to access to the Mailgun constructor with its own parameters; knowing that on top of that, the overrides for the URL should be part of the optional fourth parameter of the SinchClient.
Do you think it would be valuable to review this design?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this seems somewhat weird, I was thinking about adding an empty constructor new SInchClient() for such a use case, but dropped it for the sake of not providing confusion of two constructor. But I can add it and indicate with a doc comments that an empty constructor is for use of specific APIs in case you need only them. What do you think?
P.S. I don't want to go into a direction of static constructor so two calls of different apis, e.g. sinchClient.Sms.Send() and sinchClient.Mailgun("key").Send() would share the resources.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure to understand what you want to do, but let's go ahead and give it a try.


mailgun.Should().NotBeNull();
}

[Theory]
[InlineData("")]
[InlineData(null)]
public void FailEmptyApiKey(string apiKey)
{
var mailgunCreation = () => new SinchClient(default, default, default).Mailgun(apiKey, MailgunRegion.Eu);

mailgunCreation.Should().Throw<ArgumentNullException>();
}
}
}
Loading