Skip to content

Commit

Permalink
feat: set User Agent for outgoing HTTP requests
Browse files Browse the repository at this point in the history
  • Loading branch information
warriordog committed Aug 22, 2024
1 parent ced3ce0 commit 2c3b3eb
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 33 deletions.
28 changes: 14 additions & 14 deletions ModShark.Tests/Reports/Reporter/SendGridReporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void Setup()
MockLogger = new Mock<ILogger<SendGridReporter>>();
MockHttpService = new Mock<IHttpService>();
MockHttpService
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<SendGridSend>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<CancellationToken>()))
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<SendGridSend>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string>>()))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.Accepted));
MockRenderService = new Mock<IRenderService>();
MockRenderService
Expand Down Expand Up @@ -63,8 +63,8 @@ public async Task MakeReport_ShouldBail_WhenDisabled()
.Verify(s => s.PostAsync(
It.IsAny<string>(),
It.IsAny<SendGridSend>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<CancellationToken>()
It.IsAny<CancellationToken>(),
It.IsAny<IDictionary<string, string>>()
), Times.Never);
}

Expand All @@ -80,8 +80,8 @@ public async Task MakeReport_ShouldBail_WhenApiKeyIsMissing()
.Verify(s => s.PostAsync(
It.IsAny<string>(),
It.IsAny<SendGridSend>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<CancellationToken>()
It.IsAny<CancellationToken>(),
It.IsAny<IDictionary<string, string>>()
), Times.Never);
}

Expand All @@ -97,8 +97,8 @@ public async Task MakeReport_ShouldBail_WhenFromAddressIsMissing()
.Verify(s => s.PostAsync(
It.IsAny<string>(),
It.IsAny<SendGridSend>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<CancellationToken>()
It.IsAny<CancellationToken>(),
It.IsAny<IDictionary<string, string>>()
), Times.Never);
}

Expand All @@ -114,8 +114,8 @@ public async Task MakeReport_ShouldBail_WhenToAddressesIsMissing()
.Verify(s => s.PostAsync(
It.IsAny<string>(),
It.IsAny<SendGridSend>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<CancellationToken>()
It.IsAny<CancellationToken>(),
It.IsAny<IDictionary<string, string>>()
), Times.Never);
}

Expand All @@ -129,8 +129,8 @@ public async Task MakeReport_ShouldBail_WhenReportIsEmpty()
.Verify(s => s.PostAsync(
It.IsAny<string>(),
It.IsAny<SendGridSend>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<CancellationToken>()
It.IsAny<CancellationToken>(),
It.IsAny<IDictionary<string, string>>()
), Times.Never);
}

Expand All @@ -141,10 +141,10 @@ public async Task MakeReport_ShouldMakeRequest()
.Setup(h => h.PostAsync(
It.IsAny<string>(),
It.IsAny<SendGridSend>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<CancellationToken>()
It.IsAny<CancellationToken>(),
It.IsAny<IDictionary<string, string>>()
))
.Callback((string u, SendGridSend b, IDictionary<string, string> h, CancellationToken _) =>
.Callback((string u, SendGridSend b, CancellationToken _, IDictionary<string, string> h) =>
{
u.Should().Be("https://api.sendgrid.com/v3/mail/send");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ public void Setup()
ResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent);
MockHttpService = new Mock<IHttpService>();
MockHttpService
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<DiscordExecute>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((string _, DiscordExecute _, CancellationToken _) => ResponseMessage)
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<DiscordExecute>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string>>()))
.ReturnsAsync((string _, DiscordExecute _, CancellationToken _, IDictionary<string, string> _) => ResponseMessage)
.Verifiable();

MockRenderService = new Mock<IRenderService>();
Expand Down Expand Up @@ -96,15 +96,15 @@ public async Task SendReport_ShouldPostReport()
{
await PublisherUnderTest.SendReport(WebHook, FakeReport, CancellationToken.None);

MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.Is<DiscordExecute>(e => e.Content.Contains("example.com")), CancellationToken.None), Times.Once);
MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.Is<DiscordExecute>(e => e.Content.Contains("example.com")), CancellationToken.None, It.IsAny<IDictionary<string, string>>()), Times.Once);
}

[Test]
public async Task SendReport_ShouldDisableMentions()
{
await PublisherUnderTest.SendReport(WebHook, FakeReport, CancellationToken.None);

MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.Is<DiscordExecute>(e => e.AllowedMentions.Parse != null), CancellationToken.None), Times.Once);
MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.Is<DiscordExecute>(e => e.AllowedMentions.Parse != null), CancellationToken.None, It.IsAny<IDictionary<string, string>>()), Times.Once);

}

Expand All @@ -113,7 +113,7 @@ public async Task SendReport_ShouldDisablePreviews()
{
await PublisherUnderTest.SendReport(WebHook, FakeReport, CancellationToken.None);

MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.Is<DiscordExecute>(e => e.Flags == MessageFlags.SuppressEmbeds), CancellationToken.None), Times.Once);
MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.Is<DiscordExecute>(e => e.Flags == MessageFlags.SuppressEmbeds), CancellationToken.None, It.IsAny<IDictionary<string, string>>()), Times.Once);
}

[Test]
Expand All @@ -123,7 +123,7 @@ public async Task SendReport_ShouldSendAllPages()

