Skip to content

Commit e67d3bc

Browse files
committed
add FetchService to access remote and local media files
1 parent 6354195 commit e67d3bc

File tree

13 files changed

+330
-23
lines changed

13 files changed

+330
-23
lines changed

ModShark.Tests/Reports/Reporter/SendGridReporterTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void Setup()
3030
MockLogger = new Mock<ILogger<SendGridReporter>>();
3131
MockHttpService = new Mock<IHttpService>();
3232
MockHttpService
33-
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<SendGridSend>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string>>()))
33+
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<SendGridSend>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()))
3434
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.Accepted));
3535
MockRenderService = new Mock<IRenderService>();
3636
MockRenderService
@@ -63,7 +63,7 @@ public async Task MakeReport_ShouldBail_WhenDisabled()
6363
It.IsAny<string>(),
6464
It.IsAny<SendGridSend>(),
6565
It.IsAny<CancellationToken>(),
66-
It.IsAny<IDictionary<string, string>>()
66+
It.IsAny<IDictionary<string, string?>?>()
6767
), Times.Never);
6868
}
6969

@@ -80,7 +80,7 @@ public async Task MakeReport_ShouldBail_WhenApiKeyIsMissing()
8080
It.IsAny<string>(),
8181
It.IsAny<SendGridSend>(),
8282
It.IsAny<CancellationToken>(),
83-
It.IsAny<IDictionary<string, string>>()
83+
It.IsAny<IDictionary<string, string?>?>()
8484
), Times.Never);
8585
}
8686

@@ -97,7 +97,7 @@ public async Task MakeReport_ShouldBail_WhenFromAddressIsMissing()
9797
It.IsAny<string>(),
9898
It.IsAny<SendGridSend>(),
9999
It.IsAny<CancellationToken>(),
100-
It.IsAny<IDictionary<string, string>>()
100+
It.IsAny<IDictionary<string, string?>?>()
101101
), Times.Never);
102102
}
103103

@@ -114,7 +114,7 @@ public async Task MakeReport_ShouldBail_WhenToAddressesIsMissing()
114114
It.IsAny<string>(),
115115
It.IsAny<SendGridSend>(),
116116
It.IsAny<CancellationToken>(),
117-
It.IsAny<IDictionary<string, string>>()
117+
It.IsAny<IDictionary<string, string?>?>()
118118
), Times.Never);
119119
}
120120

@@ -129,7 +129,7 @@ public async Task MakeReport_ShouldBail_WhenReportIsEmpty()
129129
It.IsAny<string>(),
130130
It.IsAny<SendGridSend>(),
131131
It.IsAny<CancellationToken>(),
132-
It.IsAny<IDictionary<string, string>>()
132+
It.IsAny<IDictionary<string, string?>?>()
133133
), Times.Never);
134134
}
135135

@@ -141,7 +141,7 @@ public async Task MakeReport_ShouldMakeRequest()
141141
It.IsAny<string>(),
142142
It.IsAny<SendGridSend>(),
143143
It.IsAny<CancellationToken>(),
144-
It.IsAny<IDictionary<string, string>>()
144+
It.IsAny<IDictionary<string, string?>?>()
145145
))
146146
.Callback((string u, SendGridSend b, CancellationToken _, IDictionary<string, string> h) =>
147147
{

ModShark.Tests/Reports/Reporter/WebHooks/DiscordPublisherTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void Setup()
5656
ResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent);
5757
MockHttpService = new Mock<IHttpService>();
5858
MockHttpService
59-
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<DiscordExecute>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string>>()))
59+
.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<DiscordExecute>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()))
6060
.ReturnsAsync((string _, DiscordExecute _, CancellationToken _, IDictionary<string, string> _) => ResponseMessage)
6161
.Verifiable();
6262

@@ -96,15 +96,15 @@ public async Task SendReport_ShouldPostReport()
9696
{
9797
await PublisherUnderTest.SendReport(WebHook, FakeReport, CancellationToken.None);
9898

99-
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);
99+
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);
100100
}
101101

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

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

109109
}
110110

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

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

119119
[Test]
@@ -123,7 +123,7 @@ public async Task SendReport_ShouldSendAllPages()
123123

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

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

