diff --git a/.gitignore b/.gitignore index 758b4e3..dd3e691 100644 --- a/.gitignore +++ b/.gitignore @@ -206,6 +206,7 @@ PublishScripts/ # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets +dotnet-tools.json # Microsoft Azure Build Output csx/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..2451c08 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Saunter.sln b/Saunter.sln index 10d433b..85c435a 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{E0D34C77-9 .editorconfig = .editorconfig .gitattributes = .gitattributes CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props README.md = README.md EndProjectSection EndProject @@ -45,7 +46,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "npm", "npm", "{E8FACA22-CFE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncAPI.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs index aa9852b..6a1709f 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs @@ -3,12 +3,17 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Saunter.Serialization; using Saunter; +using Saunter.Serialization; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class AsyncApiDocumentExtractor(ILogger logger) +internal interface IAsyncApiDocumentExtractor +{ + IEnumerable<(string name, AsyncApiDocument document)> GetAsyncApiDocument(IServiceProvider serviceProvider, string[] requestedDocuments); +} + +internal class AsyncApiDocumentExtractor(ILogger logger) : IAsyncApiDocumentExtractor { private IEnumerable GetDocumentNames(string[] requestedDocuments, AsyncApiOptions asyncApiOptions) { diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs index 040637a..2ae66e7 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs @@ -2,26 +2,27 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class EnvironmentBuilder(ILogger logger) +internal interface IEnvironmentBuilder +{ + void SetEnvironmentVariables(string env); +} + +internal class EnvironmentBuilder(ILogger logger) : IEnvironmentBuilder { public void SetEnvironmentVariables(string env) { var envVars = !string.IsNullOrWhiteSpace(env) ? env.Split(',').Select(x => x.Trim()) : Array.Empty(); - foreach (var envVar in envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList())) + var keyValues = envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList()); + foreach (var envVar in keyValues) { - if (envVar.Count is 1) - { - Environment.SetEnvironmentVariable(envVar[0], null, EnvironmentVariableTarget.Process); - logger.LogDebug($"Set environment flag: {envVar[0]}"); - } - if (envVar.Count is 2) + if (envVar.Count == 2 && !string.IsNullOrWhiteSpace(envVar[0])) { - Environment.SetEnvironmentVariable(envVar[0], envVar.ElementAtOrDefault(1), EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(envVar[0], envVar[1], EnvironmentVariableTarget.Process); logger.LogDebug($"Set environment variable: {envVar[0]} = {envVar[1]}"); } else { - logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2,env3"); + logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2"); } } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs index 62e1bc4..d58060e 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs @@ -2,7 +2,12 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class FileWriter(ILogger logger) +internal interface IFileWriter +{ + void Write(string outputPath, string fileTemplate, string documentName, string format, Action streamWriter); +} + +internal class FileWriter(IStreamProvider streamProvider, ILogger logger) : IFileWriter { public void Write(string outputPath, string fileTemplate, string documentName, string format, Action streamWriter) { @@ -12,12 +17,12 @@ public void Write(string outputPath, string fileTemplate, string documentName, s private void WriteFile(string outputPath, Action writeAction) { - using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + using var stream = streamProvider.GetStreamFor(outputPath); writeAction(stream); if (outputPath != null) { - logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}"); + logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath).TrimStart('.')} successfully written to {outputPath}"); } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs index 041e496..33201f1 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs @@ -6,10 +6,11 @@ internal static class ServiceExtensions { public static IServiceCollection AddToFileCommand(this IServiceCollection services) { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); return services; } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs index 5c3c6a6..69bde4f 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs @@ -4,7 +4,12 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class ServiceProviderBuilder(ILogger logger) +internal interface IServiceProviderBuilder +{ + IServiceProvider BuildServiceProvider(string startupAssembly); +} + +internal class ServiceProviderBuilder(ILogger logger) : IServiceProviderBuilder { public IServiceProvider BuildServiceProvider(string startupAssembly) { @@ -17,4 +22,3 @@ public IServiceProvider BuildServiceProvider(string startupAssembly) return serviceProvider; } } - diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs new file mode 100644 index 0000000..8af5dbd --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -0,0 +1,19 @@ +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal interface IStreamProvider +{ + Stream GetStreamFor(string path); +} + +internal class StreamProvider : IStreamProvider +{ + public Stream GetStreamFor(string path) + { + if (!string.IsNullOrEmpty(path)) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } + + return path != null ? File.Create(path) : Console.OpenStandardOutput(); + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs index 98bebc5..0a62c43 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs @@ -5,7 +5,7 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class ToFileCommand(ILogger logger, EnvironmentBuilder environment, ServiceProviderBuilder builder, AsyncApiDocumentExtractor docExtractor, FileWriter fileWriter) +internal class ToFileCommand(ILogger logger, IEnvironmentBuilder environment, IServiceProviderBuilder builder, IAsyncApiDocumentExtractor docExtractor, IFileWriter fileWriter) { private const string DEFAULT_FILENAME = "{document}_asyncapi.{extension}"; @@ -37,11 +37,7 @@ public int ToFile([Argument] string startupassembly, string output = "./", strin foreach (var (documentName, asyncApiDocument) in documents) { // Serialize to specified output location or stdout - var outputPath = !string.IsNullOrWhiteSpace(output) ? Path.Combine(Directory.GetCurrentDirectory(), output) : null; - if (!string.IsNullOrEmpty(outputPath)) - { - Directory.CreateDirectory(outputPath); - } + var outputPath = !string.IsNullOrWhiteSpace(output) ? Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), output)) : null; var exportJson = true; var exportYml = false; diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 355fdd5..6b89cdb 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,11 +9,17 @@ + - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs new file mode 100644 index 0000000..0134ce6 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests; + +public class E2ETests(ITestOutputHelper output) +{ + private string Run(string file, string args, string workingDirectory, int expectedExitCode = 0) + { + var process = Process.Start(new ProcessStartInfo(file) + { + Arguments = args, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + process.WaitForExit(TimeSpan.FromSeconds(20)); + var stdOut = process.StandardOutput.ReadToEnd().Trim(); + var stdError = process.StandardError.ReadToEnd().Trim(); + output.WriteLine($"### Output of \"{file} {args}\""); + output.WriteLine(stdOut); + output.WriteLine(stdError); + + process.ExitCode.ShouldBe(expectedExitCode); + return stdOut; + } + + [Fact(Skip = "Manual verification only")] + public void Pack_Install_Run_Uninstall_Test() + { + var workingDirectory = "../../../../../src/AsyncAPI.Saunter.Generator.Cli"; + var stdOut = this.Run("dotnet", "pack", workingDirectory); + stdOut.ShouldContain("Successfully created package"); + + // use --force flag to ensure the test starts clean every run + stdOut = this.Run("dotnet", "new tool-manifest --force", workingDirectory); + stdOut.ShouldContain("The template \"Dotnet local tool manifest file\" was created successfully"); + + stdOut = this.Run("dotnet", "tool install --local --add-source ./bin/Release AsyncAPI.Saunter.Generator.Cli", workingDirectory); + stdOut = stdOut.Replace("Skipping NuGet package signature verification.", "").Trim(); + stdOut.ShouldContain("You can invoke the tool from this directory using the following commands: 'dotnet tool run dotnet-asyncapi"); + stdOut.ShouldContain("was successfully installed."); + + stdOut = this.Run("dotnet", "tool list --local asyncapi.saunter.generator.cli", workingDirectory); + stdOut.ShouldContain("dotnet-asyncapi"); + + stdOut = this.Run("dotnet", "tool run dotnet-asyncapi", workingDirectory, 1); + stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); + + stdOut = this.Run("dotnet", "tool uninstall --local asyncapi.saunter.generator.cli", workingDirectory); + stdOut.ShouldContain(" was successfully uninstalled"); + stdOut.ShouldContain("removed from manifest file"); + + stdOut = this.Run("dotnet", "tool list --local asyncapi.saunter.generator.cli", workingDirectory, 1); + stdOut.ShouldNotContain("dotnet-asyncapi"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs similarity index 92% rename from test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs rename to test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index 8d9e12d..105916d 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -1,26 +1,29 @@ -using System.Diagnostics; -using Shouldly; +using Shouldly; using Xunit.Abstractions; namespace AsyncAPI.Saunter.Generator.Cli.Tests; -public class DotnetCliToolTests(ITestOutputHelper output) +public class IntegrationTests(ITestOutputHelper output) { private string RunTool(string args, int expectedExitCode = 1) { - var process = Process.Start(new ProcessStartInfo("dotnet") - { - Arguments = $"../../../../../src/AsyncAPI.Saunter.Generator.Cli/bin/Debug/net8.0/AsyncAPI.Saunter.Generator.Cli.dll tofile {args}", - RedirectStandardOutput = true, - RedirectStandardError = true, - }); - process.WaitForExit(); - var stdOut = process.StandardOutput.ReadToEnd().Trim(); - var stdError = process.StandardError.ReadToEnd().Trim(); + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + Console.SetOut(outWriter); + Console.SetError(errorWriter); + + var entryPoint = typeof(Program).Assembly.EntryPoint!; + entryPoint.Invoke(null, new object[] { args.Split(' ') }); + + var stdOut = outWriter.ToString(); + var stdError = errorWriter.ToString(); + output.WriteLine($"RUN: {args}"); + output.WriteLine("### STD OUT"); output.WriteLine(stdOut); + output.WriteLine("### STD ERROR"); output.WriteLine(stdError); - process.ExitCode.ShouldBe(expectedExitCode); + Environment.ExitCode.ShouldBe(expectedExitCode); //stdError.ShouldBeEmpty(); LEGO lib doesn't like id: "id is not a valid property at #/components/schemas/lightMeasuredEvent"" return stdOut; } @@ -28,7 +31,7 @@ private string RunTool(string args, int expectedExitCode = 1) [Fact] public void DefaultCallPrintsCommandInfo() { - var stdOut = RunTool("", 0).Trim(); + var stdOut = RunTool("tofile", 0).Trim(); stdOut.ShouldBe(""" Usage: tofile [arguments...] [options...] [-h|--help] [--version] @@ -52,7 +55,7 @@ public void StreetlightsAPIExportSpecTest() { var path = Directory.GetCurrentDirectory(); output.WriteLine($"Output path: {path}"); - var stdOut = RunTool($"../../../../../examples/StreetlightsAPI/bin/Debug/net8.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); + var stdOut = RunTool($"tofile ../../../../../examples/StreetlightsAPI/bin/Debug/net8.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); stdOut.ShouldNotBeEmpty(); stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs deleted file mode 100644 index 2bf87ba..0000000 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Diagnostics; -using Shouldly; -using Xunit.Abstractions; - -namespace AsyncAPI.Saunter.Generator.Cli.Tests; - -public class PackAndInstallLocalTests(ITestOutputHelper output) -{ - private string Run(string file, string args, string workingDirectory, int expectedExitCode = 0) - { - var process = Process.Start(new ProcessStartInfo(file) - { - Arguments = args, - WorkingDirectory = workingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }); - process.WaitForExit(TimeSpan.FromSeconds(20)); - var stdOut = process.StandardOutput.ReadToEnd().Trim(); - var stdError = process.StandardError.ReadToEnd().Trim(); - output.WriteLine($"### Output of \"{file} {args}\""); - output.WriteLine(stdOut); - output.WriteLine(stdError); - - process.ExitCode.ShouldBe(expectedExitCode); - return stdOut; - } - - [Fact] - public void Pack_Install_Run_Uninstall_Test() - { - var stdOut = this.Run("dotnet", "pack", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); - stdOut.ShouldContain("Successfully created package"); - - stdOut = this.Run("dotnet", "tool install --global --add-source ./bin/Release AsyncAPI.Saunter.Generator.Cli", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); - stdOut.ShouldBeOneOf("You can invoke the tool using the following command: dotnet-asyncapi\r\nTool 'asyncapi.saunter.generator.cli' (version '1.0.1') was successfully installed.", - "Tool 'asyncapi.saunter.generator.cli' was reinstalled with the stable version (version '1.0.1')."); - - stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", ""); - stdOut.ShouldContain("dotnet-asyncapi"); - - stdOut = this.Run("dotnet", "asyncapi", "", 1); - stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); - - stdOut = this.Run("dotnet", "tool uninstall -g asyncapi.saunter.generator.cli", ""); - stdOut.ShouldContain(" was successfully uninstalled."); - - stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", "", 1); - stdOut.ShouldNotContain("dotnet-asyncapi"); - } -} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs new file mode 100644 index 0000000..bf09183 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs @@ -0,0 +1,152 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using NSubstitute.Community.Logging; +using Saunter; +using Saunter.AsyncApiSchema.v2; +using Saunter.Serialization; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class AsyncApiDocumentExtractorTests +{ + private readonly AsyncApiDocumentExtractor _extractor; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IAsyncApiDocumentProvider _documentProvider; + private readonly IOptions _asyncApiOptions; + private readonly IAsyncApiDocumentSerializer _documentSerializer; + + public AsyncApiDocumentExtractorTests() + { + var services = new ServiceCollection(); + this._documentProvider = Substitute.For(); + this._asyncApiOptions = Substitute.For>(); + var options = new AsyncApiOptions(); + this._asyncApiOptions.Value.Returns(options); + this._documentSerializer = Substitute.For(); + services.AddSingleton(this._documentProvider); + services.AddSingleton(this._asyncApiOptions); + services.AddSingleton(this._documentSerializer); + this._serviceProvider = services.BuildServiceProvider(); + + this._logger = Substitute.For>(); + this._extractor = new AsyncApiDocumentExtractor(this._logger); + } + + [Fact] + public void GetAsyncApiDocument_Null_NoMarkerAssemblies() + { + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(1).CallToLog(LogLevel.Critical); + } + + [Fact] + public void GetAsyncApiDocument_Default_WithMarkerAssembly() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBeNull(); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } + + [Fact] + public void GetAsyncApiDocument_1NamedDocument() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._asyncApiOptions.Value.NamedApis["service 1"] = doc; + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBe("service 1"); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } + + [Fact] + public void GetAsyncApiDocument_2NamedDocument() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc1 = new AsyncApiDocument { Id = "1" }; + var doc2 = new AsyncApiDocument { Id = "2" }; + this._asyncApiOptions.Value.NamedApis["service 1"] = doc1; + this._asyncApiOptions.Value.NamedApis["service 2"] = doc2; + this._documentProvider.GetDocument(Arg.Any(), Arg.Is(doc1)).Returns(doc1); + this._documentProvider.GetDocument(Arg.Any(), Arg.Is(doc2)).Returns(doc2); + this._documentSerializer.Serialize(doc1).Returns(""" + asyncapi: 2.6.0 + info: + title: Streetlights API 1 + """); + this._documentSerializer.Serialize(doc2).Returns(""" + asyncapi: 2.6.0 + info: + title: Streetlights API 2 + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).OrderBy(x => x.name).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(2); + documents[0].name.ShouldBe("service 1"); + documents[0].document.Info.Title.ShouldBe("Streetlights API 1"); + documents[1].name.ShouldBe("service 2"); + documents[1].document.Info.Title.ShouldBe("Streetlights API 2"); + } + + [Fact] + public void GetAsyncApiDocument_LogErrors() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + channels: + publish/light/measured: + servers: + - webapi + publish: + operationId: MeasureLight + summary: Inform about environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + $ref: '#/components/messages/lightMeasuredEvent' + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + this._logger.Received(3).CallToLog(LogLevel.Error); + this._logger.Received(0).CallToLog(LogLevel.Warning); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBeNull(); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs new file mode 100644 index 0000000..de26811 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs @@ -0,0 +1,94 @@ +using System.Collections; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Community.Logging; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class EnvironmentBuilderTests : IDisposable +{ + private readonly IDictionary _variablesBefore = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); + private readonly EnvironmentBuilder _environment; + private readonly ILogger _logger; + + public EnvironmentBuilderTests() + { + this._logger = Substitute.For>(); + this._environment = new EnvironmentBuilder(this._logger); + } + + private Dictionary GetAddedEnvironmentVariables() + { + var after = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); + return after.Cast().ExceptBy(this._variablesBefore.Keys.Cast(), x => x.Key).ToDictionary(x => x.Key.ToString(), x => x.Value?.ToString()); + } + + public void Dispose() + { + foreach (var variable in this.GetAddedEnvironmentVariables()) + { + Environment.SetEnvironmentVariable(variable.Key, null, EnvironmentVariableTarget.Process); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void EmptyEnvStringProvided(string env) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.ReceivedCalls().Count().ShouldBe(0); + this.GetAddedEnvironmentVariables().ShouldBeEmpty(); + } + + [Theory] + [InlineData("env1=val1", 1)] + [InlineData("a=b,b=c", 2)] + public void ValidEnvStringProvided(string env, int expectedSets) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.Received(expectedSets).CallToLog(LogLevel.Debug); + this.GetAddedEnvironmentVariables().ShouldNotBeEmpty(); + } + + [Theory] + [InlineData(",", 2)] + [InlineData(",,,,", 5)] + [InlineData("=a", 1)] + [InlineData("b", 1)] + [InlineData("=", 1)] + [InlineData("====", 1)] + public void InvalidEnvStringProvided(string env, int expectedSets) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.Received(expectedSets).CallToLog(LogLevel.Critical); + this.GetAddedEnvironmentVariables().ShouldBeEmpty(); + } + + [Fact] + public void ValidateEnvValues() + { + this._environment.SetEnvironmentVariables("ENV=1,,Test=two"); + + Environment.GetEnvironmentVariable("ENV").ShouldBe("1"); + Environment.GetEnvironmentVariable("Test").ShouldBe("two"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void EmptyValueDeletesEnvValue(string value) + { + this._environment.SetEnvironmentVariables($"ENV=1,,ENV={value}"); + + Environment.GetEnvironmentVariable("ENV").ShouldBe(null); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs new file mode 100644 index 0000000..129959b --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs @@ -0,0 +1,107 @@ +using System.Text; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class FileWriterTests +{ + private readonly Action _testContextWriter = stream => stream.Write(Encoding.Default.GetBytes("ananas")); + + private readonly FileWriter _writer; + private readonly IStreamProvider _streamProvider; + private readonly ILogger _logger; + private readonly MemoryStream _stream = new(); + + public FileWriterTests(ITestOutputHelper output) + { + this._logger = Substitute.For>(); + this._streamProvider = Substitute.For(); + this._streamProvider.GetStreamFor(default).ReturnsForAnyArgs(x => + { + output.WriteLine($"GetStreamFor({x.Args()[0]})"); + return this._stream; + }); + this._writer = new FileWriter(this._streamProvider, this._logger); + } + + [Fact] + public void CheckStreamContents() + { + this._writer.Write("/", "", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/")); + Encoding.Default.GetString(this._stream.GetBuffer().Take(6).ToArray()).ShouldBe("ananas"); + } + + [Fact] + public void CheckName_NoVariablesInTemplate() + { + this._writer.Write("/some/path", "fixed_name", "doc", "json", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/some/path/fixed_name")); + } + + [Theory] + [InlineData("./")] + [InlineData("/")] + [InlineData("/test/")] + [InlineData("/test/1/2/3/4/")] + public void CheckOutputPath_BaseOutputPath_Absolute(string path) + { + this._writer.Write(path, "document.something", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"{path}document.something")); + } + + [Theory] + [InlineData(".")] + [InlineData("")] + [InlineData("asyncApi/")] + [InlineData("service-1/")] + [InlineData("service 1/")] + [InlineData("service 1/spec")] + public void CheckOutputPath_BaseOutputPath_Relative(string path) + { + this._writer.Write(path, "document.something", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), path, "document.something"))); + } + + [Theory] + [InlineData("json")] + [InlineData("yml")] + [InlineData("txt")] + public void CheckOutputPath_FormatTemplate(string format) + { + this._writer.Write("/some/path", "{extension}_name.{extension}", "doc", format, _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"/some/path/{format}_name.{format}")); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void CheckOutputPath_FormatTemplate_trimmed(string format) + { + this._writer.Write("/some/path", "{extension}_name.{extension}", "doc", format, _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/some/path/name.")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("asyncApi")] + [InlineData("service-1")] + [InlineData("service 1")] + public void CheckOutputPath_DocumentNameTemplate(string documentName) + { + this._writer.Write("/some/path", "{document}.something", documentName, "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"/some/path/{documentName}.something")); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs new file mode 100644 index 0000000..a340b6d --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs @@ -0,0 +1,37 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class StreamProviderTests +{ + private readonly IStreamProvider _streamProvider = new StreamProvider(); + + [Fact] + public void NullPathIsStdOut() + { + using var stream = this._streamProvider.GetStreamFor(null); + + stream.ShouldNotBeNull(); + Assert.False(stream is FileStream); + } + + [Fact] + public void StringPathIsFileStream() + { + var path = Path.GetFullPath("./test"); + File.Delete(path); + try + { + using var stream = this._streamProvider.GetStreamFor(path); + + stream.ShouldNotBeNull(); + Assert.True(stream is FileStream); + File.Exists(path); + } + finally + { + File.Delete(path); + } + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs new file mode 100644 index 0000000..d879cc4 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs @@ -0,0 +1,205 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class ToFileCommandTests +{ + private readonly ToFileCommand _command; + private readonly IEnvironmentBuilder _environment; + private readonly IServiceProviderBuilder _builder; + private readonly IAsyncApiDocumentExtractor _docExtractor; + private readonly IFileWriter _fileWriter; + private readonly ILogger _logger; + private readonly ITestOutputHelper _output; + + public ToFileCommandTests(ITestOutputHelper output) + { + this._output = output; + this._logger = Substitute.For>(); + this._environment = Substitute.For(); + this._builder = Substitute.For(); + this._docExtractor = Substitute.For(); + this._fileWriter = Substitute.For(); + this._command = new ToFileCommand(this._logger, _environment, _builder, _docExtractor, _fileWriter); + } + + [Fact] + public void StartupAssembly_FileNotFoundException() + { + Assert.Throws(() => this._command.ToFile("")); + } + + [Fact] + public void SetEnvironmentVariables() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + + this._command.ToFile(me, env: "env=value"); + + this._environment.Received(1).SetEnvironmentVariables("env=value"); + } + + [Fact] + public void BuildServiceProvider() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + + this._command.ToFile(me); + + this._builder.Received(1).BuildServiceProvider(me); + } + + [Fact] + public void GetAsyncApiDocument_DefaultDocParam() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + var sp = Substitute.For(); + this._builder.BuildServiceProvider(default).ReturnsForAnyArgs(sp); + + this._command.ToFile(me); + + this._docExtractor.Received(1).GetAsyncApiDocument(sp, null); + } + + [Fact] + public void GetAsyncApiDocument_DocParam() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + var sp = Substitute.For(); + this._builder.BuildServiceProvider(default).ReturnsForAnyArgs(sp); + + this._command.ToFile(me, doc: "a"); + + this._docExtractor.Received(1).GetAsyncApiDocument(sp, Arg.Is(x => x.SequenceEqual(new[] { "a" }))); ; + } + + [Fact] + public void GetAsyncApiDocument_DocParamMultiple() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + var sp = Substitute.For(); + this._builder.BuildServiceProvider(default).ReturnsForAnyArgs(sp); + + this._command.ToFile(me, doc: "a,b, c ,,"); + + this._docExtractor.Received(1).GetAsyncApiDocument(sp, Arg.Is(x => x.SequenceEqual(new[] { "a", "b", " c " }))); + } + + [Fact] + public void WriteFile_DefaultParams() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } } )]); + + this._command.ToFile(me); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + } + + [Theory] + [InlineData("json")] + [InlineData("yml")] + [InlineData("yaml")] + public void WriteFile_FormatParam(string format) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: format); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, format, Arg.Any>()); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void WriteFile_EmptyFormatParamVariants_FallbackToJson(string format) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: format); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + } + + [Theory] + [InlineData("a")] + [InlineData("json1")] + [InlineData(".json")] + public void WriteFile_InvalidFormatParam_FallbackToJson(string format) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: format); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(0); + } + + [Fact] + public void WriteFile_FormatParamMultiple() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: " json , yaml,yml ,,a, "); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(3); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "yml", Arg.Any>()); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "yaml", Arg.Any>()); + } + + [Theory] + [InlineData("doc")] + [InlineData("{document}")] + [InlineData("{extension}")] + [InlineData("{document}.{extension}")] + public void WriteFile_FileTemplateParam(string template) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, filename: template); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), template, null, "json", Arg.Any>()); + } + + [Theory] + [InlineData("./")] + [InlineData("/")] + [InlineData("a/")] + [InlineData("/a/b")] + public void WriteFile_OutputPathParam(string path) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, output: path); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath(path), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + } +} diff --git a/test/Saunter.Tests/Saunter.Tests.csproj b/test/Saunter.Tests/Saunter.Tests.csproj index 8f47b9f..98b0356 100644 --- a/test/Saunter.Tests/Saunter.Tests.csproj +++ b/test/Saunter.Tests/Saunter.Tests.csproj @@ -17,11 +17,11 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive