11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License.
33
4+ using System ;
45using System . Collections . Generic ;
56using System . Linq ;
7+ using System . Reflection ;
8+ using System . Threading ;
9+ using System . Threading . Tasks ;
10+ using Azure ;
11+ using Azure . Storage . Blobs ;
12+ using Azure . Storage . Blobs . Models ;
13+ using Microsoft . Extensions . Logging ;
14+ using Microsoft . Extensions . Logging . Abstractions ;
15+ using Moq ;
616using NUnit . Framework ;
717
818namespace Microsoft . Azure . WebJobs . Extensions . Storage . Blobs . Listeners
919{
1020 public class BlobLogListenerTests
1121 {
22+ [ TestCase ( "write" , 1 , null , 0 , true , TestName = "HasBlobWritesAsync_WriteLogPresent_ReturnsTrue" ) ]
23+ [ TestCase ( "read" , 1 , null , 0 , false , TestName = "HasBlobWritesAsync_NonWriteLogPresent_ReturnsFalse" ) ]
24+ [ TestCase ( null , 1 , null , 0 , false , TestName = "HasBlobWritesAsync_NoBlobs_ReturnsFalse" ) ]
25+ [ TestCase ( "read" , 100 , "write" , 1 , true , TestName = "HasBlobWritesAsync_WriteLogPresentMultipleLogBlobs_ReturnsTrue" ) ]
26+ [ TestCase ( "delete" , 100 , "read" , 100 , false , TestName = "HasBlobWritesAsync_NonWriteLogPresentMultipleLogBlobs_ReturnsFalse" ) ]
27+ public async Task HasBlobWritesAsync_VariousCases ( string logType1 , int logType1Count , string logType2 , int logType2Count , bool expected )
28+ {
29+ // Arrange
30+ var blobServiceClientMock = new Mock < BlobServiceClient > ( MockBehavior . Strict ) ;
31+ var containerClientMock = new Mock < BlobContainerClient > ( MockBehavior . Strict ) ;
32+
33+ TestAsyncPageable < BlobItem > pageable ;
34+ var blobItems = new List < BlobItem > ( ) ;
35+ if ( logType1 != null )
36+ {
37+ for ( int i = 0 ; i < logType1Count ; i ++ )
38+ {
39+ var blobItem = BlobItemFactory . Create (
40+ name : Guid . NewGuid ( ) . ToString ( ) ,
41+ metadata : new Dictionary < string , string > { { "LogType" , logType1 } } ) ;
42+ blobItems . Add ( blobItem ) ;
43+ }
44+ }
45+ if ( logType2 != null )
46+ {
47+ for ( int i = 0 ; i < logType2Count ; i ++ )
48+ {
49+ var blobItem = BlobItemFactory . Create (
50+ name : Guid . NewGuid ( ) . ToString ( ) ,
51+ metadata : new Dictionary < string , string > { { "LogType" , logType2 } } ) ;
52+ blobItems . Add ( blobItem ) ;
53+ }
54+ }
55+
56+ if ( blobItems . Count == 0 )
57+ {
58+ pageable = new TestAsyncPageable < BlobItem > ( Enumerable . Empty < BlobItem > ( ) ) ;
59+ }
60+ else
61+ {
62+ pageable = new TestAsyncPageable < BlobItem > ( blobItems ) ;
63+ }
64+
65+ containerClientMock
66+ . Setup ( c => c . GetBlobsAsync (
67+ It . IsAny < BlobTraits > ( ) ,
68+ It . IsAny < BlobStates > ( ) ,
69+ It . IsAny < string > ( ) ,
70+ It . IsAny < CancellationToken > ( ) ) )
71+ . Returns ( pageable ) ;
72+
73+ blobServiceClientMock
74+ . Setup ( c => c . GetBlobContainerClient ( "$logs" ) )
75+ . Returns ( containerClientMock . Object ) ;
76+
77+ var listener = new BlobLogListener ( blobServiceClientMock . Object , NullLogger < BlobListener > . Instance ) ;
78+
79+ // Act
80+ bool result = await listener . HasBlobWritesAsync ( CancellationToken . None , hoursWindow : 1 ) ;
81+
82+ // Assert
83+ Assert . AreEqual ( expected , result ) ;
84+ }
85+
1286 [ Test ]
1387 public void GetPathsForValidBlobWrites_Returns_ValidBlobWritesOnly ( )
1488 {
1589 StorageAnalyticsLogEntry [ ] entries = new [ ]
1690 {
17- // This is a valid write entry with a valid path
1891 new StorageAnalyticsLogEntry
1992 {
2093 ServiceType = StorageServiceType . Blob ,
2194 OperationType = StorageServiceOperationType . PutBlob ,
2295 RequestedObjectKey = @"/storagesample/sample-container/""0x8D199A96CB71468""/sample-blob.txt"
2396 } ,
24-
25- // This is an invalid path and will be filtered out
2697 new StorageAnalyticsLogEntry
2798 {
2899 ServiceType = StorageServiceType . Blob ,
29100 OperationType = StorageServiceOperationType . PutBlob ,
30101 RequestedObjectKey = "/"
31102 } ,
32-
33- // This does not constitute a write and will be filtered out
34103 new StorageAnalyticsLogEntry
35104 {
36105 ServiceType = StorageServiceType . Blob ,
@@ -45,5 +114,52 @@ public void GetPathsForValidBlobWrites_Returns_ValidBlobWritesOnly()
45114 Assert . AreEqual ( "sample-container" , singlePath . ContainerName ) ;
46115 Assert . AreEqual ( @"""0x8D199A96CB71468""/sample-blob.txt" , singlePath . BlobName ) ;
47116 }
117+
118+ private static class BlobItemFactory
119+ {
120+ private static readonly Type BlobItemType = typeof ( BlobItem ) ;
121+ private static readonly System . Reflection . PropertyInfo NameProp = BlobItemType . GetProperty ( nameof ( BlobItem . Name ) ) ! ;
122+ private static readonly System . Reflection . PropertyInfo MetadataProp = BlobItemType . GetProperty ( nameof ( BlobItem . Metadata ) ) ! ;
123+
124+ public static BlobItem Create ( string name , IDictionary < string , string > metadata )
125+ {
126+ var ctor = BlobItemType . GetConstructor ( BindingFlags . Instance | BindingFlags . NonPublic , binder : null , types : Type . EmptyTypes , modifiers : null )
127+ ?? throw new InvalidOperationException ( "BlobItem internal constructor not found." ) ;
128+ object raw = ctor . Invoke ( null ) ;
129+
130+ NameProp . SetValue ( raw , name ) ;
131+ var dict = new Dictionary < string , string > ( metadata , StringComparer . OrdinalIgnoreCase ) ;
132+ MetadataProp . SetValue ( raw , dict ) ;
133+
134+ return ( BlobItem ) raw ;
135+ }
136+ }
137+
138+ private sealed class TestAsyncPageable < T > : AsyncPageable < T >
139+ {
140+ private readonly IReadOnlyList < Page < T > > _pages ;
141+
142+ public TestAsyncPageable ( IEnumerable < T > items )
143+ {
144+ var list = items . ToList ( ) ;
145+ var responseMock = new Mock < Response > ( ) ;
146+ responseMock . Setup ( r => r . Status ) . Returns ( 200 ) ;
147+ responseMock . Setup ( r => r . ClientRequestId ) . Returns ( Guid . NewGuid ( ) . ToString ( ) ) ;
148+
149+ _pages = new [ ]
150+ {
151+ Page < T > . FromValues ( list , continuationToken : null , response : responseMock . Object )
152+ } ;
153+ }
154+
155+ public override async IAsyncEnumerable < Page < T > > AsPages ( string continuationToken = null , int ? pageSizeHint = null )
156+ {
157+ foreach ( var p in _pages )
158+ {
159+ yield return p ;
160+ await Task . Yield ( ) ;
161+ }
162+ }
163+ }
48164 }
49- }
165+ }
0 commit comments