129129
[TestCase("2", "0", null)]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System.Net;
2+
using FluentAssertions;
3+
using ModShark.Services;
4+
using ModShark.Tests._Utils;
5+
using Moq;
6+
7+
namespace ModShark.Tests.Services;
8+
9+
public class FetchServiceTests
10+
{
11+
private SharkeyConfig FakeSharkeyConfig { get; set; } = null!;
12+
private Mock<IHttpService> MockHttpService { get; set; } = null!;
13+
private Mock<IFileService> MockFileService { get; set; } = null!;
14+
private FetchService ServiceUnderTest { get; set; } = null!;
15+
16+
[SetUp]
17+
public void Setup()
18+
{
19+
MockHttpService = new Mock<IHttpService>();
20+
MockFileService = new Mock<IFileService>();
21+
FakeSharkeyConfig = ConfigFakes.MakeSharkeyConfig();
22+
ServiceUnderTest = new FetchService(FakeSharkeyConfig, MockHttpService.Object, MockFileService.Object);
23+
}
24+
25+
[Test]
26+
public void FetchUrl_ShouldRejectInvalidUrl()
27+
{
28+
Assert.ThrowsAsync<ArgumentException>(async () =>
29+
{
30+
await ServiceUnderTest.FetchUrl("http:://example.com/bad-url", CancellationToken.None);
31+
});
32+
}
33+
34+
[TestCase("file")]
35+
[TestCase("ftp")]
36+
[TestCase("ssh")]
37+
public void FetchUrl_ShouldRejectInvalidSchemes(string scheme)
38+
{
39+
var testUrl = $"{scheme}://example.com/file";
40+
41+
Assert.ThrowsAsync<InvalidOperationException>(async () =>
42+
{
43+
await ServiceUnderTest.FetchUrl(testUrl, CancellationToken.None);
44+
});
45+
}
46+
47+
[TestCase("http")]
48+
[TestCase("HTTP")]
49+
[TestCase("https")]
50+
[TestCase("HTTPS")]
51+
public async Task FetchUrl_ShouldDownloadFile_WhenSchemeIsHttp(string scheme)
52+
{
53+
var testUrl = $"{scheme}://example.com/url";
54+
var expectedBytes = new byte[] { 1, 2, 3 };
55+
56+
MockHttpService
57+
.Setup(h => h.GetAsync(testUrl, It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()))
58+
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.Accepted)
59+
{
60+
Content = new ByteArrayContent(expectedBytes)
61+
});
62+
63+
await using var stream = await ServiceUnderTest.FetchUrl(testUrl, CancellationToken.None);
64+
65+
var buffer = new byte[3];
66+
var read = await stream.ReadAsync(buffer, CancellationToken.None);
67+
read.Should().Be(3);
68+
buffer.Should().BeEquivalentTo(expectedBytes);
69+
}
70+
71+
[Test]
72+
public async Task FetchUrl_ShouldDownloadFile_WithCustomUserAgent()
73+
{
74+
const string testUrl = "https://example.com/url";
75+
76+
IDictionary<string, string?>? actualHeaders = null;
77+
MockHttpService
78+
.Setup(h => h.GetAsync(testUrl, It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()))
79+
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.Accepted)
80+
{
81+
Content = new ByteArrayContent([])
82+
})
83+
.Callback((string _, CancellationToken _, IDictionary<string, string?>? headers) =>
84+
{
85+
actualHeaders = headers;
86+
});
87+
88+
await ServiceUnderTest.FetchUrl(testUrl, CancellationToken.None);
89+
90+
actualHeaders.Should().Contain("User-Agent", null);
91+
}
92+
93+
[Test]
94+
public void FetchUrl_ShouldReject_WhenFetchFails()
95+
{
96+
const string testUrl = "https://example.com/url";
97+
98+
MockHttpService
99+
.Setup(h => h.GetAsync(testUrl, It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()))
100+
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError));
101+
102+
Assert.ThrowsAsync<HttpRequestException>(async () =>
103+
{
104+
await ServiceUnderTest.FetchUrl(testUrl, CancellationToken.None);
105+
});
106+
}
107+
108+
[TestCase("123-456")]
109+
[TestCase("123-456.png")]
110+
[TestCase("123-456.tar.gz")]
111+
public async Task FetchUrl_ShouldReadFile_WhenUrlIsLocal(string suffix)
112+
{
113+
var testUrl = $"https://example.com/files/{suffix}";
114+
var expectedPath = Path.Join("/home/sharkey/files", suffix);
115+
var expectedStream = new MemoryStream([]);
116+
MockFileService
117+
.Setup(s => s.OpenRead(expectedPath))
118+
.Returns(expectedStream);
119+
120+
await using var actualStream = await ServiceUnderTest.FetchUrl(testUrl, CancellationToken.None);
121+
122+
actualStream.Should().BeSameAs(expectedStream);
123+
}
124+
125+
[TestCase("https://example.com/files/abc-123", true)]
126+
[TestCase("https://example.com/files/abc-123.png", true)]
127+
[TestCase("https://example.com/files/abc-123.", false)]
128+
[TestCase("https://example.com/files/abc-123.a/", false)]
129+
[TestCase("https://example.com/files/abc-123/bad", false)]
130+
[TestCase("https://example.com/bad/abc-123", false)]
131+
[TestCase("https://other.com/files/abc-123", false)]
132+
[TestCase("https://bad.example.com/files/abc-123", false)]
133+
public async Task FetchUrl_ShouldReadFile_OnlyWhenUrlIsCorrect(string testUrl, bool expectLocal)
134+
{
135+
MockHttpService
136+
.Setup(h => h.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()))
137+
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.Accepted)
138+
{
139+
Content = new ByteArrayContent([])
140+
});
141+
MockFileService
142+
.Setup(s => s.OpenRead(It.IsAny<string>()))
143+
.Returns(new MemoryStream([]));
144+
145+
await ServiceUnderTest.FetchUrl(testUrl, CancellationToken.None);
146+
147+
if (expectLocal)
148+
{
149+
MockFileService.Verify(s => s.OpenRead(It.IsAny<string>()), Times.Once);
150+
MockHttpService.Verify(h => h.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()), Times.Never);
151+
}
152+
else
153+
{
154+
MockFileService.Verify(s => s.OpenRead(It.IsAny<string>()), Times.Never);
155+
MockHttpService.Verify(h => h.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>(), It.IsAny<IDictionary<string, string?>?>()), Times.Once);
156+
}
157+
}
158+
}