await PublisherUnderTest.SendReport(WebHook, FakeReport, CancellationToken.None);

MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.IsAny<DiscordExecute>(), CancellationToken.None), Times.AtLeast(2));
MockHttpService.Verify(h => h.PostAsync(WebHook.Url, It.IsAny<DiscordExecute>(), CancellationToken.None, It.IsAny<IDictionary<string, string>>()), Times.AtLeast(2));
}

[TestCase("2", "0", null)]
Expand Down
67 changes: 67 additions & 0 deletions ModShark.Tests/Services/HttpServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Net;
using ModShark.Services;
using Moq;
using Moq.Protected;

namespace ModShark.Tests.Services;

public class HttpServiceTests
{
private HttpResponseMessage FakeResponse { get; set; } = null!;

private HttpService ServiceUnderTest { get; set; } = null!;

private HttpClient HttpClient { get; set; } = null!;
private Mock<HttpMessageHandler> MockHttpMessageHandler { get; set; } = null!;

[SetUp]
public void Setup()
{
FakeResponse = new HttpResponseMessage(HttpStatusCode.OK);

MockHttpMessageHandler = new Mock<HttpMessageHandler>();
MockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync((HttpRequestMessage _, CancellationToken _) => FakeResponse)
.Verifiable();

HttpClient = new HttpClient(MockHttpMessageHandler.Object);
ServiceUnderTest = new HttpService(HttpClient);
}

[TearDown]
public void Teardown()
{
FakeResponse.Dispose();
HttpClient.Dispose();
}

[Test]
public async Task PostAsync_ShouldAttachDefaultUserAgent_WhenNotSet()
{
const string expected = "ModShark (https://github.com/warriordog/ModShark)";

await ServiceUnderTest.PostAsync("https://example.com", CancellationToken.None);

MockHttpMessageHandler
.Protected()
.Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.Is<HttpRequestMessage>(m => string.Join(" ", m.Headers.GetValues("User-Agent")) == expected), ItExpr.IsAny<CancellationToken>());
}

[Test]
public async Task PostAsync_ShouldNotAttachUserAgent_WhenAlreadySet()
{
const string expected = "Custom Agent";
var headers = new Dictionary<string, string>
{
["User-Agent"] = expected
};

await ServiceUnderTest.PostAsync("https://example.com", CancellationToken.None, headers: headers);

MockHttpMessageHandler
.Protected()
.Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.Is<HttpRequestMessage>(m => string.Join(" ", m.Headers.GetValues("User-Agent")) == expected), ItExpr.IsAny<CancellationToken>());
}
}
2 changes: 1 addition & 1 deletion ModShark/Reports/Reporter/SendGridReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private async Task SendEmail(SendGridSend send, CancellationToken stoppingToken)
["Authorization"] = $"Bearer {reporterConfig.ApiKey}"
};

var response = await httpService.PostAsync("https://api.sendgrid.com/v3/mail/send", send, headers, stoppingToken);
var response = await httpService.PostAsync("https://api.sendgrid.com/v3/mail/send", send, stoppingToken, headers: headers);

if (response.StatusCode != HttpStatusCode.Accepted)
{
Expand Down
22 changes: 10 additions & 12 deletions ModShark/Services/HttpService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,16 @@ namespace ModShark.Services;

public interface IHttpService
{
Task<HttpResponseMessage> PostAsync(string url, CancellationToken stoppingToken);
Task<HttpResponseMessage> PostAsync(string url, IDictionary<string, string> headers, CancellationToken stoppingToken);
Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, CancellationToken stoppingToken);
Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, IDictionary<string, string> headers, CancellationToken stoppingToken);
Task<HttpResponseMessage> PostAsync(string url, CancellationToken stoppingToken, IDictionary<string, string>? headers = null);
Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, CancellationToken stoppingToken, IDictionary<string, string>? headers = null);
}

public class HttpService(HttpClient client) : IHttpService
{
public async Task<HttpResponseMessage> PostAsync(string url, CancellationToken stoppingToken)
=> await PostAsync<object?>(url, null, false, null, stoppingToken);

public async Task<HttpResponseMessage> PostAsync(string url, IDictionary<string, string> headers, CancellationToken stoppingToken)
public async Task<HttpResponseMessage> PostAsync(string url, CancellationToken stoppingToken, IDictionary<string, string>? headers = null)
=> await PostAsync<object?>(url, null, false, headers, stoppingToken);

public async Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, CancellationToken stoppingToken)
=> await PostAsync(url, body, true, null, stoppingToken);

public async Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, IDictionary<string, string> headers, CancellationToken stoppingToken)
public async Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, CancellationToken stoppingToken, IDictionary<string, string>? headers = null)
=> await PostAsync(url, body, true, headers, stoppingToken);

private async Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body, bool hasBody, IDictionary<string, string>? headers, CancellationToken stoppingToken)
Expand All @@ -37,6 +29,12 @@ private async Task<HttpResponseMessage> PostAsync<TBody>(string url, TBody body,
}
}

// Set default user-agent
if (!message.Headers.Contains("User-Agent"))
{
message.Headers.Add("User-Agent", "ModShark (https://github.com/warriordog/ModShark)");
}

// Attach body
if (hasBody)
{
Expand Down

0 comments on commit 2c3b3eb

Please sign in to comment.