22using System . Collections . Immutable ;
33using System . IO ;
44using System . Security . Cryptography ;
5+ using System . Threading ;
56using System . Threading . Tasks ;
67using FluentAssertions ;
78using Light . TemporaryStreams . Hashing ;
@@ -17,10 +18,8 @@ public static class CopyToTemporaryStreamTests
1718 public static async Task CopyToTemporaryStreamAsync_ShouldCreateMemoryStream_WhenSourceIsSmall ( int bufferSize )
1819 {
1920 // Arrange
21+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( bufferSize ) ;
2022 var cancellationToken = TestContext . Current . CancellationToken ;
21- var service = CreateDefaultService ( ) ;
22- var sourceData = CreateTestData ( bufferSize ) ;
23- using var sourceStream = new MemoryStream ( sourceData ) ;
2423
2524 // Act
2625 await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
@@ -29,13 +28,12 @@ public static async Task CopyToTemporaryStreamAsync_ShouldCreateMemoryStream_Whe
2928 ) ;
3029
3130 // Assert
32- temporaryStream . Should ( ) . NotBeNull ( ) ;
33- temporaryStream . IsFileBased . Should ( ) . BeFalse ( ) ;
34- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
35- temporaryStream . Position = 0 ;
36- var copiedData = new byte [ sourceData . Length ] ;
37- await temporaryStream . ReadExactlyAsync ( copiedData , cancellationToken ) ;
38- copiedData . Should ( ) . Equal ( sourceData ) ;
31+ await AssertTemporaryStreamContentsMatchAsync (
32+ temporaryStream ,
33+ sourceData ,
34+ expectFileBased : false ,
35+ cancellationToken
36+ ) ;
3937 }
4038
4139 [ Theory ]
@@ -44,10 +42,8 @@ public static async Task CopyToTemporaryStreamAsync_ShouldCreateMemoryStream_Whe
4442 public static async Task CopyToTemporaryStreamAsync_ShouldCreateFileStream_WhenSourceIsLarge ( int bufferSize )
4543 {
4644 // Arrange
45+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( bufferSize ) ;
4746 var cancellationToken = TestContext . Current . CancellationToken ;
48- var service = CreateDefaultService ( ) ;
49- var sourceData = CreateTestData ( bufferSize ) ; // More than 80KB threshold
50- using var sourceStream = new MemoryStream ( sourceData ) ;
5147
5248 // Act
5349 await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
@@ -56,23 +52,20 @@ public static async Task CopyToTemporaryStreamAsync_ShouldCreateFileStream_WhenS
5652 ) ;
5753
5854 // Assert
59- temporaryStream . Should ( ) . NotBeNull ( ) ;
60- temporaryStream . IsFileBased . Should ( ) . BeTrue ( ) ;
61- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
62- temporaryStream . Position = 0 ;
63- var copiedData = new byte [ sourceData . Length ] ;
64- await temporaryStream . ReadExactlyAsync ( copiedData , cancellationToken ) ;
65- copiedData . Should ( ) . Equal ( sourceData ) ;
55+ await AssertTemporaryStreamContentsMatchAsync (
56+ temporaryStream ,
57+ sourceData ,
58+ expectFileBased : true ,
59+ cancellationToken
60+ ) ;
6661 }
6762
6863 [ Fact ]
6964 public static async Task CopyToTemporaryStreamAsync_ShouldUseCopyBufferSize_WhenSpecified ( )
7065 {
71- var cancellationToken = TestContext . Current . CancellationToken ;
7266 // Arrange
73- var service = CreateDefaultService ( ) ;
74- var sourceData = CreateTestData ( 100_000 ) ;
75- using var sourceStream = new MemoryStream ( sourceData ) ;
67+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 100_000 ) ;
68+ var cancellationToken = TestContext . Current . CancellationToken ;
7669 var filePath = Path . GetFullPath ( "test.txt" ) ;
7770
7871 // Act
@@ -83,21 +76,20 @@ public static async Task CopyToTemporaryStreamAsync_ShouldUseCopyBufferSize_When
8376 ) ;
8477
8578 // Assert
86- temporaryStream . Should ( ) . NotBeNull ( ) ;
87- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
88- temporaryStream . IsFileBased . Should ( ) . BeTrue ( ) ;
89- temporaryStream . GetUnderlyingFilePath ( ) . Should ( ) . Be ( filePath ) ;
79+ await AssertTemporaryStreamContentsMatchAsync (
80+ temporaryStream ,
81+ sourceData ,
82+ expectFileBased : true ,
83+ cancellationToken
84+ ) ;
9085 }
9186
9287 [ Fact ]
9388 public static async Task CopyToTemporaryStreamAsync_ShouldUseCustomOptions_WhenProvided ( )
9489 {
9590 // Arrange
91+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 50_000 ) ; // Would normally use MemoryStream
9692 var cancellationToken = TestContext . Current . CancellationToken ;
97- var service = CreateDefaultService ( ) ;
98- var sourceData = CreateTestData ( 50_000 ) ; // Would normally use MemoryStream
99- using var sourceStream = new MemoryStream ( sourceData ) ;
100-
10193 var customOptions = new TemporaryStreamServiceOptions
10294 {
10395 FileThresholdInBytes = 30_000 // Force FileStream usage
@@ -111,19 +103,20 @@ public static async Task CopyToTemporaryStreamAsync_ShouldUseCustomOptions_WhenP
111103 ) ;
112104
113105 // Assert
114- temporaryStream . Should ( ) . NotBeNull ( ) ;
115- temporaryStream . IsFileBased . Should ( ) . BeTrue ( ) ; // Should use file due to custom threshold
116- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
106+ await AssertTemporaryStreamContentsMatchAsync (
107+ temporaryStream ,
108+ sourceData ,
109+ expectFileBased : true ,
110+ cancellationToken
111+ ) ;
117112 }
118113
119114 [ Fact ]
120115 public static async Task CopyToTemporaryStreamAsync_ShouldForwardFilePath ( )
121116 {
122117 // Arrange
118+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 100_000 ) ;
123119 var cancellationToken = TestContext . Current . CancellationToken ;
124- var service = CreateDefaultService ( ) ;
125- var sourceData = CreateTestData ( 100_000 ) ;
126- using var sourceStream = new MemoryStream ( sourceData ) ;
127120
128121 // Act
129122 await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
@@ -133,13 +126,12 @@ public static async Task CopyToTemporaryStreamAsync_ShouldForwardFilePath()
133126 ) ;
134127
135128 // Assert
136- temporaryStream . Should ( ) . NotBeNull ( ) ;
137- temporaryStream . IsFileBased . Should ( ) . BeTrue ( ) ;
138- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
139- temporaryStream . Position = 0 ;
140- var copiedData = new byte [ sourceData . Length ] ;
141- await temporaryStream . ReadExactlyAsync ( copiedData , cancellationToken ) ;
142- copiedData . Should ( ) . Equal ( sourceData ) ;
129+ await AssertTemporaryStreamContentsMatchAsync (
130+ temporaryStream ,
131+ sourceData ,
132+ expectFileBased : true ,
133+ cancellationToken
134+ ) ;
143135 }
144136
145137 [ Theory ]
@@ -150,12 +142,12 @@ HashConversionMethod hashConversionMethod
150142 )
151143 {
152144 // Arrange
145+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 40_000 ) ; // Less than 80KB threshold
153146 var cancellationToken = TestContext . Current . CancellationToken ;
154- var service = CreateDefaultService ( ) ;
155- var sourceData = CreateTestData ( 40_000 ) ; // Less than 80KB threshold
156- using var sourceStream = new MemoryStream ( sourceData ) ;
157- await using var hashingPlugin =
158- new HashingPlugin ( [ new CopyToHashCalculator ( SHA1 . Create ( ) , hashConversionMethod ) ] ) ;
147+ var ( hashingPlugin , expectedHash ) = CreateHashingPluginWithExpectedHash (
148+ sourceData ,
149+ new CopyToHashCalculator ( SHA1 . Create ( ) , hashConversionMethod )
150+ ) ;
159151
160152 // Act
161153 await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
@@ -165,14 +157,12 @@ HashConversionMethod hashConversionMethod
165157 ) ;
166158
167159 // Assert
168- temporaryStream . Should ( ) . NotBeNull ( ) ;
169- temporaryStream . IsFileBased . Should ( ) . BeFalse ( ) ;
170- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
171- temporaryStream . Position = 0 ;
172- var expectedHash = HashConverter . ConvertHashToString ( SHA1 . HashData ( sourceData ) , hashConversionMethod ) ;
173- var copiedData = new byte [ sourceData . Length ] ;
174- await temporaryStream . ReadExactlyAsync ( copiedData , cancellationToken ) ;
175- copiedData . Should ( ) . Equal ( sourceData ) ;
160+ await AssertTemporaryStreamContentsMatchAsync (
161+ temporaryStream ,
162+ sourceData ,
163+ expectFileBased : false ,
164+ cancellationToken
165+ ) ;
176166 hashingPlugin . GetHash ( nameof ( SHA1 ) ) . Should ( ) . Be ( expectedHash ) ;
177167 }
178168
@@ -184,12 +174,12 @@ HashConversionMethod hashConversionMethod
184174 )
185175 {
186176 // Arrange
177+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 100_000 ) ; // More than 80KB threshold
187178 var cancellationToken = TestContext . Current . CancellationToken ;
188- var service = CreateDefaultService ( ) ;
189- var sourceData = CreateTestData ( 100_000 ) ; // More than 80KB threshold
190- using var sourceStream = new MemoryStream ( sourceData ) ;
191- await using var hashingPlugin =
192- new HashingPlugin ( [ new CopyToHashCalculator ( MD5 . Create ( ) , hashConversionMethod ) ] ) ;
179+ var ( hashingPlugin , expectedHash ) = CreateHashingPluginWithExpectedHash (
180+ sourceData ,
181+ new CopyToHashCalculator ( MD5 . Create ( ) , hashConversionMethod )
182+ ) ;
193183
194184 // Act
195185 await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
@@ -199,43 +189,25 @@ HashConversionMethod hashConversionMethod
199189 ) ;
200190
201191 // Assert
202- temporaryStream . Should ( ) . NotBeNull ( ) ;
203- temporaryStream . IsFileBased . Should ( ) . BeTrue ( ) ;
204- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
205- temporaryStream . Position = 0 ;
206- var copiedData = new byte [ sourceData . Length ] ;
207- await temporaryStream . ReadExactlyAsync ( copiedData , cancellationToken ) ;
208- copiedData . Should ( ) . Equal ( sourceData ) ;
209- var expectedHash = HashConverter . ConvertHashToString ( MD5 . HashData ( sourceData ) , hashConversionMethod ) ;
192+ await AssertTemporaryStreamContentsMatchAsync (
193+ temporaryStream ,
194+ sourceData ,
195+ expectFileBased : true ,
196+ cancellationToken
197+ ) ;
210198 hashingPlugin . GetHash ( nameof ( MD5 ) ) . Should ( ) . Be ( expectedHash ) ;
211199 }
212200
213201 [ Fact ]
214202 public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldSupportMultipleHashAlgorithms ( )
215203 {
216204 // Arrange
205+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 50_000 ) ;
217206 var cancellationToken = TestContext . Current . CancellationToken ;
218- var service = CreateDefaultService ( ) ;
219- var sourceData = CreateTestData ( 50_000 ) ;
220- using var sourceStream = new MemoryStream ( sourceData ) ;
221-
222- await using var hashingPlugin = new HashingPlugin (
223- [
224- new CopyToHashCalculator ( SHA1 . Create ( ) , HashConversionMethod . UpperHexadecimal ) ,
225- new CopyToHashCalculator ( SHA256 . Create ( ) , HashConversionMethod . Base64 )
226- ]
227- ) ;
228207
229- // Act
230- await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
231- sourceStream ,
232- [ hashingPlugin ] ,
233- cancellationToken : cancellationToken
234- ) ;
208+ var sha1Calculator = new CopyToHashCalculator ( SHA1 . Create ( ) , HashConversionMethod . UpperHexadecimal ) ;
209+ var sha256Calculator = new CopyToHashCalculator ( SHA256 . Create ( ) , HashConversionMethod . Base64 ) ;
235210
236- // Assert
237- temporaryStream . Should ( ) . NotBeNull ( ) ;
238- temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
239211 var expectedSha1Hash = HashConverter . ConvertHashToString (
240212 SHA1 . HashData ( sourceData ) ,
241213 HashConversionMethod . UpperHexadecimal
@@ -244,6 +216,17 @@ public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldSupp
244216 SHA256 . HashData ( sourceData ) ,
245217 HashConversionMethod . Base64
246218 ) ;
219+
220+ await using var hashingPlugin = new HashingPlugin ( [ sha1Calculator , sha256Calculator ] ) ;
221+
222+ // Act
223+ await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
224+ sourceStream ,
225+ [ hashingPlugin ] ,
226+ cancellationToken : cancellationToken
227+ ) ;
228+
229+ // Assert
247230 hashingPlugin . GetHash ( nameof ( SHA1 ) ) . Should ( ) . Be ( expectedSha1Hash ) ;
248231 hashingPlugin . GetHash ( nameof ( SHA256 ) ) . Should ( ) . Be ( expectedSha256Hash ) ;
249232 }
@@ -252,11 +235,12 @@ public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldSupp
252235 public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldUseCustomBufferSize ( )
253236 {
254237 // Arrange
238+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 60_000 ) ;
255239 var cancellationToken = TestContext . Current . CancellationToken ;
256- var service = CreateDefaultService ( ) ;
257- var sourceData = CreateTestData ( 60_000 ) ;
258- using var sourceStream = new MemoryStream ( sourceData ) ;
259- await using var hashingPlugin = new HashingPlugin ( [ SHA1 . Create ( ) ] ) ;
240+ var ( hashingPlugin , expectedHash ) = CreateHashingPluginWithExpectedHash (
241+ sourceData ,
242+ new CopyToHashCalculator ( SHA1 . Create ( ) , HashConversionMethod . Base64 )
243+ ) ;
260244
261245 // Act
262246 await using var temporaryStream = await service . CopyToTemporaryStreamAsync (
@@ -269,19 +253,19 @@ public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldUseC
269253 // Assert
270254 temporaryStream . Should ( ) . NotBeNull ( ) ;
271255 temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
272- var expectedHash = HashConverter . ConvertHashToString ( SHA1 . HashData ( sourceData ) , HashConversionMethod . Base64 ) ;
273256 hashingPlugin . GetHash ( nameof ( SHA1 ) ) . Should ( ) . Be ( expectedHash ) ;
274257 }
275258
276259 [ Fact ]
277260 public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldForwardFilePath ( )
278261 {
279262 // Arrange
263+ var ( service , sourceData , sourceStream ) = CreateTestSetup ( 100_000 ) ;
280264 var cancellationToken = TestContext . Current . CancellationToken ;
281- var service = CreateDefaultService ( ) ;
282- var sourceData = CreateTestData ( 100_000 ) ;
283- using var sourceStream = new MemoryStream ( sourceData ) ;
284- await using var hashingPlugin = new HashingPlugin ( [ SHA1 . Create ( ) ] ) ;
265+ var ( hashingPlugin , expectedHash ) = CreateHashingPluginWithExpectedHash (
266+ sourceData ,
267+ new CopyToHashCalculator ( SHA1 . Create ( ) , HashConversionMethod . Base64 )
268+ ) ;
285269 var filePath = Path . GetFullPath ( "test.txt" ) ;
286270
287271 // Act
@@ -297,10 +281,52 @@ public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldForw
297281 temporaryStream . IsFileBased . Should ( ) . BeTrue ( ) ;
298282 temporaryStream . Length . Should ( ) . Be ( sourceData . Length ) ;
299283 temporaryStream . GetUnderlyingFilePath ( ) . Should ( ) . Be ( filePath ) ;
300- var expectedHash = HashConverter . ConvertHashToString ( SHA1 . HashData ( sourceData ) , HashConversionMethod . Base64 ) ;
301284 hashingPlugin . GetHash ( nameof ( SHA1 ) ) . Should ( ) . Be ( expectedHash ) ;
302285 }
303286
287+ private static ( TemporaryStreamService service , byte [ ] sourceData , MemoryStream sourceStream ) CreateTestSetup (
288+ int dataSize
289+ )
290+ {
291+ var service = CreateDefaultService ( ) ;
292+ var sourceData = CreateTestData ( dataSize ) ;
293+ var sourceStream = new MemoryStream ( sourceData ) ;
294+
295+ return ( service , sourceData , sourceStream ) ;
296+ }
297+
298+ private static async Task AssertTemporaryStreamContentsMatchAsync (
299+ TemporaryStream temporaryStream ,
300+ byte [ ] expectedData ,
301+ bool expectFileBased ,
302+ CancellationToken cancellationToken
303+ )
304+ {
305+ temporaryStream . Should ( ) . NotBeNull ( ) ;
306+ temporaryStream . IsFileBased . Should ( ) . Be ( expectFileBased ) ;
307+ temporaryStream . Length . Should ( ) . Be ( expectedData . Length ) ;
308+
309+ temporaryStream . Position = 0 ;
310+ var copiedData = new byte [ expectedData . Length ] ;
311+ await temporaryStream . ReadExactlyAsync ( copiedData , cancellationToken ) ;
312+ copiedData . Should ( ) . Equal ( expectedData ) ;
313+ }
314+
315+ private static ( HashingPlugin plugin , string expectedHash ) CreateHashingPluginWithExpectedHash (
316+ byte [ ] sourceData ,
317+ CopyToHashCalculator hashCalculator
318+ )
319+ {
320+ var plugin = new HashingPlugin ( [ hashCalculator ] ) ;
321+
322+ var expectedHash = HashConverter . ConvertHashToString (
323+ hashCalculator . HashAlgorithm . ComputeHash ( sourceData ) ,
324+ hashCalculator . ConversionMethod
325+ ) ;
326+
327+ return ( plugin , expectedHash ) ;
328+ }
329+
304330 private static TemporaryStreamService CreateDefaultService ( ) =>
305331 new (
306332 new TemporaryStreamServiceOptions ( ) ,
0 commit comments