Add unit tests for new controller

This commit is contained in:
Chris Cheetham 2021-06-15 12:32:24 -04:00
Родитель 939a4ebf30
Коммит 6b253cf45c
34 изменённых файлов: 1325 добавлений и 442 удалений

23
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,23 @@
root = true
[*]
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
charset = utf-8
[*.{csproj,props,targets,DotSettings}]
indent_size = 2
[*.cs]
indent_size = 4
[*.yaml]
indent_size = 2
[*.ps1]
indent_size = 4
[*.json]
indent_size = 2

Просмотреть файл

@ -5,14 +5,19 @@ VisualStudioVersion = 16.6.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C742A7B8-80CA-4365-85CA-C29AA744CE54}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreToolService", "src\NetCoreToolService\NetCoreToolService.csproj", "{1462EDFE-F1FC-48C2-80C1-917317EE3C97}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Utils", "src\Common.Utils\Steeltoe.Common.Utils.csproj", "{3E82184B-FA14-4B24-9ED0-A823465B6AB9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.NetCoreToolService", "src\NetCoreToolService\Steeltoe.NetCoreToolService.csproj", "{1462EDFE-F1FC-48C2-80C1-917317EE3C97}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{410C0E72-737F-4168-AECA-2F6D19EE86D5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreToolService.Test", "test\NetCoreToolService.Test\NetCoreToolService.Test.csproj", "{6BD6C793-E555-475F-A2E1-12D7F3F56A3B}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Utils.Test", "test\Common.Utils.Test\Steeltoe.Common.Utils.Test.csproj", "{06247239-19D8-4FB5-99F1-F399346ADE4F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.NetCoreToolService.Test", "test\NetCoreToolService.Test\Steeltoe.NetCoreToolService.Test.csproj", "{6BD6C793-E555-475F-A2E1-12D7F3F56A3B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C37528B8-BDD1-440F-B69A-AD57F939620C}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitconfig = .gitconfig
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
@ -57,6 +62,30 @@ Global
{6BD6C793-E555-475F-A2E1-12D7F3F56A3B}.Release|x64.Build.0 = Release|Any CPU
{6BD6C793-E555-475F-A2E1-12D7F3F56A3B}.Release|x86.ActiveCfg = Release|Any CPU
{6BD6C793-E555-475F-A2E1-12D7F3F56A3B}.Release|x86.Build.0 = Release|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Debug|x64.ActiveCfg = Debug|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Debug|x64.Build.0 = Debug|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Debug|x86.ActiveCfg = Debug|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Debug|x86.Build.0 = Debug|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Release|Any CPU.Build.0 = Release|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Release|x64.ActiveCfg = Release|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Release|x64.Build.0 = Release|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Release|x86.ActiveCfg = Release|Any CPU
{3E82184B-FA14-4B24-9ED0-A823465B6AB9}.Release|x86.Build.0 = Release|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Debug|x64.ActiveCfg = Debug|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Debug|x64.Build.0 = Debug|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Debug|x86.ActiveCfg = Debug|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Debug|x86.Build.0 = Debug|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Release|Any CPU.Build.0 = Release|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Release|x64.ActiveCfg = Release|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Release|x64.Build.0 = Release|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Release|x86.ActiveCfg = Release|Any CPU
{06247239-19D8-4FB5-99F1-F399346ADE4F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -64,6 +93,8 @@ Global
GlobalSection(NestedProjects) = preSolution
{1462EDFE-F1FC-48C2-80C1-917317EE3C97} = {C742A7B8-80CA-4365-85CA-C29AA744CE54}
{6BD6C793-E555-475F-A2E1-12D7F3F56A3B} = {410C0E72-737F-4168-AECA-2F6D19EE86D5}
{3E82184B-FA14-4B24-9ED0-A823465B6AB9} = {C742A7B8-80CA-4365-85CA-C29AA744CE54}
{06247239-19D8-4FB5-99F1-F399346ADE4F} = {410C0E72-737F-4168-AECA-2F6D19EE86D5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D8EFB01A-92BF-418B-B2B2-A12045B772E2}

Просмотреть файл

@ -2,7 +2,22 @@
<PropertyGroup>
<SteeltoeNetCoreToolServiceVersion>0.0.1</SteeltoeNetCoreToolServiceVersion>
<SwashbuckleVersion>5.6.*</SwashbuckleVersion>
</PropertyGroup>
<PropertyGroup>
<MicrosoftExtensionsVersion>5.0.*</MicrosoftExtensionsVersion>
<SwashbuckleVersion>5.6.*</SwashbuckleVersion>
</PropertyGroup>
<PropertyGroup>
<CoverletCollectorVersion>1.3.*</CoverletCollectorVersion>
<CoverletMsBuildVersion>2.9.*</CoverletMsBuildVersion>
<FluentAssertionsVersion>5.10.*</FluentAssertionsVersion>
<FluentAssertionsJsonVersion>5.5.*</FluentAssertionsJsonVersion>
<MicrosoftAspNetCoreMvcTestingVersion>3.1.*</MicrosoftAspNetCoreMvcTestingVersion>
<MicrosoftNetTestSdkVersion>16.7.*</MicrosoftNetTestSdkVersion>
<MoqVersion>4.14.*</MoqVersion>
<XunitVersion>2.4.*</XunitVersion>
</PropertyGroup>
</Project>

Просмотреть файл

@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using System;
namespace Steeltoe.Common.Utils.Diagnostics
{
/// <summary>
/// The exception that is thrown when a system error occurs running a command.
/// </summary>
public class CommandException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandException"/> class.
/// </summary>
/// <param name="message">Foo.</param>
/// <param name="innerException">Foo bar.</param>
public CommandException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

Просмотреть файл

@ -0,0 +1,134 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace Steeltoe.Common.Utils.Diagnostics
{
/// <inheritdoc/>
public class CommandExecutor : ICommandExecutor
{
private static int _commandCounter;
private readonly ILogger<CommandExecutor> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CommandExecutor"/> class.
/// </summary>
/// <param name="logger">Injected logger.</param>
public CommandExecutor(ILogger<CommandExecutor> logger = null)
{
_logger = logger;
}
/// <inheritdoc/>
public async Task<CommandResult> ExecuteAsync(string command, string workingDirectory = null, int timeout = -1)
{
var commandId = ++_commandCounter;
using var process = new Process();
var arguments = command.Split(new[] { ' ' }, 2);
process.StartInfo.FileName = arguments[0];
if (arguments.Length > 1)
{
process.StartInfo.Arguments = arguments[1];
}
if (workingDirectory != null)
{
process.StartInfo.WorkingDirectory = workingDirectory;
}
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
var output = new StringBuilder();
var outputCloseEvent = new TaskCompletionSource<bool>();
process.OutputDataReceived += (_, e) =>
{
if (e.Data is null)
{
outputCloseEvent.SetResult(true);
}
else
{
output.AppendLine(e.Data);
}
};
var error = new StringBuilder();
var errorCloseEvent = new TaskCompletionSource<bool>();
process.ErrorDataReceived += (_, e) =>
{
if (e.Data is null)
{
errorCloseEvent.SetResult(true);
}
else
{
error.AppendLine(e.Data);
}
};
_logger?.LogDebug("[{CommandId}] command: {Command}", commandId, command);
try
{
if (!process.Start())
{
_logger?.LogDebug("[{CommandId}] failed to start: {Error}", commandId, "no details available");
throw new Exception($"'{command}' failed to start; no details available");
}
}
catch (Exception ex)
{
_logger?.LogDebug("[{CommandId}] failed to start: {Error}", commandId, ex.Message);
throw new CommandException($"'{command}' failed to start: {ex.Message}", ex);
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// ReSharper disable once AccessToDisposedClosure
var waitForExit = Task.Run(() => process.WaitForExit(timeout));
var processTask = Task.WhenAll(waitForExit, outputCloseEvent.Task, errorCloseEvent.Task);
if (await Task.WhenAny(Task.Delay(timeout), processTask) == processTask && waitForExit.Result)
{
var result = new CommandResult
{
ExitCode = process.ExitCode, Output = output.ToString(), Error = error.ToString(),
};
_logger?.LogDebug("[{CommandId}] exit code: {ExitCode}", commandId, result.ExitCode);
if (result.Output.Length > 0)
{
_logger?.LogDebug("[{CommandId}] stdout:\n{Output}", commandId, result.Output);
}
if (result.Error.Length > 0)
{
_logger?.LogDebug("[{CommandId}] stderr:\n{Error}", commandId, result.Error);
}
return result;
}
try
{
process.Kill();
}
catch
{
// ignore
}
_logger?.LogDebug("[{CommandId}] timed out: {TimeOut}ms", commandId, timeout);
throw new Exception($"'{process.StartInfo.FileName} {process.StartInfo.Arguments}' timed out");
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
namespace Steeltoe.Common.Utils.Diagnostics
{
/// <summary>
/// An simple abstraction of a command result.
/// </summary>
public struct CommandResult
{
/// <summary>
/// Gets the command exit code.
/// </summary>
public int ExitCode { get; init; }
/// <summary>
/// Gets the command exit STDOUT.
/// </summary>
public string Output { get; init; }
/// <summary>
/// Gets the command exit STDERR.
/// </summary>
public string Error { get; init; }
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace Steeltoe.Common.Utils.Diagnostics
{
/// <summary>
/// A utility abstraction to simplify the running of commands.
/// </summary>
public interface ICommandExecutor
{
/// <summary>
/// Execute the command and return the result.
/// </summary>
/// <param name="command">Command to be executed.</param>
/// <param name="workingDirectory">The directory that contains the command process.</param>
/// <param name="timeout">The amount of time in milliseconds to wait for command to complete.</param>
/// <returns>Command result.</returns>
/// <exception cref="CommandException">If a process can not be started for command.</exception>
Task<CommandResult> ExecuteAsync(string command, string workingDirectory = null, int timeout = -1);
}
}

Просмотреть файл

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

Просмотреть файл

@ -4,45 +4,42 @@
using System.IO;
namespace Steeltoe.NetCoreToolService.Utils.IO
namespace Steeltoe.Common.Utils.IO
{
/// <summary>
/// A temp directory.
/// A temporary directory.
/// </summary>
public sealed class TempDirectory : TempPath
public class TempDirectory : TempPath
{
/// <summary>
/// Initializes a new instance of the <see cref="TempDirectory"/> class.
/// </summary>
public TempDirectory()
: this(true)
/// <param name="prefix">Temporary directory prefix.</param>
public TempDirectory(string prefix = null)
: base(prefix)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TempDirectory"/> class.
/// Creates the temporary directory.
/// </summary>
/// <param name="create">If true, create the directory.</param>
public TempDirectory(bool create)
protected override void InitializePath()
{
if (create)
{
Directory.CreateDirectory(FullName);
}
Directory.CreateDirectory(FullPath);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!Directory.Exists(FullName))
if (!Directory.Exists(FullPath))
{
return;
}
try
{
Directory.Delete(FullName, true);
Directory.Delete(FullPath, true);
}
catch
{

Просмотреть файл

@ -4,31 +4,28 @@
using System.IO;
namespace Steeltoe.NetCoreToolService.Utils.IO
namespace Steeltoe.Common.Utils.IO
{
/// <summary>
/// A temp file.
/// A temporary directory.
/// </summary>
public sealed class TempFile : TempPath
public class TempFile : TempPath
{
/// <summary>
/// Initializes a new instance of the <see cref="TempFile"/> class.
/// </summary>
public TempFile()
: this(true)
/// <param name="prefix">Temporary file prefix.</param>
public TempFile(string prefix = null)
: base(prefix)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TempFile"/> class.
/// Creates the temporary file.
/// </summary>
/// <param name="create">If true, create file.</param>
public TempFile(bool create)
protected override void InitializePath()
{
if (create)
{
File.Create(FullName).Dispose();
}
File.Create(FullPath).Dispose();
}
/// <inheritdoc/>
@ -36,14 +33,14 @@ namespace Steeltoe.NetCoreToolService.Utils.IO
{
base.Dispose(disposing);
if (!File.Exists(FullName))
if (!File.Exists(FullPath))
{
return;
}
try
{
File.Delete(FullName);
File.Delete(FullPath);
}
catch
{

Просмотреть файл

@ -4,22 +4,23 @@
using System;
using System.IO;
using System.Reflection;
namespace Steeltoe.NetCoreToolService.Utils.IO
namespace Steeltoe.Common.Utils.IO
{
/// <summary>
/// An abstraction of temp files.
/// An abstraction of a temporary path, such as a file.
/// </summary>
public abstract class TempPath : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="TempPath"/> class.
/// </summary>
protected TempPath()
/// <param name="prefix">Temporary path prefix.</param>
protected TempPath(string prefix = null)
{
Name = Guid.NewGuid().ToString();
FullName = Path.Join(Path.GetTempPath(), Assembly.GetExecutingAssembly().GetName().Name, Name);
Name = $"{prefix ?? string.Empty}{Guid.NewGuid()}";
FullPath = $"{Path.GetTempPath()}{Path.DirectorySeparatorChar}{Name}";
Initialize();
}
/// <summary>
@ -31,17 +32,17 @@ namespace Steeltoe.NetCoreToolService.Utils.IO
}
/// <summary>
/// Gets the full name of this path.
/// Gets the absolute path of the TempPath.
/// </summary>
public string FullName { get; }
public string FullPath { get; }
/// <summary>
/// Gets the name of this path.
/// Gets the name of the TempPath.
/// </summary>
public string Name { get; }
/// <summary>
/// Ensures the underlying temporary path is deleted.
/// Ensures the temporary path is deleted.
/// </summary>
public void Dispose()
{
@ -49,7 +50,7 @@ namespace Steeltoe.NetCoreToolService.Utils.IO
}
/// <summary>
/// Ensures the underlying temporary path is deleted.
/// Ensures the temporary path is deleted.
/// </summary>
/// <param name="disposing">If disposing.</param>
protected virtual void Dispose(bool disposing)
@ -59,5 +60,17 @@ namespace Steeltoe.NetCoreToolService.Utils.IO
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Subclasses should override and perform any path initialization here.
/// </summary>
protected virtual void InitializePath()
{
}
private void Initialize()
{
InitializePath();
}
}
}

Просмотреть файл

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsVersion)" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,58 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Logging;
using Steeltoe.NetCoreToolService.Services;
using System.Collections.Generic;
namespace Steeltoe.NetCoreToolService.Archivers
{
/// <summary>
/// An in-memory <see cref="IArchiverRegistry"/> implementation.
/// </summary>
public class ArchiverRegistry : Service, IArchiverRegistry
{
/* ----------------------------------------------------------------- *
* fields *
* ----------------------------------------------------------------- */
private readonly Dictionary<string, IArchiver> _archivers = new Dictionary<string, IArchiver>();
/* ----------------------------------------------------------------- *
* constructors *
* ----------------------------------------------------------------- */
/// <summary>
/// Initializes a new instance of the <see cref="ArchiverRegistry"/> class.
/// </summary>
/// <param name="logger">Injected logger.</param>
public ArchiverRegistry(ILogger<ArchiverRegistry> logger)
: base(logger)
{
Logger.LogInformation("Initializing archiver registry");
Register(new ZipArchiver());
}
/* ----------------------------------------------------------------- *
* methods *
* ----------------------------------------------------------------- */
/// <inheritdoc />
public void Register(IArchiver archiver)
{
Logger.LogInformation(
"Registering archiver: {Archiver} -> {ArchiverType}",
archiver.Name,
archiver.GetType());
_archivers.Add(archiver.Name, archiver);
}
/// <inheritdoc />
public IArchiver Lookup(string packaging)
{
_archivers.TryGetValue(packaging, out var archiver);
return archiver;
}
}
}

Просмотреть файл

@ -5,13 +5,15 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Steeltoe.NetCoreToolService.Services;
using Steeltoe.NetCoreToolService.Utils.IO;
using Steeltoe.Common.Utils.Diagnostics;
using Steeltoe.Common.Utils.IO;
using Steeltoe.NetCoreToolService.Models;
using Steeltoe.NetCoreToolService.Packagers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Steeltoe.NetCoreToolService.Controllers
@ -20,21 +22,30 @@ namespace Steeltoe.NetCoreToolService.Controllers
/// The controller for "dotnet new".
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Route("api/new")]
public class NewController : ControllerBase
{
private readonly IArchiverRegistry _archiverRegistry;
private const string DefaultOutput = "Sample";
private const string DefaultPackaging = "zip";
private readonly ICommandExecutor _commandExecutor;
private readonly ILogger<NewController> _logger;
private readonly Dictionary<string, IPackager> _packagers = new ()
{
{ "zip", new ZipPackager() },
};
/// <summary>
/// Initializes a new instance of the <see cref="NewController"/> class.
/// </summary>
/// <param name="archiverRegistry">Injected registry of available archivers.</param>
/// <param name="commandExecutor">Injected command.</param>
/// <param name="logger">Injected logger.</param>
public NewController(IArchiverRegistry archiverRegistry, ILogger<NewController> logger)
public NewController(ICommandExecutor commandExecutor, ILogger<NewController> logger = null)
{
_archiverRegistry = archiverRegistry;
_commandExecutor = commandExecutor;
_logger = logger;
}
@ -45,76 +56,7 @@ namespace Steeltoe.NetCoreToolService.Controllers
[HttpGet]
public async Task<ActionResult> GetTemplates()
{
var dict = await GetTemplateDictionary();
return Ok(dict);
}
/// <summary>
/// Gets a generated project for the specified Net Core Tool template.
/// </summary>
/// <param name="template">Template name.</param>
/// <param name="options">Template options.</param>
/// <returns>Project archive.</returns>
[HttpGet]
[Route("{template}")]
public async Task<ActionResult> GetProjectArchiveForTemplate(string template, string options)
{
var opts = options?.Split(',').Select(opt => opt.Trim()).ToList() ?? new List<string>();
var pArgs = new List<string>() { "new", template };
var name = opts.Find(opt => opt.StartsWith("output="))?.Split('=', 2)[1];
if (name is null)
{
name = "Sample";
pArgs.AddRange(new[] { "--output", name });
}
pArgs.AddRange(opts.Select(opt => $"--{opt}"));
using var workDir = new TempDirectory();
var pInfo = new ProcessStartInfo
{
Arguments = string.Join(' ', pArgs),
WorkingDirectory = workDir.FullName,
};
var result = await ProcessToResultAsync(pInfo);
var ok = result as ContentResult;
if (ok is null)
{
return result;
}
if (!Directory.EnumerateFileSystemEntries(workDir.FullName).Any())
{
return NotFound($"template {template} does not exist");
}
var archivalType = "zip";
var archiver = _archiverRegistry.Lookup(archivalType);
if (archiver is null)
{
return NotFound($"Packaging '{archivalType}' not found.");
}
var archiveBytes = archiver.ToBytes(workDir.FullName);
return File(archiveBytes, archiver.MimeType, $"{name}{archiver.FileExtension}");
}
/// <summary>
/// Returns "help" for the specified Net Core Tool template.
/// </summary>
/// <param name="template">Template name.</param>
/// <returns>Template help.</returns>
[HttpGet]
[Route("{template}/help")]
public async Task<ActionResult> GetTemplateHelp(string template)
{
var pInfo = new ProcessStartInfo
{
ArgumentList = { "new", template, "--help" },
};
return await ProcessToResultAsync(pInfo);
return Ok(await GetTemplateDictionary());
}
/// <summary>
@ -122,138 +64,210 @@ namespace Steeltoe.NetCoreToolService.Controllers
/// </summary>
/// <param name="nuGetId">Template NuGet ID.</param>
/// <returns>Information about the installed templates.</returns>
[HttpPost]
public async Task<ActionResult> InstallTemplate(string nuGetId)
[HttpPut("nuget/{nuGetId}")]
public async Task<ActionResult> InstallTemplates(string nuGetId)
{
if (nuGetId is null)
await _commandExecutor.ExecuteAsync($"{NetCoreTool.Command} new --uninstall {nuGetId}");
var oldTemplates = await GetTemplateDictionary();
var installCommand = await _commandExecutor.ExecuteAsync($"{NetCoreTool.Command} new --install {nuGetId}");
const string notFoundError = "error NU1101: ";
if (installCommand.Output.Contains(notFoundError))
{
return BadRequest("missing NuGet ID");
var start = installCommand.Output.IndexOf(notFoundError, StringComparison.Ordinal) +
notFoundError.Length;
var end = installCommand.Output.IndexOf('\n', start);
return BadRequest(installCommand.Output[start..end].Trim());
}
var preInstallTemplates = await GetTemplateDictionary();
var pInfo = new ProcessStartInfo
var newTemplates = await GetTemplateDictionary();
foreach (var oldTemplate in oldTemplates.Keys)
{
ArgumentList = { "new", "--install", nuGetId },
};
await ProcessToStringAsync(pInfo);
var postInstallTemplates = await GetTemplateDictionary();
foreach (var template in preInstallTemplates.Keys)
{
postInstallTemplates.Remove(template);
newTemplates.Remove(oldTemplate);
}
return Ok(postInstallTemplates);
return CreatedAtAction(nameof(InstallTemplates), newTemplates);
}
private async Task<Dictionary<string, TemplateInfo>> GetTemplateDictionary()
/// <summary>
/// Uninstalls the Net Core Tool templates for the specified NuGet ID.
/// </summary>
/// <param name="nuGetId">Template NuGet ID.</param>
/// <returns>Information about the installed templates.</returns>
[HttpDelete("nuget/{nuGetId}")]
public async Task<ActionResult> UninstallTemplates(string nuGetId)
{
var pInfo = new ProcessStartInfo
var oldTemplates = await GetTemplateDictionary();
var uninstallCommand =
await _commandExecutor.ExecuteAsync($"{NetCoreTool.Command} new --uninstall {nuGetId}");
if (uninstallCommand.Output.Contains($"Could not find something to uninstall"))
{
ArgumentList = { "new", "--list" },
};
var listing = await ProcessToStringAsync(pInfo);
var lines = listing.Split('\n').ToList().FindAll(line => !string.IsNullOrWhiteSpace(line));
return NotFound($"No templates with NuGet ID '{nuGetId}' installed.");
}
var newTemplates = await GetTemplateDictionary();
foreach (var newTemplate in newTemplates.Keys)
{
oldTemplates.Remove(newTemplate);
}
return Ok(oldTemplates);
}
/// <summary>
/// Returns "help" for the specified Net Core Tool template.
/// </summary>
/// <param name="template">Template name.</param>
/// <returns>Template help.</returns>
[HttpGet("{template}/help")]
public async Task<ActionResult> GetTemplateHelp(string template)
{
var helpCommand = await _commandExecutor.ExecuteAsync($"{NetCoreTool.Command} new {template} --help");
if (helpCommand.ExitCode != 0)
{
var start = helpCommand.Error.IndexOf("No templates found", StringComparison.Ordinal);
var end = helpCommand.Error.IndexOf('\n', start);
return NotFound(helpCommand.Error[start..end].Trim());
}
return Ok(helpCommand.Output.Trim());
}
/// <summary>
/// Gets a generated project for the specified Net Core Tool template.
/// </summary>
/// <param name="template">Template name.</param>
/// <param name="options">Template options.</param>
/// <param name="packaging">Project packaging, e.g. zip.</param>
/// <returns>Project archive.</returns>
[HttpGet]
[Route("{template}")]
public async Task<ActionResult> GetTemplateProject(
string template,
string options = null,
string packaging = DefaultPackaging)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
try
{
var output = DefaultOutput;
var optionList = new List<string>();
if (options is not null)
{
foreach (var option in options.Split(','))
{
if (option.Contains('='))
{
var nvp = option.Split('=', 2);
if (nvp[0].Equals("output"))
{
output = nvp[1];
continue;
}
}
optionList.Add($"--{option}");
}
}
if (!_packagers.TryGetValue(packaging, out var packager))
{
return BadRequest($"Unknown or unsupported packaging '{packaging}'.");
}
using var projectDir = new TempDirectory("NetCoreToolService-");
var commandLine = new StringBuilder();
commandLine.Append(NetCoreTool.Command).Append(" new ").Append(template);
commandLine.Append(" --output=").Append(output);
foreach (var option in optionList)
{
commandLine.Append(' ').Append(option);
}
var newCommand =
await _commandExecutor.ExecuteAsync(commandLine.ToString(), projectDir.FullPath);
const string unknownTemplateError = "No templates found";
if (newCommand.Error.Contains(unknownTemplateError))
{
return NotFound($"Template '{template}' not found.");
}
const string invalidSwitchError = "Invalid input switch:";
if (newCommand.Error.Contains(invalidSwitchError))
{
var start = newCommand.Error.IndexOf(invalidSwitchError, StringComparison.Ordinal) +
invalidSwitchError.Length;
start = newCommand.Error.IndexOf("--", start, StringComparison.Ordinal) + "--".Length;
var end = newCommand.Error.IndexOf('\n', start);
return NotFound($"Switch '{newCommand.Error[start..end]}' not found.");
}
const string invalidParameterError = "Error: Invalid parameter(s):";
if (newCommand.Error.Contains(invalidParameterError))
{
var start = newCommand.Error.IndexOf(invalidParameterError, StringComparison.Ordinal) +
invalidParameterError.Length;
start = newCommand.Error.IndexOf("--", start, StringComparison.Ordinal) + "--".Length;
var end = newCommand.Error.IndexOf('\n', start);
var nvp = newCommand.Error[start..end].Split(' ', 2);
return NotFound($"Option '{nvp[0]}' parameter '{nvp[1]}' not found.");
}
if (newCommand.ExitCode != 0)
{
return StatusCode(StatusCodes.Status500InternalServerError, newCommand.Error.Trim());
}
if (!newCommand.Output.Contains(" was created successfully."))
{
return StatusCode(StatusCodes.Status500InternalServerError, newCommand.Output.Trim());
}
var package = packager.ToBytes(projectDir.FullPath);
return File(package, packager.MimeType, $"{output}{packager.FileExtension}");
}
finally
{
stopwatch.Stop();
_logger?.LogDebug("Generated project in {Elapsed:m\\:s\\.fff}", stopwatch.Elapsed);
}
}
private async Task<TemplateDictionary> GetTemplateDictionary()
{
var listCommand = await _commandExecutor.ExecuteAsync($"{NetCoreTool.Command} new --list");
var lines = listCommand.Output.Split('\n').ToList()
.FindAll(line => !string.IsNullOrWhiteSpace(line));
var headingIdx = lines.FindIndex(line => line.StartsWith("-"));
var headings = lines[headingIdx].Split(" ");
var nameColLength = headings[0].Length;
var shortNameColStart = nameColLength + 2;
var shortNameColLength = headings[1].Length;
var languageColStart = shortNameColStart + shortNameColLength + 2;
var languageColLength = headings[2].Length;
var tagsColStart = languageColStart + languageColLength + 2;
var tagsColLength = headings[3].Length;
const int nameColStart = 0;
var nameColEnd = nameColStart + headings[0].Length;
var shortNameColStart = nameColEnd + 2;
var shortNameColEnd = shortNameColStart + headings[1].Length;
var languageColStart = shortNameColEnd + 2;
var languageColEnd = languageColStart + headings[2].Length;
var tagsColStart = languageColEnd + 2;
var tagsColEnd = tagsColStart + headings[3].Length;
lines = lines.GetRange(headingIdx + 1, lines.Count - headingIdx - 1);
var dict = new Dictionary<string, TemplateInfo>();
var dict = new TemplateDictionary();
foreach (var line in lines)
{
var templateInfo = new TemplateInfo();
var template = line.Substring(shortNameColStart, shortNameColLength).Trim();
templateInfo.Name = line.Substring(0, nameColLength).Trim();
templateInfo.Languages = line.Substring(languageColStart, languageColLength).Trim();
templateInfo.Tags = line.Substring(tagsColStart, tagsColLength).Trim();
var template = line[shortNameColStart..shortNameColEnd].Trim();
var templateInfo = new TemplateInfo
{
Name = line[nameColStart..nameColEnd].Trim(),
Languages = line[languageColStart..languageColEnd].Trim(),
Tags = line[tagsColStart.. Math.Min(tagsColEnd, line.Length)].Trim(),
};
dict.Add(template, templateInfo);
}
return dict;
}
private async Task<string> ProcessToStringAsync(ProcessStartInfo processStartInfo)
{
processStartInfo.FileName = NetCoreTool.Command;
TempDirectory workDir = null;
if (string.IsNullOrEmpty(processStartInfo.WorkingDirectory))
{
workDir = new TempDirectory();
processStartInfo.WorkingDirectory = workDir.FullName;
}
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
var guid = Path.GetFileName(processStartInfo.WorkingDirectory) ?? "unknown";
_logger.LogInformation(
"{Guid}: {Command} {Args}",
guid,
processStartInfo.FileName,
processStartInfo.Arguments);
var proc = Process.Start(processStartInfo);
if (proc is null)
{
throw new ActionResultException(StatusCode(StatusCodes.Status503ServiceUnavailable));
}
await proc.WaitForExitAsync();
workDir?.Dispose();
if (proc.ExitCode == 0)
{
var output = await proc.StandardOutput.ReadToEndAsync();
_logger.LogInformation("{Guid}>\n{Output}", guid, output);
return output;
}
var error = await proc.StandardError.ReadToEndAsync();
_logger.LogInformation("{Guid}: {Error}", guid, error);
throw new ActionResultException(NotFound(error));
}
private async Task<ActionResult> ProcessToResultAsync(ProcessStartInfo processStartInfo)
{
try
{
return Content(await ProcessToStringAsync(processStartInfo));
}
catch (ActionResultException e)
{
return e.ActionResult;
}
}
}
internal class TemplateInfo
{
public string Name { get; set; }
public string Languages { get; set; }
public string Tags { get; set; }
public override string ToString()
{
return $"[name={Name},languages={Languages},tags={Tags}";
}
}
internal class ActionResultException : Exception
{
internal ActionResultException(ActionResult actionResult)
{
ActionResult = actionResult;
}
internal ActionResult ActionResult { get; }
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace Steeltoe.NetCoreToolService.Models
{
/// <summary>
/// A convenience dictionary for template information.
/// </summary>
public class TemplateDictionary : Dictionary<string, TemplateInfo>
{
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
namespace Steeltoe.NetCoreToolService.Models
{
/// <summary>
/// Contains information about a Net Core Tool template.
/// </summary>
public class TemplateInfo
{
/// <summary>
/// Gets the name of the template.
/// </summary>
public string Name { get; internal init; }
/// <summary>
/// Gets the supported languages of the template.
/// </summary>
public string Languages { get; internal init; }
/// <summary>
/// Gets the template tags.
/// </summary>
public string Tags { get; internal init; }
/// <inheritdoc/>
public override string ToString()
{
return $"[name={Name},languages={Languages},tags={Tags}";
}
}
}

Просмотреть файл

@ -2,33 +2,33 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
namespace Steeltoe.NetCoreToolService.Services
namespace Steeltoe.NetCoreToolService.Packagers
{
/// <summary>
/// Contract for archiver implementations.
/// Contract for packager implementations.
/// </summary>
public interface IArchiver
public interface IPackager
{
/// <summary>
/// Gets the name for the archiver, e.g. <c>"zip"</c>.
/// Gets the name for the packager, e.g. <c>"zip"</c>.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the file extension for the archive, e.g. <c>".zip"</c>.
/// Gets the file extension for package, e.g. <c>".zip"</c>.
/// </summary>
string FileExtension { get; }
/// <summary>
/// Gets the mime type for the archive, e.g. <c>"application/zip"</c>.
/// Gets the mime type for a package, e.g. <c>"application/zip"</c>.
/// </summary>
string MimeType { get; }
/// <summary>
/// Returns an archive of the files rooted at <c>path</c> as a byte array.
/// Returns a package of the files rooted at <c>path</c> as a byte array.
/// </summary>
/// <param name="path">Path to be archived.</param>
/// <returns>A byte array containing the archive.</returns>
/// <param name="path">Path to be packaged.</param>
/// <returns>A byte array containing the package.</returns>
byte[] ToBytes(string path);
}
}

Просмотреть файл

@ -2,17 +2,16 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using Steeltoe.NetCoreToolService.Services;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
namespace Steeltoe.NetCoreToolService.Archivers
namespace Steeltoe.NetCoreToolService.Packagers
{
/// <summary>
/// An <see cref="IArchiver"/> implementation using the ZIP archive file format.
/// An <see cref="IPackager"/> implementation using the ZIP archive file format.
/// </summary>
public class ZipArchiver : IArchiver
public class ZipPackager : IPackager
{
/* ----------------------------------------------------------------- *
* fields *
@ -35,10 +34,10 @@ namespace Steeltoe.NetCoreToolService.Archivers
* ----------------------------------------------------------------- */
/// <summary>
/// Initializes a new instance of the <see cref="ZipArchiver"/> class.
/// Initializes a new instance of the <see cref="ZipPackager"/> class.
/// </summary>
/// <param name="compression">Compression level default <see cref="CompressionLevel.Fastest"/>.</param>
public ZipArchiver(CompressionLevel compression = CompressionLevel.Fastest)
public ZipPackager(CompressionLevel compression = CompressionLevel.Fastest)
{
_compression = compression;
}

Просмотреть файл

@ -1,25 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
namespace Steeltoe.NetCoreToolService.Services
{
/// <summary>
/// Contract for archiver registry implementations.
/// </summary>
public interface IArchiverRegistry
{
/// <summary>
/// Registers the archiver.
/// </summary>
/// <param name="value">The archiver to register.</param>
void Register(IArchiver value);
/// <summary>
/// Look for an archiver with the specified name.
/// </summary>
/// <param name="name">The name of the archiver to loookup.</param>
/// <returns>The named archiver,or <c>null</c> if no value found.</returns>
IArchiver Lookup(string name);
}
}

Просмотреть файл

@ -1,28 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Logging;
namespace Steeltoe.NetCoreToolService.Services
{
/// <summary>
/// Base class for services.
/// </summary>
public abstract class Service
{
/// <summary>
/// Initializes a new instance of the <see cref="Service"/> class.
/// </summary>
/// <param name="logger">Injected logger.</param>
protected Service(ILogger logger)
{
Logger = logger;
}
/// <summary>
/// Gets the logger.
/// </summary>
protected ILogger Logger { get; }
}
}

Просмотреть файл

@ -8,8 +8,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Steeltoe.NetCoreToolService.Archivers;
using Steeltoe.NetCoreToolService.Services;
using Steeltoe.Common.Utils.Diagnostics;
namespace Steeltoe.NetCoreToolService
{
@ -43,7 +42,7 @@ namespace Steeltoe.NetCoreToolService
options.JsonSerializerOptions.IgnoreNullValues = true;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
services.AddSingleton<IArchiverRegistry, ArchiverRegistry>();
services.AddTransient<ICommandExecutor, CommandExecutor>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Steeltoe.NetCoreToolService", Version = "v0" });

Просмотреть файл

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<RootNamespace>Steeltoe.NetCoreToolService</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(SwashbuckleVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common.Utils\Steeltoe.Common.Utils.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -4,6 +4,7 @@
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Steeltoe.Common.Utils": "Debug",
"Steeltoe.NetCoreToolService": "Debug"
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using Steeltoe.Common.Utils.Diagnostics;
using Xunit;
namespace Steeltoe.Common.Utils.Test.Diagnostics
{
public class CommandExecutorTest
{
private readonly CommandExecutor _commandExecutor = new();
[Fact]
public async void SuccessfulCommandShouldReturn0()
{
var result = await _commandExecutor.ExecuteAsync("dotnet --help");
Assert.Equal(0, result.ExitCode);
Assert.Contains("Usage: dotnet", result.Output);
}
[Fact]
public async void UnsuccessfulCommandShouldNotReturn0()
{
var result = await _commandExecutor.ExecuteAsync("dotnet --no-such-option");
Assert.NotEqual(0, result.ExitCode);
Assert.Contains("Unknown option: --no-such-option", result.Error);
}
[Fact]
public async void UnknownCommandShouldThrowException()
{
Task Act() => _commandExecutor.ExecuteAsync("no-such-command");
var exc = await Assert.ThrowsAsync<CommandException>(Act);
Assert.Contains("'no-such-command' failed to start", exc.Message);
Assert.NotNull(exc.InnerException);
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0;</TargetFrameworks>
</PropertyGroup>
<Import Project="..\..\..\..\versions.props" />
<Import Project="..\..\..\..\sharedtest.props" />
<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Common.IO\Steeltoe.Common.IO.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using Steeltoe.Common.Utils.IO;
using System.IO;
using Xunit;
namespace Steeltoe.Common.Utils.Test.IO
{
public class TempDirectoryTest
{
[Fact]
public void TempDirectoryRemovesItself()
{
var tempDir = new TempDirectory();
Assert.True(Directory.Exists(tempDir.FullPath));
File.Create(Path.Join(tempDir.FullPath, "foo")).Dispose();
tempDir.Dispose();
Assert.False(Directory.Exists(tempDir.FullPath));
}
[Fact]
public void TempDirectoryCanBePrefixed()
{
const string prefix = "XXX-";
using var tempDir = new TempDirectory(prefix);
Assert.StartsWith(prefix, tempDir.Name);
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using Steeltoe.Common.Utils.IO;
using System.IO;
using Xunit;
namespace Steeltoe.Common.Utils.Test.IO
{
public class TempFileTest
{
[Fact]
public void TempFileRemovesItself()
{
var tempFile = new TempFile();
Assert.True(File.Exists(tempFile.FullPath));
tempFile.Dispose();
Assert.False(File.Exists(tempFile.FullPath));
}
[Fact]
public void TempFileCanBePrefixed()
{
const string prefix = "XXX-";
using var tempFile = new TempFile(prefix);
Assert.StartsWith(prefix, tempFile.Name);
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Common.Utils\Steeltoe.Common.Utils.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -3,15 +3,21 @@
<Import Project="..\Directory.Build.props" />
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="1.3.*" />
<PackageReference Include="coverlet.msbuild" Version="2.9.*" />
<PackageReference Include="FluentAssertions" Version="5.10.*" />
<PackageReference Include="FluentAssertions.Json" Version="5.5.*" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.*" />
<PackageReference Include="Moq" Version="4.14.*" />
<PackageReference Include="xunit" Version="2.4.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.*" />
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="$(CoverletMsBuildVersion)" />
<PackageReference Include="FluentAssertions" Version="$(FluentAssertionsVersion)" />
<PackageReference Include="FluentAssertions.Json" Version="$(FluentAssertionsJsonVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

Просмотреть файл

@ -1,49 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Steeltoe.NetCoreToolService.Archivers;
using Xunit;
namespace Steeltoe.NetCoreToolService.Test.Archivers
{
public class ArchiverRegistryTests
{
/* ----------------------------------------------------------------- *
* positive tests *
* ----------------------------------------------------------------- */
[Fact]
public void Zip_Format_Should_Return_ZipArchiver()
{
// Arrange
var registry = new ArchiverRegistry(new NullLogger<ArchiverRegistry>());
// Act
var archiver = registry.Lookup("zip");
// Assert
archiver.Should().NotBeNull();
archiver.Should().BeOfType<ZipArchiver>();
}
/* ----------------------------------------------------------------- *
* negative tests *
* ----------------------------------------------------------------- */
[Fact]
public void Unknown_Format_Should_Return_Null()
{
// Arrange
var registry = new ArchiverRegistry(new NullLogger<ArchiverRegistry>());
// Act
var archiver = registry.Lookup("unknown");
// Assert
archiver.Should().BeNull();
}
}
}

Просмотреть файл

@ -0,0 +1,553 @@
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Steeltoe.Common.Utils.Diagnostics;
using Steeltoe.NetCoreToolService.Controllers;
using Steeltoe.NetCoreToolService.Models;
using Xunit;
namespace Steeltoe.NetCoreToolService.Test.Controllers
{
public class NewControllerTest
{
/* ----------------------------------------------------------------- *
* positive tests *
* ----------------------------------------------------------------- */
[Fact]
public async Task GetTemplates_Should_Return_AllTemplates()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(expression: c => c.ExecuteAsync($"{NetCoreTool.Command} new --list", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ---------
My Template myt lang tags
My Other Template myot otherlang othertags
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplates();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var templates = Assert.IsType<TemplateDictionary>(okResult.Value);
templates.Count.Should().Be(2);
templates.Keys.Should().Contain("myt");
templates["myt"].Name.Should().Be("My Template");
templates["myt"].Languages.Should().Be("lang");
templates["myt"].Tags.Should().Be("tags");
templates.Keys.Should().Contain("myot");
templates["myot"].Name.Should().Be("My Other Template");
templates["myot"].Languages.Should().Be("otherlang");
templates["myot"].Tags.Should().Be("othertags");
}
[Fact]
public async Task InstallTemplates_Should_Return_InstalledTemplates()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.SetupSequence(c => c.ExecuteAsync($"{NetCoreTool.Command} new --list", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ---------
My Template myt lang tags
My Other Template myot otherlang othertags
",
}
)
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ----------
A New Template ant smalltalk newstuff
My Template myt lang tags
My Other Template myot otherlang othertags
Other New Template ont bigtalk otherstuff
",
}
);
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new --install My.Templates", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.InstallTemplates("My.Templates");
// Assert
var createdResult = Assert.IsType<CreatedAtActionResult>(result);
var templates = Assert.IsType<TemplateDictionary>(createdResult.Value);
templates.Count.Should().Be(2);
templates["ant"].Name.Should().Be("A New Template");
templates["ant"].Languages.Should().Be("smalltalk");
templates["ant"].Tags.Should().Be("newstuff");
templates["ont"].Name.Should().Be("Other New Template");
templates["ont"].Languages.Should().Be("bigtalk");
templates["ont"].Tags.Should().Be("otherstuff");
}
[Fact]
public async Task UninstallTemplates_Should_Return_UninstalledTemplates()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.SetupSequence(c => c.ExecuteAsync($"{NetCoreTool.Command} new --list", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ----------
A New Template ant smalltalk newstuff
My Template myt lang tags
My Other Template myot otherlang othertags
Other New Template ont bigtalk otherstuff
",
}
)
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ---------
My Template myt lang tags
My Other Template myot otherlang othertags
",
}
);
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new --uninstall My.Templates", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.UninstallTemplates("My.Templates");
// Assert
var ok = Assert.IsType<OkObjectResult>(result);
var templates = Assert.IsType<TemplateDictionary>(ok.Value);
templates.Count.Should().Be(2);
templates["ant"].Name.Should().Be("A New Template");
templates["ant"].Languages.Should().Be("smalltalk");
templates["ant"].Tags.Should().Be("newstuff");
templates["ont"].Name.Should().Be("Other New Template");
templates["ont"].Languages.Should().Be("bigtalk");
templates["ont"].Tags.Should().Be("otherstuff");
}
[Fact]
public async Task GetTemplateHelp_Should_Return_Help()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --help", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
Some helpful tips for you
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateHelp("mytemplate");
// Assert
var ok = Assert.IsType<OkObjectResult>(result);
var help = Assert.IsType<string>(ok.Value);
help.Should().Be("Some helpful tips for you");
}
[Fact]
public async Task GetTemplateProject_Should_Return_ProjectPackage()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Sample",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
The template ""mytemplate"" was created successfully.
",
Error = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate");
// Assert
Assert.IsType<FileContentResult>(result);
}
[Fact]
public async Task GetTemplateProject_Should_Use_Defaults()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Sample",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
The template ""mytemplate"" was created successfully.
",
Error = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate");
// Assert
var file = Assert.IsType<FileContentResult>(result);
file.ContentType.Should().Be("application/zip");
file.FileDownloadName.Should().Be("Sample.zip");
}
[Fact]
public async Task GetTemplateProject_Can_Specify_Output()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Joe",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
The template ""mytemplate"" was created successfully.
",
Error = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate", "output=Joe");
// Assert
var file = Assert.IsType<FileContentResult>(result);
file.FileDownloadName.Should().StartWith("Joe.");
}
[Fact]
public async Task GetTemplateProject_Can_Specify_ZipPackaging()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Sample",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
The template ""mytemplate"" was created successfully.
",
Error = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate", packaging: "zip");
// Assert
var file = Assert.IsType<FileContentResult>(result);
var _ = new ZipArchive(new MemoryStream(file.FileContents));
}
/* ----------------------------------------------------------------- *
* negative tests *
* ----------------------------------------------------------------- */
[Fact]
public async Task InstallTemplates_UnknownNuGet_Should_Return_BadRequest()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new --list", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ---------
My Template myt lang tags
My Other Template myot otherlang othertags
",
}
);
executor.Setup(c =>
c.ExecuteAsync($"{NetCoreTool.Command} new --install No.Such.Template", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 2,
Output = @"
... error NU1101: Unable to find package No.Such.Template. No packages exist with this id in source(s): myget.org, nuget.org
Failed to restore ...
",
});
var controller = new NewController(executor.Object);
// Act
var result = await controller.InstallTemplates("No.Such.Template");
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
badRequest.Value.Should()
.Be(
"Unable to find package No.Such.Template. No packages exist with this id in source(s): myget.org, nuget.org");
}
[Fact]
public async Task UninstallTemplates_UnknownNuGet_Should_Return_NotFound()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new --list", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
------------------- -------- --------- ---------
",
}
);
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new --uninstall My.Templates", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
Could not find something to uninstall called 'My.Templates'.
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.UninstallTemplates("My.Templates");
// Assert
var notFound = Assert.IsType<NotFoundObjectResult>(result);
notFound.Value.Should().Be("No templates with NuGet ID 'My.Templates' installed.");
}
[Fact]
public async Task GetTemplateHelp_UnknownTemplate_Should_Return_NotFound()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync($"{NetCoreTool.Command} new nosuchtemplate --help", null, -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 6,
Error = @"
No templates found matching: 'nosuchtemplate'.
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateHelp("nosuchtemplate");
// Assert
var notFound = Assert.IsType<NotFoundObjectResult>(result);
notFound.Value.Should().Be("No templates found matching: 'nosuchtemplate'.");
}
[Fact]
public async Task GetTemplateProject_UnknownTemplate_Should_Return_NotFound()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c =>
c.ExecuteAsync($"{NetCoreTool.Command} new nosuchtemplate --output=Sample", It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 14,
Error = @"
No templates found matching: 'nosuchtemplate'.
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("nosuchtemplate");
// Assert
var notFound = Assert.IsType<NotFoundObjectResult>(result);
notFound.Value.Should().Be("Template 'nosuchtemplate' not found.");
}
[Fact]
public async Task GetTemplateProject_UnknownSwitch_Should_Return_NotFound()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c =>
c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Sample --unknown-switch",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 5,
Error = @"
Invalid input switch:
--unknown-switch
For a list of valid options, run 'dotnet new webapi --help'.
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate", "unknown-switch");
// Assert
var notFound = Assert.IsType<NotFoundObjectResult>(result);
notFound.Value.Should().Be("Switch 'unknown-switch' not found.");
}
[Fact]
public async Task GetTemplateProject_UnknownParameter_Should_Return_NotFound()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c =>
c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Sample --myoption=unknown",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = "",
Error = @"
Error: Invalid parameter(s):
--myoption unknown
'unknown' is not a valid value for --myoption
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate", "myoption=unknown");
// Assert
var notFound = Assert.IsType<NotFoundObjectResult>(result);
notFound.Value.Should().Be("Option 'myoption' parameter 'unknown' not found.");
}
[Fact]
public async Task GetTemplateProject_UnknownPackaging_Should_Return_BadRequest()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c =>
c.ExecuteAsync($"{NetCoreTool.Command} new mytemplate --output=Sample --myoption=unknown",
It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = "",
Error = @"
Error: Invalid parameter(s):
--myoption unknown
'unknown' is not a valid value for --myoption
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate", packaging: "acme-packaging");
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
badRequest.Value.Should().Be("Unknown or unsupported packaging 'acme-packaging'.");
}
[Fact]
public async Task GetTemplateProject_UnknownError_Should_Return_InternalServerError()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync(It.IsAny<string>(), It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 1,
Output = "",
Error = @"
Something bad happened.
",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate");
// Assert
var internalServerError = Assert.IsType<ObjectResult>(result);
internalServerError.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
internalServerError.Value.Should().Be("Something bad happened.");
}
[Fact]
public async Task GetTemplateProject_UnknownOutput_Should_Return_InternalServerError()
{
// Arrange
var executor = new Mock<ICommandExecutor>();
executor.Setup(c => c.ExecuteAsync(It.IsAny<string>(), It.IsAny<string>(), -1))
.ReturnsAsync(new CommandResult
{
ExitCode = 0,
Output = @"
Unexpected output.
",
Error = "",
}
);
var controller = new NewController(executor.Object);
// Act
var result = await controller.GetTemplateProject("mytemplate");
// Assert
var internalServerError = Assert.IsType<ObjectResult>(result);
internalServerError.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
internalServerError.Value.Should().Be("Unexpected output.");
}
}
}

Просмотреть файл

@ -5,13 +5,13 @@
using System.IO;
using System.IO.Compression;
using FluentAssertions;
using Steeltoe.NetCoreToolService.Archivers;
using Steeltoe.NetCoreToolService.Utils.IO;
using Steeltoe.Common.Utils.IO;
using Steeltoe.NetCoreToolService.Packagers;
using Xunit;
namespace Steeltoe.NetCoreToolService.Test.Archivers
namespace Steeltoe.NetCoreToolService.Test.Packagers
{
public class ZipArchiverTests
public class ZipPackagerTests
{
/* ----------------------------------------------------------------- *
* positive tests *
@ -21,11 +21,11 @@ namespace Steeltoe.NetCoreToolService.Test.Archivers
public void ToStream_Should_Create_Zip_Archive()
{
// Arrange
var archiver = new ZipArchiver();
var archiver = new ZipPackager();
var tempDir = new TempDirectory();
// Act
var buf = archiver.ToBytes(tempDir.FullName);
var buf = archiver.ToBytes(tempDir.FullPath);
// Assert
new ZipArchive(new MemoryStream(buf)).Should().BeOfType<ZipArchive>();
@ -35,15 +35,15 @@ namespace Steeltoe.NetCoreToolService.Test.Archivers
public void ToStream_Should_Archive_File_Contents()
{
// Arrange
var archiver = new ZipArchiver();
var archiver = new ZipPackager();
using var tempDir = new TempDirectory();
var d1 = Path.Join(tempDir.FullName, "d1");
var d1 = Path.Join(tempDir.FullPath, "d1");
Directory.CreateDirectory(d1);
var f1 = Path.Join(d1, "f1");
File.WriteAllText(f1, "f1 stuff");
// Act
var buf = archiver.ToBytes(tempDir.FullName);
var buf = archiver.ToBytes(tempDir.FullPath);
// Assert
var zip = new ZipArchive(new MemoryStream(buf));
@ -64,15 +64,15 @@ namespace Steeltoe.NetCoreToolService.Test.Archivers
public void ToStream_Should_Archive_Directories()
{
// Arrange
var archiver = new ZipArchiver();
var archiver = new ZipPackager();
using var tempDir = new TempDirectory();
var d1 = Path.Join(tempDir.FullName, "d1");
var d1 = Path.Join(tempDir.FullPath, "d1");
Directory.CreateDirectory(d1);
var d2 = Path.Join(d1, "d2");
Directory.CreateDirectory(d2);
// Act
var buf = archiver.ToBytes(tempDir.FullName);
var buf = archiver.ToBytes(tempDir.FullPath);
// Assert
var zip = new ZipArchive(new MemoryStream(buf));
@ -92,7 +92,7 @@ namespace Steeltoe.NetCoreToolService.Test.Archivers
public void GetPackaging_Should_Be_application_zip()
{
// Arrange
var archiver = new ZipArchiver();
var archiver = new ZipPackager();
// Act
var packaging = archiver.Name;
@ -105,7 +105,7 @@ namespace Steeltoe.NetCoreToolService.Test.Archivers
public void GetFileExtension_Should_Be_zip()
{
// Arrange
var archiver = new ZipArchiver();
var archiver = new ZipPackager();
// Act
var ext = archiver.FileExtension;
@ -118,7 +118,7 @@ namespace Steeltoe.NetCoreToolService.Test.Archivers
public void MimeType_Should_Be_application_zip()
{
// Arrange
var archiver = new ZipArchiver();
var archiver = new ZipPackager();
// Act
var ext = archiver.MimeType;

Просмотреть файл

@ -1,9 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Steeltoe.NetCoreToolService.Test</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Content Include="..\Directory.Build.props">
<Link>Directory.Build.props</Link>
@ -11,7 +7,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\NetCoreToolService\NetCoreToolService.csproj" />
<ProjectReference Include="..\..\src\NetCoreToolService\Steeltoe.NetCoreToolService.csproj" />
</ItemGroup>
</Project>