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