ModShark.Tests/Services/HttpServiceTests.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,34 @@ public void Teardown()
3737
HttpClient.Dispose();
3838
}
3939

40+
[Test]
41+
public async Task GetAsync_ShouldAttachDefaultUserAgent_WhenNotSet()
42+
{
43+
const string expected = "ModShark (https://github.com/warriordog/ModShark)";
44+
45+
await ServiceUnderTest.GetAsync("https://example.com", CancellationToken.None);
46+
47+
MockHttpMessageHandler
48+
.Protected()
49+
.Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.Is<HttpRequestMessage>(m => string.Join(" ", m.Headers.GetValues("User-Agent")) == expected), ItExpr.IsAny<CancellationToken>());
50+
}
51+
52+
[Test]
53+
public async Task GetAsync_ShouldNotAttachUserAgent_WhenAlreadySet()
54+
{
55+
const string expected = "Custom Agent";
56+
var headers = new Dictionary<string, string?>
57+
{
58+
["User-Agent"] = expected
59+
};
60+
61+
await ServiceUnderTest.GetAsync("https://example.com", CancellationToken.None, headers: headers);
62+
63+
MockHttpMessageHandler
64+
.Protected()
65+
.Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.Is<HttpRequestMessage>(m => string.Join(" ", m.Headers.GetValues("User-Agent")) == expected), ItExpr.IsAny<CancellationToken>());
66+
}
67+
4068
[Test]
4169
public async Task PostAsync_ShouldAttachDefaultUserAgent_WhenNotSet()
4270
{
@@ -53,7 +81,7 @@ public async Task PostAsync_ShouldAttachDefaultUserAgent_WhenNotSet()
5381
public async Task PostAsync_ShouldNotAttachUserAgent_WhenAlreadySet()
5482
{
5583
const string expected = "Custom Agent";
56-
var headers = new Dictionary<string, string>
84+
var headers = new Dictionary<string, string?>
5785
{
5886
["User-Agent"] = expected
5987
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ModShark.Tests._Utils;
2+
3+
public static class ConfigFakes
4+
{
5+
public static SharkeyConfig MakeSharkeyConfig() => new()
6+
{
7+
ServiceAccount = "instance.actor",
8+
ApiEndpoint = "http://127.0.0.1:3000",
9+
PublicHost = "example.com",
10+
FilesDirectoryPath = "/home/sharkey/files"
11+
};
12+
}

ModShark/ModShark.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
</ItemGroup>
2626

2727
<ItemGroup>
28+
<ProjectReference Include="..\QR\QR.csproj" />
2829
<ProjectReference Include="..\SharkeyDB\SharkeyDB.csproj" />
2930
</ItemGroup>
3031

ModShark/ModSharkConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class SharkeyConfig
2626
public required string ApiEndpoint { get; set; }
2727
public required string PublicHost { get; set; }
2828
public int MaxNoteLength { get; set; } = 3000;
29+
public string? FilesDirectoryPath { get; set; }
2930
}
3031

3132
[PublicAPI]

ModShark/Reports/Reporter/SendGridReporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private SendGridSend CreateSend(string subject, string message)
107107

108108
private async Task SendEmail(SendGridSend send, CancellationToken stoppingToken)
109109
{
110-
var headers = new Dictionary<string, string>
110+
var headers = new Dictionary<string, string?>
111111
{
112112
["Authorization"] = $"Bearer {reporterConfig.ApiKey}"
113113
};

0 commit comments

Comments
 (0)