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+ }
0 commit comments