diff --git a/.gitignore b/.gitignore index 758b1bae9c..0eb79aa300 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ tools/ .nuget/.marker.v* nuget.exe AssemblyInfo.g.cs +*.g.txt tests/Scripts/Config-*.json # MSTest test Results diff --git a/sign.thirdparty.props b/sign.thirdparty.props index 21b8a130b2..7d468c6722 100644 --- a/sign.thirdparty.props +++ b/sign.thirdparty.props @@ -2,6 +2,7 @@ + diff --git a/src/NuGetGallery/App_Start/AppActivator.cs b/src/NuGetGallery/App_Start/AppActivator.cs index 8939aad8e0..51dbc640ef 100644 --- a/src/NuGetGallery/App_Start/AppActivator.cs +++ b/src/NuGetGallery/App_Start/AppActivator.cs @@ -100,7 +100,7 @@ private static RazorViewEngine CreateViewEngine() private static void BundlingPostStart() { // Add primary style bundle - Bundle stylesBundle = new StyleBundle("~/Content/css"); + Bundle stylesBundle = new StyleBundle("~/Content/css.min.css"); foreach (string filename in new[] { "Site.css", "Layout.css", @@ -145,6 +145,14 @@ private static void BundlingPostStart() .Include("~/Scripts/gallery/common-multi-select-dropdown.js"); BundleTable.Bundles.Add(multiSelectDropdownBundle); + var asyncFileUploadScriptBundle = new ScriptBundle("~/Scripts/gallery/async-file-upload.min.js") + .Include("~/Scripts/gallery/async-file-upload.js"); + BundleTable.Bundles.Add(asyncFileUploadScriptBundle); + + var certificatesScriptBundle = new ScriptBundle("~/Scripts/gallery/certificates.min.js") + .Include("~/Scripts/gallery/certificates.js"); + BundleTable.Bundles.Add(certificatesScriptBundle); + var homeScriptBundle = new ScriptBundle("~/Scripts/gallery/page-home.min.js") .Include("~/Scripts/gallery/page-home.js"); BundleTable.Bundles.Add(homeScriptBundle); @@ -204,16 +212,13 @@ private static void BundlingPostStart() new ScriptResourceDefinition { Path = scriptBundle.Path }); // Add support requests bundles - var jQueryUiStylesBundle = new StyleBundle("~/Content/themes/custom/jqueryui") - .Include("~/Content/themes/custom/jquery-ui-1.10.3.custom.css"); - BundleTable.Bundles.Add(jQueryUiStylesBundle); - - var supportRequestStylesBundle = new StyleBundle("~/Content/page-support-requests") + var supportRequestStylesBundle = new StyleBundle("~/Content/themes/custom/page-support-requests.min.css") + .Include("~/Content/themes/custom/jquery-ui-1.10.3.custom.css") .Include("~/Content/admin/SupportRequestStyles.css"); BundleTable.Bundles.Add(supportRequestStylesBundle); - var supportRequestsBundle = new ScriptBundle("~/Scripts/page-support-requests") - .Include("~/Scripts/gallery/jquery-ui-{version}.js") + var supportRequestsBundle = new ScriptBundle("~/Scripts/page-support-requests.min.js") + .Include("~/Scripts/gallery/jquery-ui-1.10.3.js") .Include("~/Scripts/gallery/knockout-projections.js") .Include("~/Scripts/gallery/page-support-requests.js"); BundleTable.Bundles.Add(supportRequestsBundle); diff --git a/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Admins.cshtml b/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Admins.cshtml index f59de5b5fc..65950b1c00 100644 --- a/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Admins.cshtml +++ b/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Admins.cshtml @@ -6,8 +6,7 @@ } @section TopScripts{ - @Styles.Render("~/Content/themes/custom/jqueryui") - @Styles.Render("~/Content/page-support-requests") + @Styles.Render("~/Content/themes/custom/page-support-requests.min.css") } @ViewHelpers.AjaxAntiForgeryToken(Html) @@ -87,7 +86,7 @@ @section BottomScripts{ - @Scripts.Render("~/Scripts/page-support-requests") + @Scripts.Render("~/Scripts/page-support-requests.min.js") diff --git a/src/NuGetGallery/Views/Packages/UploadPackage.cshtml b/src/NuGetGallery/Views/Packages/UploadPackage.cshtml index 34be4abb77..e32d6cbb81 100644 --- a/src/NuGetGallery/Views/Packages/UploadPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/UploadPackage.cshtml @@ -74,7 +74,7 @@ { @* Right now this is the only page that uses this script. If we increase our usage of it, we should put it in our bundles *@ @Scripts.Render("~/Scripts/gallery/page-edit-readme.min.js") - @Scripts.Render("~/Scripts/gallery/async-file-upload.js") + @Scripts.Render("~/Scripts/gallery/async-file-upload.min.js") diff --git a/src/NuGetGallery/Web.config b/src/NuGetGallery/Web.config index d61a92882c..38efcda602 100644 --- a/src/NuGetGallery/Web.config +++ b/src/NuGetGallery/Web.config @@ -214,14 +214,12 @@ - - - + - - - - + + + + @@ -566,6 +564,10 @@ + + + + @@ -656,4 +658,4 @@ - \ No newline at end of file + diff --git a/tests/NuGetGallery.FunctionalTests/NuGetGallery.FunctionalTests.csproj b/tests/NuGetGallery.FunctionalTests/NuGetGallery.FunctionalTests.csproj index a5118589a0..f374fcc835 100644 --- a/tests/NuGetGallery.FunctionalTests/NuGetGallery.FunctionalTests.csproj +++ b/tests/NuGetGallery.FunctionalTests/NuGetGallery.FunctionalTests.csproj @@ -60,6 +60,7 @@ + @@ -102,5 +103,18 @@ all + + $(MSBuildProjectDirectory)\..\..\src\NuGetGallery + + + + + + + + + + + \ No newline at end of file diff --git a/tests/NuGetGallery.FunctionalTests/StaticAssets/StaticAssetsTests.cs b/tests/NuGetGallery.FunctionalTests/StaticAssets/StaticAssetsTests.cs new file mode 100644 index 0000000000..ee6bae4019 --- /dev/null +++ b/tests/NuGetGallery.FunctionalTests/StaticAssets/StaticAssetsTests.cs @@ -0,0 +1,218 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Runtime.Remoting.Contexts; +using System.Security.Policy; +using System.ServiceModel.Syndication; +using System.Threading.Tasks; +using System.Xml; +using Xunit; +using Xunit.Abstractions; + +namespace NuGetGallery.FunctionalTests.StaticAssets +{ + public class StaticAssetsTests : GalleryTestBase, IDisposable + { + public StaticAssetsTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + HttpClient = new HttpClient(); + } + + private static readonly Lazy> AssetPaths = new Lazy>(() => GetAssetPaths().ToList()); + private static readonly IReadOnlyDictionary> Bundles = new Dictionary> + { + // CSS + { + "Content/css.min.css", + new[] + { + "Content/Site.css", + "Content/Layout.css", + "Content/PageStylings.css", + "Content/fabric.css", + } + }, + { + "Content/gallery/css/site.min.css", + new[] + { + "Content/gallery/css/bootstrap.css", + "Content/gallery/css/bootstrap-theme.css", + "Content/gallery/css/fabric.css", + } + }, + { + "Content/themes/custom/page-support-requests.min.css", + new[] + { + "Content/themes/custom/jquery-ui-1.10.3.custom.css", + "Content/admin/SupportRequestStyles.css", + } + }, + + // JavaScript + { + "Scripts/gallery/site.min.js", + new[] + { + "Scripts/gallery/jquery-3.4.1.js", + "Scripts/gallery/jquery.validate-1.16.0.js", + "Scripts/gallery/jquery.validate.unobtrusive-3.2.6.js", + "Scripts/gallery/knockout-3.4.2.js", + "Scripts/gallery/bootstrap.js", + "Scripts/gallery/moment-2.18.1.js", + "Scripts/gallery/common.js", + "Scripts/gallery/autocomplete.js", + } + }, + { + "Scripts/gallery/stats.min.js", + new[] + { + "Scripts/d3/d3.js", + "Scripts/gallery/stats-perpackagestatsgraphs.js", + "Scripts/gallery/stats-dimensions.js", + } + }, + { + "Scripts/gallery/page-display-package.min.js", + new[] + { + "Scripts/gallery/page-display-package.js", + "Scripts/gallery/clamp.js", + } + }, + { + "Scripts/gallery/page-add-organization.min.js", + new[] + { + "Scripts/gallery/page-add-organization.js", + "Scripts/gallery/md5.js", + } + }, + { + "Scripts/page-support-requests.min.js", + new[] + { + "Scripts/gallery/jquery-ui-1.10.3.js", + "Scripts/gallery/knockout-projections.js", + "Scripts/gallery/page-support-requests.js", + } + }, + }; + + private static readonly HashSet BundleInputPaths = new HashSet(Bundles.SelectMany(x => x.Value)); + + public static IEnumerable AssetData => AssetPaths.Value.Select(x => new object[] { x }); + public static IEnumerable BundleOutputData => Bundles.Select(x => new object[] { x.Key }); + public static IEnumerable BundleInputExceptBundleOutputData => BundleInputPaths + .Except(Bundles.Keys.Select(GetUnMinPath)) + .Select(x => new object[] { x }); + + public HttpClient HttpClient { get; } + + [Theory] + [Priority(2)] + [Category("P2Tests")] + [MemberData(nameof(AssetData))] + public async Task AllAssetsExistOnTheirOwn(string assetPath) + { + var bundleContent = await HttpClient.GetStringAsync(UrlHelper.BaseUrl + assetPath); + + Assert.DoesNotContain("Minification failed", Shorten(bundleContent), StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [Priority(2)] + [Category("P2Tests")] + [MemberData(nameof(BundleOutputData))] + public async Task NoBundleFailsMinification(string bundle) + { + var bundleContent = await HttpClient.GetStringAsync(UrlHelper.BaseUrl + bundle); + + Assert.DoesNotContain("Minification failed", Shorten(bundleContent), StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [Priority(2)] + [Category("P2Tests")] + [MemberData(nameof(BundleInputExceptBundleOutputData))] + public async Task BundledFilesDoNotExistAsMinified(string assetPath) + { + var minifiedAssetPath = GetMinPath(assetPath); + + using (var response = await HttpClient.GetAsync(UrlHelper.BaseUrl + minifiedAssetPath)) + { + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } + + private static IEnumerable GetAssetPaths() + { + var type = typeof(StaticAssetsTests); + using (var stream = type.Assembly.GetManifestResourceStream(type.Namespace + ".Data.g.txt")) + using (var reader = new StreamReader(stream)) + { + // First line is the gallery directory itself. + var firstLine = reader.ReadLine(); + if (firstLine == null) + { + throw new InvalidOperationException("The generated list of static assets could not be read."); + } + + var galleryDir = Path.GetFullPath(firstLine.TrimEnd('\\')) + '\\'; + string absolutePath; + while ((absolutePath = reader.ReadLine()) != null) + { + var fullPath = Path.GetFullPath(absolutePath); + if (!fullPath.StartsWith(galleryDir)) + { + continue; + } + + var relativePath = fullPath.Substring(galleryDir.Length); + yield return relativePath.Replace('\\', '/'); + } + } + } + + private static string GetMinPath(string assetPath) + { + var extension = Path.GetExtension(assetPath); + var minifiedAssetPath = assetPath.Substring(0, assetPath.Length - extension.Length) + ".min" + extension; + return minifiedAssetPath; + } + + private static string GetUnMinPath(string assetPath) + { + return assetPath.Replace(".min", string.Empty); + } + + /// + /// If the assertion fails with a massive file, it gets ugly in Visual Studio. Like crashes. The minification + /// error is at the top so we only need the beginning of the content. + /// + private static string Shorten(string content) + { + const int length = 1024; + if (content.Length > length) + { + return content.Substring(0, 1024); + } + + return content; + } + + public void Dispose() + { + HttpClient.Dispose(); + } + } +} \ No newline at end of file