// 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.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.E2ETesting; using Newtonsoft.Json.Linq; using OpenQA.Selenium; using Templates.Test.Helpers; using Xunit; using Xunit.Abstractions; // Turn off parallel test run for Edge as the driver does not support multiple Selenium tests at the same time #if EDGE [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] #endif namespace Templates.Test.SpaTemplateTest { public class SpaTemplateTestBase : BrowserTestBase { public SpaTemplateTestBase( ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output) : base(browserFixture, output) { ProjectFactory = projectFactory; } public ProjectFactoryFixture ProjectFactory { get; set; } public Project Project { get; set; } // Rather than using [Theory] to pass each of the different values for 'template', // it's important to distribute the SPA template tests over different test classes // so they can be run in parallel. Xunit doesn't parallelize within a test class. protected async Task SpaTemplateImplAsync( string key, string template, bool useLocalDb = false, bool usesAuth = false) { Project = await ProjectFactory.GetOrCreateProject(key, Output); using var createResult = await Project.RunDotNetNewAsync(template, auth: usesAuth ? "Individual" : null, language: null, useLocalDb); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult)); // We shouldn't have to do the NPM restore in tests because it should happen // automatically at build time, but by doing it up front we can avoid having // multiple NPM installs run concurrently which otherwise causes errors when // tests run in parallel. var clientAppSubdirPath = Path.Combine(Project.TemplateOutputDir, "ClientApp"); ValidatePackageJson(clientAppSubdirPath); var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj"); if (usesAuth && !useLocalDb) { Assert.Contains(".db", projectFileContents); } using var npmRestoreResult = await Project.RestoreWithRetryAsync(Output, clientAppSubdirPath); Assert.True(0 == npmRestoreResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm restore", Project, npmRestoreResult)); using var lintResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run lint"); Assert.True(0 == lintResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run lint", Project, lintResult)); if (template == "react" || template == "reactredux") { using var testResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run test"); Assert.True(0 == testResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run test", Project, testResult)); } using var publishResult = await Project.RunDotNetPublishAsync(); Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult)); // Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release // The output from publish will go into bin/Release/netcoreapp3.0/publish and won't be affected by calling build // later, while the opposite is not true. using var buildResult = await Project.RunDotNetBuildAsync(); Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult)); // localdb is not installed on the CI machines, so skip it. var shouldVisitFetchData = !useLocalDb; if (usesAuth) { using var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync(template); Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult)); Project.AssertEmptyMigration(template); if (shouldVisitFetchData) { using var dbUpdateResult = await Project.RunDotNetEfUpdateDatabaseAsync(); Assert.True(0 == dbUpdateResult.ExitCode, ErrorMessages.GetFailedProcessMessage("update database", Project, dbUpdateResult)); } } using (var aspNetProcess = Project.StartBuiltProjectAsync()) { Assert.False( aspNetProcess.Process.HasExited, ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process)); await WarmUpServer(aspNetProcess); await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html"); if (BrowserFixture.IsHostAutomationSupported()) { var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, $"{Project.ProjectName}.build"); aspNetProcess.VisitInBrowser(browser); TestBasicNavigation(visitFetchData: shouldVisitFetchData, usesAuth, browser, logs); } else { BrowserFixture.EnforceSupportedConfigurations(); } } if (usesAuth) { UpdatePublishedSettings(); } using (var aspNetProcess = Project.StartPublishedProjectAsync()) { Assert.False( aspNetProcess.Process.HasExited, ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process)); await WarmUpServer(aspNetProcess); await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html"); if (BrowserFixture.IsHostAutomationSupported()) { var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, $"{Project.ProjectName}.publish"); aspNetProcess.VisitInBrowser(browser); TestBasicNavigation(visitFetchData: shouldVisitFetchData, usesAuth, browser, logs); } else { BrowserFixture.EnforceSupportedConfigurations(); } } } private void ValidatePackageJson(string clientAppSubdirPath) { Assert.True(File.Exists(Path.Combine(clientAppSubdirPath, "package.json")), "Missing a package.json"); var packageJson = JObject.Parse(ReadFile(clientAppSubdirPath, "package.json")); // NPM package names must match ^(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$ var packageName = (string)packageJson["name"]; Regex regex = new Regex("^(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$"); Assert.True(regex.IsMatch(packageName), "package.json name is invalid format"); } private static async Task WarmUpServer(AspNetProcess aspNetProcess) { var attempt = 0; var maxAttempts = 3; do { try { attempt++; var response = await aspNetProcess.SendRequest("/"); if (response.StatusCode == HttpStatusCode.OK) { break; } } catch (OperationCanceledException) { } catch (HttpRequestException ex) when (ex.Message.StartsWith("The SSL connection could not be established")) { } await Task.Delay(TimeSpan.FromSeconds(5 * attempt)); } while (attempt < maxAttempts); } private void UpdatePublishedSettings() { // Hijack here the config file to use the development key during publish. var appSettings = JObject.Parse(File.ReadAllText(Path.Combine(Project.TemplateOutputDir, "appsettings.json"))); var appSettingsDevelopment = JObject.Parse(File.ReadAllText(Path.Combine(Project.TemplateOutputDir, "appsettings.Development.json"))); ((JObject)appSettings["IdentityServer"]).Merge(appSettingsDevelopment["IdentityServer"]); ((JObject)appSettings["IdentityServer"]).Merge(new { IdentityServer = new { Key = new { FilePath = "./tempkey.json" } } }); var testAppSettings = appSettings.ToString(); File.WriteAllText(Path.Combine(Project.TemplatePublishDir, "appsettings.json"), testAppSettings); } private void TestBasicNavigation(bool visitFetchData, bool usesAuth, IWebDriver browser, ILogs logs) { browser.Exists(By.TagName("ul")); //