1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. 2 3 using System; 4 using System.Collections.Generic; 5 using System.Web.Hosting; 6 using System.Web.WebPages.Scope; 7 using Moq; 8 using Xunit; 9 using Assert = Microsoft.TestCommon.AssertEx; 10 11 namespace Microsoft.Web.Helpers.Test 12 { 13 public class ThemesTest 14 { 15 [Fact] Initialize_WithBadParams_Throws()16 public void Initialize_WithBadParams_Throws() 17 { 18 // Arrange 19 var mockVpp = new Mock<VirtualPathProvider>().Object; 20 var scope = new ScopeStorageDictionary(); 21 22 // Act and Assert 23 Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize(null, "foo"), "themeDirectory"); 24 Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize("", "foo"), "themeDirectory"); 25 26 Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize("~/folder", null), "defaultTheme"); 27 Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize("~/folder", ""), "defaultTheme"); 28 } 29 30 [Fact] CurrentThemeThrowsIfAssignedNullOrEmpty()31 public void CurrentThemeThrowsIfAssignedNullOrEmpty() 32 { 33 // Arrange 34 var mockVpp = new Mock<VirtualPathProvider>().Object; 35 var scope = new ScopeStorageDictionary(); 36 var themesImpl = new ThemesImplementation(mockVpp, scope); 37 38 // Act and Assert 39 Assert.ThrowsArgumentNullOrEmptyString(() => { themesImpl.CurrentTheme = null; }, "value"); 40 Assert.ThrowsArgumentNullOrEmptyString(() => { themesImpl.CurrentTheme = String.Empty; }, "value"); 41 } 42 43 [Fact] InvokingPropertiesAndMethodsBeforeInitializationThrows()44 public void InvokingPropertiesAndMethodsBeforeInitializationThrows() 45 { 46 // Arrange 47 var mockVpp = new Mock<VirtualPathProvider>().Object; 48 var scope = new ScopeStorageDictionary(); 49 var themesImpl = new ThemesImplementation(mockVpp, scope); 50 51 // Act and Assert 52 Assert.Throws<InvalidOperationException>(() => themesImpl.CurrentTheme = "Foo", 53 @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class."); 54 55 Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.CurrentTheme; }, 56 @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class."); 57 58 Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.AvailableThemes; }, 59 @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class."); 60 61 Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.DefaultTheme; }, 62 @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class."); 63 64 Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.GetResourcePath("baz"); }, 65 @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class."); 66 67 Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.GetResourcePath("baz", "some-file"); }, 68 @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class."); 69 } 70 71 [Fact] InitializeThrowsIfDefaultThemeDirectoryDoesNotExist()72 public void InitializeThrowsIfDefaultThemeDirectoryDoesNotExist() 73 { 74 // Arrange 75 var defaultTheme = "default-theme"; 76 var themeDirectory = "theme-directory"; 77 78 var scope = new ScopeStorageDictionary(); 79 var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir("not-default-theme")), scope); 80 81 // Act And Assert 82 Assert.ThrowsArgument( 83 () => themesImpl.Initialize(themeDirectory: themeDirectory, defaultTheme: defaultTheme), 84 "defaultTheme", 85 "Unknown theme 'default-theme'. Ensure that a directory labeled 'default-theme' exists under the theme directory."); 86 } 87 88 [Fact] ThemesImplUsesScopeStorageToStoreProperties()89 public void ThemesImplUsesScopeStorageToStoreProperties() 90 { 91 // Arrange 92 var defaultTheme = "default-theme"; 93 var themeDirectory = "theme-directory"; 94 95 var scope = new ScopeStorageDictionary(); 96 var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme)), scope); 97 98 // Act 99 themesImpl.Initialize(themeDirectory: themeDirectory, defaultTheme: defaultTheme); 100 101 // Ensure Theme use scope storage to store properties 102 Assert.Equal(scope[ThemesImplementation.ThemesInitializedKey], true); 103 Assert.Equal(scope[ThemesImplementation.ThemeDirectoryKey], themeDirectory); 104 Assert.Equal(scope[ThemesImplementation.DefaultThemeKey], defaultTheme); 105 } 106 107 [Fact] ThemesImplUsesDefaultThemeWhenNoCurrentThemeIsSpecified()108 public void ThemesImplUsesDefaultThemeWhenNoCurrentThemeIsSpecified() 109 { 110 // Arrange 111 var defaultTheme = "default-theme"; 112 var themeDirectory = "theme-directory"; 113 114 var scope = new ScopeStorageDictionary(); 115 var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme)), scope); 116 themesImpl.Initialize(themeDirectory, defaultTheme); 117 118 // Act and Assert 119 // CurrentTheme falls back to default theme when null 120 Assert.Equal(themesImpl.CurrentTheme, defaultTheme); 121 } 122 123 [Fact] ThemesImplThrowsIfCurrentThemeIsInvalid()124 public void ThemesImplThrowsIfCurrentThemeIsInvalid() 125 { 126 // Arrange 127 var defaultTheme = "default-theme"; 128 var themeDirectory = "theme-directory"; 129 var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme), new Dir("not-a-random-value")), new ScopeStorageDictionary()); 130 themesImpl.Initialize(themeDirectory, defaultTheme); 131 132 // Act and Assert 133 Assert.ThrowsArgument(() => themesImpl.CurrentTheme = "random-value", 134 "value", 135 "Unknown theme 'random-value'. Ensure that a directory labeled 'random-value' exists under the theme directory."); 136 } 137 138 [Fact] ThemesImplUsesScopeStorageToStoreCurrentTheme()139 public void ThemesImplUsesScopeStorageToStoreCurrentTheme() 140 { 141 // Arrange 142 var defaultTheme = "default-theme"; 143 var themeDirectory = "theme-directory"; 144 var currentThemeDir = "custom-theme-dir"; 145 var scope = new ScopeStorageDictionary(); 146 var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme), new Dir("custom-theme-dir")), scope); 147 148 // Act 149 themesImpl.Initialize(themeDirectory, defaultTheme); 150 themesImpl.CurrentTheme = currentThemeDir; 151 152 // Assert 153 Assert.Equal(scope[ThemesImplementation.CurrentThemeKey], currentThemeDir); 154 } 155 156 [Fact] GetResourcePathThrowsIfCurrentDirectoryIsNull()157 public void GetResourcePathThrowsIfCurrentDirectoryIsNull() 158 { 159 // Arrange 160 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 161 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile", "wp7.css"))); 162 themesImpl.Initialize("themes", "default"); 163 164 // Act and Assert 165 Assert.ThrowsArgumentNull(() => themesImpl.GetResourcePath(folder: null, fileName: "wp7.css"), "folder"); 166 } 167 168 [Fact] GetResourcePathThrowsIfFileNameIsNullOrEmpty()169 public void GetResourcePathThrowsIfFileNameIsNullOrEmpty() 170 { 171 // Arrange 172 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 173 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile", "wp7.css"))); 174 themesImpl.Initialize("themes", "default"); 175 176 // Act and Assert 177 Assert.ThrowsArgumentNullOrEmptyString(() => themesImpl.GetResourcePath(folder: String.Empty, fileName: null), "fileName"); 178 Assert.ThrowsArgumentNullOrEmptyString(() => themesImpl.GetResourcePath(folder: String.Empty, fileName: String.Empty), "fileName"); 179 } 180 181 [Fact] GetResourcePathReturnsItemFromThemeRootIfAvailable()182 public void GetResourcePathReturnsItemFromThemeRootIfAvailable() 183 { 184 // Arrange 185 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 186 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile", "wp7.css"))); 187 themesImpl.Initialize("themes", "default"); 188 189 // Act 190 themesImpl.CurrentTheme = "mobile"; 191 var themePath = themesImpl.GetResourcePath(fileName: "wp7.css"); 192 193 // Assert 194 Assert.Equal(themePath, @"themes/mobile/wp7.css"); 195 } 196 197 [Fact] GetResourcePathReturnsItemFromCurrentThemeDirectoryIfAvailable()198 public void GetResourcePathReturnsItemFromCurrentThemeDirectoryIfAvailable() 199 { 200 // Arrange 201 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 202 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css"))); 203 themesImpl.Initialize("themes", "default"); 204 205 // Act 206 themesImpl.CurrentTheme = "mobile"; 207 var themePath = themesImpl.GetResourcePath(folder: "styles", fileName: "wp7.css"); 208 209 // Assert 210 Assert.Equal(themePath, @"themes/mobile/styles/wp7.css"); 211 } 212 213 [Fact] GetResourcePathReturnsItemFromDefaultThemeDirectoryIfNotFoundInCurrentThemeDirectory()214 public void GetResourcePathReturnsItemFromDefaultThemeDirectoryIfNotFoundInCurrentThemeDirectory() 215 { 216 // Arrange 217 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 218 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css"))); 219 themesImpl.Initialize("themes", "default"); 220 221 // Act 222 themesImpl.CurrentTheme = "mobile"; 223 var themePath = themesImpl.GetResourcePath(folder: "styles", fileName: "main.css"); 224 225 // Assert 226 Assert.Equal(themePath, @"themes/default/styles/main.css"); 227 } 228 229 [Fact] GetResourcePathReturnsNullIfDirectoryDoesNotExist()230 public void GetResourcePathReturnsNullIfDirectoryDoesNotExist() 231 { 232 // Arrange 233 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 234 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css"))); 235 themesImpl.Initialize("themes", "default"); 236 237 // Act 238 themesImpl.CurrentTheme = "mobile"; 239 var themePath = themesImpl.GetResourcePath(folder: "does-not-exist", fileName: "main.css"); 240 241 // Assert 242 Assert.Null(themePath); 243 } 244 245 [Fact] GetResourcePathReturnsNullIfItemNotFoundInCurrentAndDefaultThemeDirectories()246 public void GetResourcePathReturnsNullIfItemNotFoundInCurrentAndDefaultThemeDirectories() 247 { 248 // Arrange 249 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 250 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css"))); 251 themesImpl.Initialize("themes", "default"); 252 253 // Act 254 themesImpl.CurrentTheme = "mobile"; 255 var themePath = themesImpl.GetResourcePath(folder: "styles", fileName: "awesome-blinking-text.css"); 256 257 // Assert 258 Assert.Null(themePath); 259 } 260 261 [Fact] AvaliableThemesReturnsTopLevelDirectoriesUnderThemeDirectory()262 public void AvaliableThemesReturnsTopLevelDirectoriesUnderThemeDirectory() 263 { 264 // Arrange 265 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 266 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir("rotary-phone"))); 267 // Act 268 themesImpl.Initialize("themes", "default"); 269 var themes = themesImpl.AvailableThemes; 270 271 // Assert 272 Assert.Equal(3, themes.Count); 273 Assert.Equal(themes[0], "default"); 274 Assert.Equal(themes[1], "mobile"); 275 Assert.Equal(themes[2], "rotary-phone"); 276 } 277 278 /// <remarks> 279 /// // folder structure: 280 /// // /root 281 /// // /foo 282 /// // /bar.cs 283 /// // testing that a file specified as foo/bar in folder root will return null 284 /// </remarks> 285 [Fact] FileWithSlash_ReturnsNull()286 public void FileWithSlash_ReturnsNull() 287 { 288 // Arrange 289 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 290 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("root"), new Dir(@"root\foo", "wp7.css"), new Dir(@"default\styles", "main.css"))); 291 292 // Act 293 var actual = themesImpl.FindMatchingFile("root", "foo/bar.cs"); 294 295 // Assert 296 Assert.Null(actual); 297 } 298 299 [Fact] DirectoryWithNoFilesReturnsNull()300 public void DirectoryWithNoFilesReturnsNull() 301 { 302 // Arrange 303 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 304 vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("empty-dir"))); 305 306 // Act 307 var actual = themesImpl.FindMatchingFile(@"themes\empty-dir", "main.css"); 308 309 // Assert 310 Assert.Null(actual); 311 } 312 313 [Fact] MatchingFiles_ReturnsCorrectFile()314 public void MatchingFiles_ReturnsCorrectFile() 315 { 316 // Arrange 317 var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(), 318 vpp: GetVirtualPathProvider("themes", new Dir(@"nomatchingfiles", "foo.cs"))); 319 320 // Act 321 var bar = themesImpl.FindMatchingFile(@"themes\nomatchingfiles", "bar.cs"); 322 var foo = themesImpl.FindMatchingFile(@"themes\nomatchingfiles", "foo.cs"); 323 324 // Assert 325 Assert.Null(bar); 326 Assert.Equal(@"themes/nomatchingfiles/foo.cs", foo); 327 } 328 GetVirtualPathProvider(string themeRoot, params Dir[] fileSystem)329 private static VirtualPathProvider GetVirtualPathProvider(string themeRoot, params Dir[] fileSystem) 330 { 331 var mockVpp = new Mock<VirtualPathProvider>(); 332 var dirRoot = new Mock<VirtualDirectory>(themeRoot); 333 334 var themeDirectories = new List<VirtualDirectory>(); 335 foreach (var directory in fileSystem) 336 { 337 var dir = new Mock<VirtualDirectory>(directory.Name); 338 var directoryPath = themeRoot + '\\' + directory.Name; 339 dir.SetupGet(d => d.Name).Returns(directory.Name); 340 mockVpp.Setup(c => c.GetDirectory(It.Is<string>(p => p.Equals(directoryPath, StringComparison.OrdinalIgnoreCase)))).Returns(dir.Object); 341 342 var fileList = new List<VirtualFile>(); 343 foreach (var item in directory.Files) 344 { 345 var filePath = directoryPath + '\\' + item; 346 var file = new Mock<VirtualFile>(filePath); 347 file.SetupGet(f => f.Name).Returns(item); 348 fileList.Add(file.Object); 349 } 350 351 dir.SetupGet(c => c.Files).Returns(fileList); 352 themeDirectories.Add(dir.Object); 353 } 354 355 dirRoot.SetupGet(c => c.Directories).Returns(themeDirectories); 356 mockVpp.Setup(c => c.GetDirectory(themeRoot)).Returns(dirRoot.Object); 357 358 return mockVpp.Object; 359 } 360 361 private class Dir 362 { Dir(string name, params string[] files)363 public Dir(string name, params string[] files) 364 { 365 Name = name; 366 Files = files; 367 } 368 369 public string Name { get; private set; } 370 371 public IEnumerable<string> Files { get; private set; } 372 } 373 } 374 } 375