File: BlazorTemplateTest.cs
Web Access
Project: src\src\ProjectTemplates\test\Templates.Mvc.Tests\Templates.Mvc.Tests.csproj (Templates.Mvc.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.InternalTesting;
using Templates.Test.Helpers;
using Xunit.Sdk;
 
namespace Templates.Mvc.Test;
 
public class BlazorTemplateTest : LoggedTest
{
    public BlazorTemplateTest(ProjectFactoryFixture projectFactory)
    {
        ProjectFactory = projectFactory;
    }
 
    public ProjectFactoryFixture ProjectFactory { get; set; }
 
    public static TheoryData<object[]> ArgsData() =>
    [
        [],
        [ArgConstants.UseProgramMain],
        [ArgConstants.NoHttps],
        [ArgConstants.Empty],
        [ArgConstants.NoInteractivity],
        [ArgConstants.WebAssemblyInteractivity],
        [ArgConstants.AutoInteractivity],
        [ArgConstants.GlobalInteractivity],
        [ArgConstants.GlobalInteractivity, ArgConstants.WebAssemblyInteractivity],
        [ArgConstants.GlobalInteractivity, ArgConstants.AutoInteractivity],
        [ArgConstants.NoInteractivity, ArgConstants.UseProgramMain, ArgConstants.NoHttps, ArgConstants.Empty],
    ];
 
    [ConditionalTheory]
    [MemberData(nameof(ArgsData))]
    [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)]
    public Task BlazorWebTemplate_NoAuth(string[] args) => BlazorWebTemplate_Core(args);
 
    [ConditionalTheory]
    [MemberData(nameof(ArgsData))]
    [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)]
    public Task BlazorWebTemplate_IndividualAuth(string[] args) => BlazorWebTemplate_Core([ArgConstants.IndividualAuth, ..args]);
 
    [ConditionalTheory]
    [InlineData(false)]
    [InlineData(true)]
    [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)]
    [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "No LocalDb on non-Windows")]
    public Task BlazorWebTemplate_IndividualAuth_LocalDb(bool useProgramMain) => useProgramMain
        ? BlazorWebTemplate_Core([ArgConstants.IndividualAuth, ArgConstants.UseLocalDb, ArgConstants.UseProgramMain])
        : BlazorWebTemplate_Core([ArgConstants.IndividualAuth, ArgConstants.UseLocalDb]);
 
    private async Task BlazorWebTemplate_Core(string[] args)
    {
        var project = await ProjectFactory.CreateProject(TestOutputHelper);
 
        await project.RunDotNetNewAsync("blazor", args: args);
 
        var expectedLaunchProfileNames = args.Contains(ArgConstants.NoHttps)
            ? new[] { "http", "IIS Express" }
            : new[] { "http", "https", "IIS Express" };
        await project.VerifyLaunchSettings(expectedLaunchProfileNames);
 
        var projectFileContents = await ReadProjectFileAsync(project);
        Assert.DoesNotContain("Microsoft.VisualStudio.Web.CodeGeneration.Design", projectFileContents);
        Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents);
        Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents);
 
        if (!args.Contains(ArgConstants.IndividualAuth))
        {
            Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools", projectFileContents);
            Assert.DoesNotContain("app.db", projectFileContents);
        }
        else
        {
            Assert.Contains("Microsoft.EntityFrameworkCore.Tools", projectFileContents);
 
            if (args.Contains(ArgConstants.UseLocalDb))
            {
                Assert.DoesNotContain("app.db", projectFileContents);
            }
            else
            {
                Assert.Contains("app.db", projectFileContents);
            }
        }
 
        // This can be removed once https://github.com/dotnet/razor/issues/9343 is fixed.
        await WorkAroundNonNullableRenderModeAsync(project);
 
        // 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/netcoreappX.Y/publish and won't be affected by calling build
        // later, while the opposite is not true.
        await project.RunDotNetPublishAsync();
        await project.RunDotNetBuildAsync();
        var expectedPages = GetExpectedPages(args);
        var unexpectedPages = GetUnxpectedPages(args);
 
        async Task VerifyProcessAsync(AspNetProcess process)
        {
            Assert.False(
                process.Process.HasExited,
                ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, process.Process));
 
            await process.AssertPagesOk(expectedPages);
            await process.AssertPagesNotFound(unexpectedPages);
 
            if (args.Contains(ArgConstants.IndividualAuth) && !args.Contains(ArgConstants.Empty))
            {
                var response = await process.SendRequest(BlazorTemplatePages.Auth);
                response.EnsureSuccessStatusCode();
                Assert.Equal("/Account/Login?ReturnUrl=%2Fauth", response.RequestMessage.RequestUri.PathAndQuery);
            }
        }
 
        using (var process = project.StartBuiltProjectAsync())
        {
            await VerifyProcessAsync(process);
        }
        using (var process = project.StartPublishedProjectAsync())
        {
            await VerifyProcessAsync(process);
        }
    }
 
    private static IEnumerable<Page> GetExpectedPages(string[] args)
    {
        yield return new(BlazorTemplatePages.Index);
 
        if (args.Contains(ArgConstants.IndividualAuth))
        {
            yield return new(BlazorTemplatePages.Login);
            yield return new(BlazorTemplatePages.Register);
            yield return new(BlazorTemplatePages.ForgotPassword);
            yield return new(BlazorTemplatePages.ResendEmailConfirmation);
        }
 
        if (args.Contains(ArgConstants.Empty))
        {
            yield break;
        }
 
        yield return new(BlazorTemplatePages.Weather);
 
        if (args.Contains(ArgConstants.NoInteractivity))
        {
            yield break;
        }
 
        yield return new(BlazorTemplatePages.Counter);
    }
 
    private static IEnumerable<string> GetUnxpectedPages(string[] args)
    {
        if (!args.Contains(ArgConstants.IndividualAuth))
        {
            yield return BlazorTemplatePages.Auth;
            yield return BlazorTemplatePages.Login;
        }
 
        if (args.Contains(ArgConstants.Empty))
        {
            yield return BlazorTemplatePages.Weather;
            yield return BlazorTemplatePages.Counter;
            yield return BlazorTemplatePages.Auth;
        }
 
        if (args.Contains(ArgConstants.NoInteractivity))
        {
            yield return BlazorTemplatePages.Counter;
        }
    }
 
    private Task<string> ReadProjectFileAsync(Project project)
    {
        var singleProjectPath = Path.Combine(project.TemplateOutputDir, $"{project.ProjectName}.csproj");
        if (File.Exists(singleProjectPath))
        {
            return File.ReadAllTextAsync(singleProjectPath);
        }
 
        var multiProjectPath = Path.Combine(project.TemplateOutputDir, project.ProjectName, $"{project.ProjectName}.csproj");
        if (File.Exists(multiProjectPath))
        {
            // Change the TemplateOutputDir to that of the main project.
            project.TemplateOutputDir = Path.GetDirectoryName(multiProjectPath);
            return File.ReadAllTextAsync(multiProjectPath);
        }
 
        throw new FailException($"Expected file to exist, but it doesn't: {singleProjectPath}");
    }
 
    private async Task WorkAroundNonNullableRenderModeAsync(Project project)
    {
        var appRazorPath = Path.Combine(project.TemplateOutputDir, "Components", "App.razor");
        var appRazorText = await File.ReadAllTextAsync(appRazorPath);
        appRazorText = appRazorText.Replace("IComponentRenderMode?", "IComponentRenderMode").Replace(": null", ": null!");
        await File.WriteAllTextAsync(appRazorPath, appRazorText);
    }
 
    private class BlazorTemplatePages
    {
        internal const string Index = "";
        internal const string Weather = "weather";
        internal const string Counter = "counter";
        internal const string Auth = "auth";
        internal const string Login = "Account/Login";
        internal const string Register = "Account/Register";
        internal const string ForgotPassword = "Account/ForgotPassword";
        internal const string ResendEmailConfirmation = "Account/ResendEmailConfirmation";
    }
}