22
33using System ;
44using System . IO ;
5+ using System . Runtime . InteropServices ;
56using System . Text ;
67using System . Threading ;
78using System . Threading . Tasks ;
@@ -17,13 +18,14 @@ public class CloudDrivePluginTests
1718 public async Task UploadSmallFileAsyncSucceedsAsync ( )
1819 {
1920 // Arrange
20- string anyFilePath = Guid . NewGuid ( ) . ToString ( ) ;
21+ string allowedDir = Path . GetTempPath ( ) ;
22+ string anyFilePath = Path . Combine ( allowedDir , Guid . NewGuid ( ) . ToString ( ) ) ;
2123
2224 Mock < ICloudDriveConnector > connectorMock = new ( ) ;
2325 connectorMock . Setup ( c => c . UploadSmallFileAsync ( It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
2426 . Returns ( Task . CompletedTask ) ;
2527
26- CloudDrivePlugin target = new ( connectorMock . Object ) ;
28+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ allowedDir ] } ;
2729
2830 // Act
2931 await target . UploadFileAsync ( anyFilePath , Guid . NewGuid ( ) . ToString ( ) ) ;
@@ -74,4 +76,164 @@ public async Task GetFileContentAsyncSucceedsAsync()
7476 Assert . Equal ( expectedContent , actual ) ;
7577 connectorMock . VerifyAll ( ) ;
7678 }
79+
80+ [ Fact ]
81+ public async Task ItDeniesAllPathsByDefaultAsync ( )
82+ {
83+ // Arrange
84+ string filePath = Path . Combine ( Path . GetTempPath ( ) , "somefile.txt" ) ;
85+
86+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
87+ CloudDrivePlugin target = new ( connectorMock . Object ) ;
88+
89+ // Act & Assert — default config denies all paths
90+ await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
91+ await target . UploadFileAsync ( filePath , "/remote.txt" ) ) ;
92+ }
93+
94+ [ Fact ]
95+ public async Task ItDeniesPathTraversalAsync ( )
96+ {
97+ // Arrange
98+ var allowedDir = Path . Combine ( Path . GetTempPath ( ) , "allowed-folder" ) ;
99+ var traversalPath = Path . Combine ( allowedDir , ".." , "outside-folder" , "secret.txt" ) ;
100+
101+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
102+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ allowedDir ] } ;
103+
104+ // Act & Assert — traversal path is canonicalized and rejected
105+ await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
106+ await target . UploadFileAsync ( traversalPath , "/remote.txt" ) ) ;
107+ }
108+
109+ [ Fact ]
110+ public async Task ItDeniesUncPathsAsync ( )
111+ {
112+ // Arrange
113+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
114+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ Path . GetTempPath ( ) ] } ;
115+
116+ // Act & Assert — UNC paths are rejected (ArgumentException on Windows, InvalidOperationException on Linux
117+ // where the path is canonicalized differently and fails the allowlist check instead)
118+ await Assert . ThrowsAnyAsync < Exception > ( async ( ) =>
119+ await target . UploadFileAsync ( "\\ \\ UNC\\ server\\ folder\\ file.txt" , "/remote.txt" ) ) ;
120+ }
121+
122+ [ Fact ]
123+ public async Task ItDeniesDisallowedDirectoriesAsync ( )
124+ {
125+ // Arrange
126+ var allowedDir = Path . Combine ( Path . GetTempPath ( ) , "allowed" ) ;
127+ var disallowedPath = Path . Combine ( Path . GetTempPath ( ) , "disallowed" , "file.txt" ) ;
128+
129+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
130+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ allowedDir ] } ;
131+
132+ // Act & Assert
133+ await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
134+ await target . UploadFileAsync ( disallowedPath , "/remote.txt" ) ) ;
135+ }
136+
137+ [ Fact ]
138+ public async Task ItAllowsSubdirectoriesOfAllowedDirectoriesAsync ( )
139+ {
140+ // Arrange
141+ var allowedDir = Path . GetTempPath ( ) ;
142+ var subDirPath = Path . Combine ( allowedDir , "subdir" , "nested" , "file.txt" ) ;
143+
144+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
145+ connectorMock . Setup ( c => c . UploadSmallFileAsync ( It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
146+ . Returns ( Task . CompletedTask ) ;
147+
148+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ allowedDir ] } ;
149+
150+ // Act — subdirectory of allowed folder should succeed
151+ await target . UploadFileAsync ( subDirPath , "/remote.txt" ) ;
152+
153+ // Assert
154+ connectorMock . VerifyAll ( ) ;
155+ }
156+
157+ [ Fact ]
158+ public async Task ItExpandsEnvironmentVariablesAndValidatesAsync ( )
159+ {
160+ // Arrange — set a dedicated test env var to avoid platform-specific assumptions
161+ var tempDir = Path . GetTempPath ( ) . TrimEnd ( Path . DirectorySeparatorChar ) ;
162+ var envVarName = "SK_TEST_UPLOAD_DIR" ;
163+ var originalValue = Environment . GetEnvironmentVariable ( envVarName ) ;
164+ try
165+ {
166+ Environment . SetEnvironmentVariable ( envVarName , tempDir ) ;
167+ var envVarPath = Path . Combine ( $ "%{ envVarName } %", "testfile.txt" ) ;
168+
169+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
170+ connectorMock . Setup ( c => c . UploadSmallFileAsync ( It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
171+ . Returns ( Task . CompletedTask ) ;
172+
173+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ tempDir ] } ;
174+
175+ // Act — env var should be expanded and path should be allowed
176+ await target . UploadFileAsync ( envVarPath , "/remote.txt" ) ;
177+
178+ // Assert
179+ connectorMock . VerifyAll ( ) ;
180+ }
181+ finally
182+ {
183+ Environment . SetEnvironmentVariable ( envVarName , originalValue ) ;
184+ }
185+ }
186+
187+ [ Fact ]
188+ public async Task ItDeniesExpandedEnvironmentVariablePathsOutsideAllowedAsync ( )
189+ {
190+ // Arrange — set a dedicated test env var; allow a subdirectory but env var expands outside it
191+ var tempDir = Path . GetTempPath ( ) . TrimEnd ( Path . DirectorySeparatorChar ) ;
192+ var allowedDir = Path . Combine ( tempDir , "specific-allowed" ) ;
193+ var envVarName = "SK_TEST_UPLOAD_DIR" ;
194+ var originalValue = Environment . GetEnvironmentVariable ( envVarName ) ;
195+ try
196+ {
197+ Environment . SetEnvironmentVariable ( envVarName , tempDir ) ;
198+ var envVarPath = Path . Combine ( $ "%{ envVarName } %", "outside-file.txt" ) ;
199+
200+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
201+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ allowedDir ] } ;
202+
203+ // Act & Assert — expanded path is outside allowed directory
204+ await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
205+ await target . UploadFileAsync ( envVarPath , "/remote.txt" ) ) ;
206+ }
207+ finally
208+ {
209+ Environment . SetEnvironmentVariable ( envVarName , originalValue ) ;
210+ }
211+ }
212+
213+ [ Fact ]
214+ public async Task ItRespectsPlatformCaseSensitivityAsync ( )
215+ {
216+ // Arrange — use differently-cased allowed dir vs file path
217+ var allowedDir = Path . Combine ( Path . GetTempPath ( ) , "AllowedFolder" ) ;
218+ var filePath = Path . Combine ( Path . GetTempPath ( ) , "allowedfolder" , "file.txt" ) ;
219+
220+ Mock < ICloudDriveConnector > connectorMock = new ( ) ;
221+ connectorMock . Setup ( c => c . UploadSmallFileAsync ( It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
222+ . Returns ( Task . CompletedTask ) ;
223+
224+ CloudDrivePlugin target = new ( connectorMock . Object ) { AllowedUploadDirectories = [ allowedDir ] } ;
225+
226+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
227+ {
228+ // Windows: case-insensitive FS — differently-cased path should be allowed
229+ await target . UploadFileAsync ( filePath , "/remote.txt" ) ;
230+ connectorMock . VerifyAll ( ) ;
231+ }
232+ else
233+ {
234+ // Linux/macOS: case-sensitive FS — differently-cased path should be denied
235+ await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
236+ await target . UploadFileAsync ( filePath , "/remote.txt" ) ) ;
237+ }
238+ }
77239}
0 commit comments