diff --git a/OpenApiLINQPadDriver.sln.DotSettings b/OpenApiLINQPadDriver.sln.DotSettings new file mode 100644 index 0000000..b0e5f9c --- /dev/null +++ b/OpenApiLINQPadDriver.sln.DotSettings @@ -0,0 +1,3 @@ + + False + True \ No newline at end of file diff --git a/OpenApiLINQPadDriver/Extensions/EnumerableExtensions.cs b/OpenApiLINQPadDriver/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..bdb8a8f --- /dev/null +++ b/OpenApiLINQPadDriver/Extensions/EnumerableExtensions.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Linq; + +namespace OpenApiLINQPadDriver.Extensions; +internal static class EnumerableExtensions +{ + public static IEnumerable AppendIf(this IEnumerable enumerable, bool shouldAppend, T element) + => shouldAppend ? enumerable.Append(element) : enumerable; +} diff --git a/OpenApiLINQPadDriver/OpenApiContextDriver.cs b/OpenApiLINQPadDriver/OpenApiContextDriver.cs index a1b36bc..6878ee0 100644 --- a/OpenApiLINQPadDriver/OpenApiContextDriver.cs +++ b/OpenApiLINQPadDriver/OpenApiContextDriver.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Reflection; diff --git a/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs b/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs index 2194361..59e916f 100644 --- a/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs +++ b/OpenApiLINQPadDriver/OpenApiContextDriverProperties.cs @@ -112,7 +112,7 @@ private bool GetValue(bool defaultValue, [CallerMemberName] string callerMemberN private T GetValue(T defaultValue, [CallerMemberName] string callerMemberName = "") where T : struct, Enum - => (T)GetValue(v => Enum.TryParse(v, out var val) ? val : defaultValue, defaultValue, callerMemberName); + => GetValue(v => Enum.TryParse(v, out var val) ? val : defaultValue, defaultValue, callerMemberName); private void SetValue(T value, [CallerMemberName] string callerMemberName = "") { diff --git a/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj b/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj index 7c110a4..7afb576 100644 --- a/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj +++ b/OpenApiLINQPadDriver/OpenApiLINQPadDriver.csproj @@ -28,6 +28,12 @@ $(AssemblyTitle). + + + <_Parameter1>OpenApiLINQPadDriverTests + + + true true @@ -70,6 +76,7 @@ + diff --git a/OpenApiLINQPadDriver/SchemaBuilder.cs b/OpenApiLINQPadDriver/SchemaBuilder.cs index adfd96c..d6dd5b3 100644 --- a/OpenApiLINQPadDriver/SchemaBuilder.cs +++ b/OpenApiLINQPadDriver/SchemaBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Reflection; using LINQPad.Extensibility.DataContext; @@ -30,7 +31,6 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive #else var stopWatch = Stopwatch.StartNew(); #endif - var document = OpenApiDocumentHelper.GetFromUri(new Uri(openApiContextDriverProperties.OpenApiDocumentUri!)); MeasureTimeAndAddTimeExecutionExplorerItem("Downloading document"); @@ -38,7 +38,8 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive document.SetServer(openApiContextDriverProperties.ApiUri!); var endpointGrouping = openApiContextDriverProperties.EndpointGrouping; - var settings = CreateCsharpClientGeneratorSettings(endpointGrouping, openApiContextDriverProperties.JsonLibrary, openApiContextDriverProperties.ClassStyle, + var classStyle = openApiContextDriverProperties.ClassStyle; + var settings = CreateCsharpClientGeneratorSettings(endpointGrouping, openApiContextDriverProperties.JsonLibrary, classStyle, openApiContextDriverProperties.GenerateSyncMethods, mainContextType); var generator = new CSharpClientGenerator(document, settings); @@ -47,6 +48,7 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive MeasureTimeAndAddTimeExecutionExplorerItem("Generating NSwag classes"); + //possibly this switch should be an if based on SupportsMultipleClients? var clientSourceCode = endpointGrouping switch { EndpointGrouping.SingleClientFromOperationIdOperationName => ClientGenerator.SingleClientFromOperationIdOperationNameGenerator(mainContextType), @@ -58,6 +60,7 @@ internal static List GetSchemaAndBuildAssembly(OpenApiContextDrive var references = openApiContextDriverProperties.GetCoreFxReferenceAssemblies() .Append(typeof(JsonConvert).Assembly.Location) //required for code generation, otherwise NSwag will use lowest possible version 10.0.1 + .AppendIf(classStyle == ClassStyle.Prism, typeof(Prism.IActiveAware).Assembly.Location) .ToArray(); #pragma warning disable SYSLIB0044 //this is the only way to read this assembly, LINQPad does not give any other reference to it @@ -130,17 +133,21 @@ void MeasureTimeAndAddTimeExecutionExplorerItem(string name) var elapsed = stopWatch.Elapsed; stopWatch.Restart(); #endif + File.AppendAllText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "log.txt"), name + " " + elapsed + Environment.NewLine); timeExplorerItem.Children.Add(ExplorerItemHelper.CreateForElapsedTime(name, elapsed)); } } - private static CSharpClientGeneratorSettings CreateCsharpClientGeneratorSettings(EndpointGrouping endpointGrouping, JsonLibrary jsonLibrary, ClassStyle classStyle, bool generateSyncMethods, - TypeDescriptor type) + private static CSharpClientGeneratorSettings CreateCsharpClientGeneratorSettings(EndpointGrouping endpointGrouping, JsonLibrary jsonLibrary, ClassStyle classStyle, + bool generateSyncMethods, TypeDescriptor type) { var (operationNameGenerator, className) = endpointGrouping switch { - EndpointGrouping.MultipleClientsFromFirstTagAndOperationName => ((IOperationNameGenerator)new MultipleClientsFromFirstTagAndOperationNameGenerator(), "{controller}" + ClientPostFix), - EndpointGrouping.SingleClientFromOperationIdOperationName => (new SingleClientFromOperationIdOperationNameGenerator(), type.Name), + EndpointGrouping.MultipleClientsFromFirstTagAndOperationName + => ((IOperationNameGenerator)new MultipleClientsFromFirstTagAndOperationNameGenerator(), "{controller}" + ClientPostFix), + + EndpointGrouping.SingleClientFromOperationIdOperationName + => (new SingleClientFromOperationIdOperationNameGenerator(), type.Name), _ => throw new InvalidOperationException() }; diff --git a/OpenApiLINQPadDriver/packages.lock.json b/OpenApiLINQPadDriver/packages.lock.json index 22ff6ed..ba3d9ae 100644 --- a/OpenApiLINQPadDriver/packages.lock.json +++ b/OpenApiLINQPadDriver/packages.lock.json @@ -98,6 +98,12 @@ "Newtonsoft.Json": "10.0.1" } }, + "Prism.Core": { + "type": "Direct", + "requested": "[8.1.97, )", + "resolved": "8.1.97", + "contentHash": "EP5zrvWddw3eSq25Y7hHnDYdmLZEC2Z/gMrvmHzUuLbitmA1UaS7wQUlSwNr9Km8lzJNCvytFnaGBEFukHgoHg==" + }, "Fluid.Core": { "type": "Transitive", "resolved": "2.2.15", @@ -1236,6 +1242,12 @@ "Newtonsoft.Json": "10.0.1" } }, + "Prism.Core": { + "type": "Direct", + "requested": "[8.1.97, )", + "resolved": "8.1.97", + "contentHash": "EP5zrvWddw3eSq25Y7hHnDYdmLZEC2Z/gMrvmHzUuLbitmA1UaS7wQUlSwNr9Km8lzJNCvytFnaGBEFukHgoHg==" + }, "Fluid.Core": { "type": "Transitive", "resolved": "2.2.15", diff --git a/Tests/OpenApiLINQPadDriverTests/AuthTests.cs b/Tests/OpenApiLINQPadDriverTests/AuthTests.cs new file mode 100644 index 0000000..436c908 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/AuthTests.cs @@ -0,0 +1,52 @@ +namespace OpenApiLINQPadDriverTests; +public class AuthTests : BaseDriverApiTest +{ + [Theory] //todo test both global and local setter + [MemberData(nameof(EnumInlineData.Data), MemberType = typeof(EnumInlineData))] + public async Task Setting_Headers_In_PrepareRequestFunction_For_MultipleClientsFromFirstTagAndOperationName(JsonLibrary jsonLibrary, ClassStyle classStyle) + { + MapGet("Header", "/replay", "Replay", static (HttpContext context) => context.Request.Headers); + StartApi(); + + await ExecuteScriptAsync( + """ + var tokenValue = "fakeBearer"; + this.PrepareRequestFunction = (a, requestMessage, c) => + { + requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenValue); + }; + + var replayedHeaders = await this.HeaderClient.ReplayAsync(); + + replayedHeaders.Should() + .Contain(h => h.Key == "Authorization" && h.Value.Any(v => v == $"Bearer {tokenValue}")); + // return 1; + """, EndpointGrouping.MultipleClientsFromFirstTagAndOperationName, jsonLibrary, classStyle); + } + + [Theory] //todo test both global and local setter + [MemberData(nameof(EnumInlineData.Data), MemberType = typeof(EnumInlineData))] + public async Task Setting_Headers_In_PrepareRequestFunction_For_SingleClientFromOperationIdOperationName(JsonLibrary jsonLibrary, ClassStyle classStyle) + { + MapGet("Header", "/replay", "Replay", static (HttpContext context) => context.Request.Headers); + StartApi(); + + await ExecuteScriptAsync( + """ + var tokenValue = "fakeBearer"; + this.PrepareRequestFunction = (a, requestMessage, c) => + { + requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenValue); + }; + + var replayedHeaders = await this.ReplayAsync(); + + replayedHeaders.Should() + .Contain(h => h.Key == "Authorization" && h.Value.Any(v => v == $"Bearer {tokenValue}")); + + //replayedHeaders.Dump(); + return replayedHeaders; + """, EndpointGrouping.SingleClientFromOperationIdOperationName, jsonLibrary, classStyle); + + } +} diff --git a/Tests/OpenApiLINQPadDriverTests/CustomResponseBody.cs b/Tests/OpenApiLINQPadDriverTests/CustomResponseBody.cs new file mode 100644 index 0000000..50d4a64 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/CustomResponseBody.cs @@ -0,0 +1,74 @@ +namespace OpenApiLINQPadDriverTests; +public class CustomResponseBody : BaseDriverApiTest +{ + [Theory] + [MemberData(nameof(EnumInlineData.Data), MemberType = typeof(EnumInlineData))] + public async Task MultipleClientsFromFirstTagAndOperationName(JsonLibrary jsonLibrary, ClassStyle classStyle) + { + MapGet("CustomResponseAndRequest", "/First", "first", static ([FromBody] CustomRequest request) => new CustomResponse(request.Foo, request.Bar)); + StartApi(); + + var requestConstructor = GetRequestCtor(classStyle); + var responseConstructor = GetResponseCtor(classStyle); + + await ExecuteScriptAsync( + $""" + {nameof(CustomRequest)} request = {requestConstructor}; + + var response = await this.CustomResponseAndRequestClient.FirstAsync(request); + response.Should().BeEquivalentTo({responseConstructor}); + + """, EndpointGrouping.MultipleClientsFromFirstTagAndOperationName, jsonLibrary, classStyle); + } + + [Theory] + [MemberData(nameof(EnumInlineData.Data), MemberType = typeof(EnumInlineData))] + public async Task SingleClientFromOperationIdOperationName(JsonLibrary jsonLibrary, ClassStyle classStyle) + { + MapGet("CustomResponseAndRequest", "/First", "first", static ([FromBody] CustomRequest request) => new CustomResponse(request.Foo, request.Bar)); + StartApi(); + + var requestConstructor = GetRequestCtor(classStyle); + var responseConstructor = GetResponseCtor(classStyle); + + await ExecuteScriptAsync( + $""" + {nameof(CustomRequest)} request = {requestConstructor}; + + var response = await this.FirstAsync(request); + response.Should().BeEquivalentTo({responseConstructor}); + + """, EndpointGrouping.SingleClientFromOperationIdOperationName, jsonLibrary, classStyle); + } + + private record CustomResponse(string Foo, int Bar); + private record CustomRequest(string Foo, int Bar); + + private static string GetResponseCtor(ClassStyle classStyle) => classStyle switch + { + ClassStyle.Poco or ClassStyle.Inpc or ClassStyle.Prism => + """ + new CustomResponse() + { + Foo = "string", + Bar = 1 + } + """, + ClassStyle.Record => "new CustomResponse(1, \"string\")", + _ => throw new InvalidOperationException() + }; + + private static string GetRequestCtor(ClassStyle classStyle) => classStyle switch + { + ClassStyle.Poco or ClassStyle.Inpc or ClassStyle.Prism => + """ + new CustomRequest() + { + Foo = "string", + Bar = 1 + } + """, + ClassStyle.Record => "new CustomRequest(1, \"string\")", + _ => throw new InvalidOperationException() + }; +} diff --git a/Tests/OpenApiLINQPadDriverTests/LPRun/Templates/Test.linq b/Tests/OpenApiLINQPadDriverTests/LPRun/Templates/Test.linq deleted file mode 100644 index e4e91d2..0000000 --- a/Tests/OpenApiLINQPadDriverTests/LPRun/Templates/Test.linq +++ /dev/null @@ -1,11 +0,0 @@ -var number = 2; -var summary = "test"; - -var weathers = await this.WeatherClient.GetWeatherForecastAsync(number, summary); -weathers.Should().HaveCount(number) -.And.ContainEquivalentOf(new WeatherForecast -{ - Date = DateTime.Now.Date.AddDays(2), - TemperatureC = number, - Summary = summary -}, Reason()); \ No newline at end of file diff --git a/Tests/OpenApiLINQPadDriverTests/OpenApiLINQPadDriverTests.csproj b/Tests/OpenApiLINQPadDriverTests/OpenApiLINQPadDriverTests.csproj index 08b5b68..bbb2c93 100644 --- a/Tests/OpenApiLINQPadDriverTests/OpenApiLINQPadDriverTests.csproj +++ b/Tests/OpenApiLINQPadDriverTests/OpenApiLINQPadDriverTests.csproj @@ -3,53 +3,42 @@ net7.0-windows enable - true + enable latest false - false true + - + + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + - - - - - - - Always - - - - - - diff --git a/Tests/OpenApiLINQPadDriverTests/SimpleTests.cs b/Tests/OpenApiLINQPadDriverTests/SimpleTests.cs new file mode 100644 index 0000000..281a7c7 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/SimpleTests.cs @@ -0,0 +1,53 @@ +namespace OpenApiLINQPadDriverTests; + +public class SimpleTests : BaseDriverApiTest +{ + [Theory] //todo test both global and local setter + [MemberData(nameof(EnumInlineData.Data), MemberType = typeof(EnumInlineData))] + public async Task MultipleClientsFromFirstTagAndOperationName(JsonLibrary jsonLibrary, ClassStyle classStyle) + { + MapGet("arithmetic", "/sum", "sum", static (int x, int y) => x + y); + MapGet("arithmetic", "/divide", "divide", static (int dividend, int divisor) => dividend / divisor); + StartApi(); + + await ExecuteScriptAsync( + """ + var sum = await this.ArithmeticClient.SumAsync(x: 1, y: 2); + sum.Should().Be(3, Reason()); + + var asyncAction = () => this.ArithmeticClient.DivideAsync(dividend: 1, divisor: 0); + + await asyncAction.Should().ThrowAsync().WithMessage("*The HTTP status code of the response was not expected (500)*"); + + //this.Dump(); + """, EndpointGrouping.MultipleClientsFromFirstTagAndOperationName, jsonLibrary, classStyle); + } + + [Theory] //todo test both global and local setter + [MemberData(nameof(EnumInlineData.Data), MemberType = typeof(EnumInlineData))] + public async Task SingleClientFromOperationIdOperationName(JsonLibrary jsonLibrary, ClassStyle classStyle) + { + MapGet("arithmetic", "/sum", "sum", static (int x, int y) => x + y); + MapGet("arithmetic", "/divide", "divide", static (int dividend, int divisor) => dividend / divisor); + StartApi(); + + await ExecuteScriptAsync( + """ + var sum = await this.SumAsync(x: 1, y: 2); + sum.Should().Be(3, Reason()); + + var division = await this.DivideAsync(dividend: 10, divisor: 5); + division.Should().Be(2, Reason()); + """, EndpointGrouping.SingleClientFromOperationIdOperationName, jsonLibrary, classStyle); + } + + + + //todo some form of physical cache based on swagger.json hash? Write to temp and read it or something + + //todo auth tests + + //todo defaults props are set (for both modes) + + //todo test with a lot of endpoints/clients +} diff --git a/Tests/OpenApiLINQPadDriverTests/UI.cs b/Tests/OpenApiLINQPadDriverTests/UI.cs new file mode 100644 index 0000000..02530a0 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/UI.cs @@ -0,0 +1,90 @@ +//using LINQPad.Extensibility.DataContext; +//using Moq; +//using System.Xml.Linq; +//using FlaUI.UIA3; +//using OpenApiLINQPadDriver; +//using FluentAssertions; +//using FlaUI.Core.AutomationElements; +//using FlaUI.Core.Definitions; + +//namespace OpenApiLINQPadDriverTests; +//public class UI +//{ + +// [Fact] +// public async Task Foo() +// { +// Action? close = null; +// var connectionInfoMock = new Mock(); + +// var xElement = XDocument.Parse(@$" +// https://localhost/swagger/v1/swagger.json +// https://localhost/ +//").Root!; + +// connectionInfoMock.Setup(s => s.DriverData).Returns(xElement); + +// var driverProperties = new OpenApiContextDriverProperties(connectionInfoMock.Object); + +// var thread = new Thread(() => +// { + + +// var dialog = new ConnectionDialog(driverProperties); + +// close = () => dialog.Dispatcher.Invoke(dialog.Close); +// var result = dialog.ShowDialog(); + +// result.Should().BeTrue(); +// }); + +// try +// { +// thread.SetApartmentState(ApartmentState.STA); +// thread.Start(); + +// var app = FlaUI.Core.Application.Attach(Environment.ProcessId); + +// using var automation = new UIA3Automation(); + +// var window = app.GetMainWindow(automation); +// window.Title.Should().Be("Open API Connection"); + +// var okButton = window.FindFirstDescendant(cf => cf.ByText("OK"))?.AsButton(); +// var x = window.FindFirstDescendant(cf => cf.ByText("Open", PropertyConditionFlags.MatchSubstring)).AsLabel(); + + +// //var children = window.FindAllDescendants().ToList(); + +// //children.Should().NotBeNull(); +// x.Should().NotBeNull(); + +// x.Text.Should().Be("Open Api Json Uri (swagger.json): "); + +// var input = window.FindFirstDescendant(cf => +// cf.ByAutomationId("OpenApiDocumentUri", PropertyConditionFlags.MatchSubstring)) +// .AsTextBox(); + +// input.Should().NotBeNull(); +// input.Text.Should().Be("https://localhost/swagger/v1/swagger.json"); + + +// input.Enter("https://foobar/swagger/v1/swagger.json"); + +// await Task.Delay(200); + +// okButton.Click(); +// close = null; + +// //result = result; +// } +// finally +// { +// close?.Invoke(); +// thread.Join(); +// } + + +// driverProperties.OpenApiDocumentUri.Should().Be("https://foobar/swagger/v1/swagger.json"); +// } +//} diff --git a/Tests/OpenApiLINQPadDriverTests/UnitTest1.cs b/Tests/OpenApiLINQPadDriverTests/UnitTest1.cs deleted file mode 100644 index f0de723..0000000 --- a/Tests/OpenApiLINQPadDriverTests/UnitTest1.cs +++ /dev/null @@ -1,183 +0,0 @@ -using FluentAssertions; -using LINQPad.Extensibility.DataContext; -using LPRun; -using Moq; -using OpenApiLINQPadDriver; -using System.Text; -using System.Xml.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; -using OpenApiServer = Microsoft.OpenApi.Models.OpenApiServer; - -namespace OpenApiLINQPadDriverTests; - -public class UnitTest1 : IClassFixture -{ - private readonly ApiFixture _apiFixture; - public UnitTest1(ApiFixture apiFixture) => _apiFixture = apiFixture; - - [Fact] - public async Task Test1() - { - var connectionInfoMock = new Mock(); - - connectionInfoMock.Setup(s => s.DriverData).Returns(XDocument.Parse(@$" - {_apiFixture.ApiUrl}swagger/v1/swagger.json - {_apiFixture.ApiUrl} -").Root!); - - var driverProperties = new OpenApiContextDriverProperties(connectionInfoMock.Object); - var linqScriptName = "Test"; - - - var queryConfig = GetQueryHeaders().Aggregate(new StringBuilder(), static (stringBuilder, header) => - { - if (ShouldRender(header)) - { - stringBuilder.AppendLine(header); - stringBuilder.AppendLine(); - } - - return stringBuilder; - }).ToString(); - - var linqScript = LinqScript.Create( - $"{linqScriptName}.linq", - queryConfig, - linqScriptName); - - - // Act: Execute test LNQPad script. - var (output, error, exitCode) = - Runner.Execute(linqScript); - - // Assert. - error.Should().BeNullOrWhiteSpace(); - exitCode.Should().Be(0); - - IEnumerable GetQueryHeaders() - { - // Connection header. - var nameSpace = nameof(OpenApiLINQPadDriver); - var driverTypeName = nameof(OpenApiContextDriver); - yield return ConnectionHeader.Get( - nameSpace, - $"{nameSpace}.{driverTypeName}", - driverProperties, - "System.Runtime.CompilerServices"); - - // FluentAssertions helper. - yield return - @"string Reason([CallerLineNumber] int sourceLineNumber = 0) =>" + - @" $""something went wrong at line #{sourceLineNumber}"";"; - - // Test context. - //if (!string.IsNullOrWhiteSpace(context)) - //{ - // yield return $"var context = {context};"; - //} - } - } - static bool ShouldRender(string? str) => - !string.IsNullOrWhiteSpace(str); - -} - -public class ApiFixture : IDisposable -{ - private readonly CancellationTokenSource _cancellationTokenSource = new(); - public readonly string ApiUrl = "https://localhost:5003/"; - public ApiFixture() - { - try - { - const string driverName = "OpenApiLINQPadDriver"; - var linqPadFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPad"); - var nugetDriverPath = Path.Combine(linqPadFolderPath, "NuGet.Drivers", driverName); - var folderDriverPath = Path.Combine(linqPadFolderPath, "Drivers", "DataContext", driverName); - if (Directory.Exists(nugetDriverPath)) - throw new InvalidOperationException($"Driver already exists inside \"{nugetDriverPath}\", please remove it via LINQPad"); - if (Directory.Exists(folderDriverPath)) - throw new InvalidOperationException($"Driver already exists inside \"{folderDriverPath}\", please remove it via LINQPad"); - - // Copy driver to LPRun drivers folder. - Driver.InstallWithDepsJson( - driverName, - driverName + ".dll", - "Tests" - ); - - - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddControllers(); - - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(options => - { - options.AddServer(new OpenApiServer - { - Url = ApiUrl - }); - }); - builder.Services.AddMvc(); - - builder.WebHost - .UseUrls(ApiUrl); - - var app = builder.Build(); - - - app.UseSwagger(c => - { - }); - app.UseSwaggerUI(); - - app.UseHttpsRedirection(); - - app.MapControllers(); - - app.MapGet("/weatherforecast", (int numberOf, string summary) => - { - var forecast = Enumerable.Range(1, numberOf).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - numberOf, - summary - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast") - .WithOpenApi(config => - { - config.Tags.Clear(); - config.Tags.Add(new OpenApiTag - { - Name = "Weather" - }); - return config; - }); - - - app.RunAsync(_cancellationTokenSource.Token); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - - } - - internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary); - - public void Dispose() - { - _cancellationTokenSource.Cancel(); - } -} \ No newline at end of file diff --git a/Tests/OpenApiLINQPadDriverTests/Usings.cs b/Tests/OpenApiLINQPadDriverTests/Usings.cs index 8c927eb..f794039 100644 --- a/Tests/OpenApiLINQPadDriverTests/Usings.cs +++ b/Tests/OpenApiLINQPadDriverTests/Usings.cs @@ -1 +1,5 @@ +global using OpenApiLINQPadDriverTests.Utils; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using OpenApiLINQPadDriver.Enums; global using Xunit; \ No newline at end of file diff --git a/Tests/OpenApiLINQPadDriverTests/Utils/BaseDriverApiTest.cs b/Tests/OpenApiLINQPadDriverTests/Utils/BaseDriverApiTest.cs new file mode 100644 index 0000000..47d49de --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/Utils/BaseDriverApiTest.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using System.Reflection; + +namespace OpenApiLINQPadDriverTests.Utils; + +public abstract class BaseDriverApiTest : IAsyncLifetime +{ + private const string DriverName = nameof(OpenApiLINQPadDriver); + private WebApplication? _webApplication; + + private string? _apiUri; + private string ApiUrl => _apiUri ?? throw new ArgumentNullException(nameof(_apiUri)); + private static readonly string BaseDir = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().Location).LocalPath)!; + + private readonly List> _endpoints = new(); + private readonly QueryExecutor? _queryExecutor = new(); + + protected void StartApi() + { + LinqPadHelper.ThrowIfDriverExists(DriverName); + + InstallDriver(); + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(options => + { + //options.AddServer(new OpenApiServer + //{ + // Url = ApiUrl + //}); + }); + + //by using :0 port an empty one will be assigned + builder.WebHost.UseUrls("http://127.0.0.1:0"); + + var app = builder.Build(); + + app.Lifetime.ApplicationStarted.Register(() => _apiUri = app.Urls.Single()); + + app.UseSwagger(); + + foreach (var endpoint in _endpoints) + { + endpoint.Invoke(app); + } + + app.RunAsync(); + + _webApplication = app; + } + + private static void InstallDriver() + { + var files = new List { DriverName + ".dll", GetDepsJsonRelativePath(DriverName + ".dll", "Tests") }; + + var driverOutputPath = Path.Combine("drivers", "DataContext", "NetCore", DriverName); + + Directory.CreateDirectory(driverOutputPath); + + foreach (var file in files) + { + CopyFile(file); + } + + return; + + void ExecIfFileIsNewer(string file, Action action) + { + var srcFile = Path.GetFullPath(file); + var dstFile = Path.Combine(driverOutputPath, Path.GetFileName(file)); + + var srcFileInfo = new FileInfo(srcFile); + var dstFileInfo = new FileInfo(dstFile); + + if (!dstFileInfo.Exists || dstFileInfo.LastWriteTime < srcFileInfo.LastWriteTime) + { + action(srcFile, dstFile); + } + } + + void CopyFile(string file) => + ExecIfFileIsNewer(file, (srcFile, dstFile) => File.Copy(srcFile, dstFile, true)); + } + + protected Task ExecuteScriptAsync(string script, EndpointGrouping endpointGrouping, JsonLibrary jsonLibrary, ClassStyle classStyle) + => _queryExecutor!.RunAsync(ApiUrl, script, endpointGrouping, jsonLibrary, classStyle); + + protected void MapGet(string controllerName, string pattern, string name, Delegate handler) + => _endpoints.Add(app => + app.MapGet(pattern, handler) + .WithName(name) + .WithOpenApi(openApiConfig => + { + openApiConfig.Tags.Clear(); + openApiConfig.Tags.Add(new OpenApiTag + { + Name = controllerName + }); + return openApiConfig; + })); + + private static string GetDepsJsonRelativePath(string driverFileName, string testsFolderPath) + => GetDepsJsonRelativePath(driverFileName, baseDir => baseDir.Replace(testsFolderPath, string.Empty, StringComparison.OrdinalIgnoreCase)); + + private static string GetDepsJsonRelativePath(string driverFileName, Func getDepsJsonFileFullPath) + => Path.Combine(Path.GetRelativePath(BaseDir, Path.Combine(getDepsJsonFileFullPath(BaseDir), Path.ChangeExtension(driverFileName, ".deps.json")))); + + Task IAsyncLifetime.InitializeAsync() + => Task.CompletedTask; + + async Task IAsyncLifetime.DisposeAsync() + { + if (_webApplication != null) + await _webApplication.DisposeAsync(); + + _queryExecutor?.Dispose(); + } +} diff --git a/Tests/OpenApiLINQPadDriverTests/Utils/ConnectionHeader.cs b/Tests/OpenApiLINQPadDriverTests/Utils/ConnectionHeader.cs new file mode 100644 index 0000000..e451cf7 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/Utils/ConnectionHeader.cs @@ -0,0 +1,44 @@ +using System.Globalization; +using System.Reflection; +using System.Security; + +namespace OpenApiLINQPadDriverTests.Utils; +internal static class ConnectionHeader +{ + public static string Get(string driverAssemblyName, string driverNamespace, T driverConfig, params string[] additionalNamespaces) + where T : notnull + => + $""" + + + + 2 + Test + {driverNamespace} + + {string.Join(Environment.NewLine, GetKeyValues(driverConfig).Select(keyValuePair => $" <{keyValuePair.Key}>{SecurityElement.Escape(keyValuePair.Value)}"))} + + + FluentAssertions + {string.Join(Environment.NewLine, new[] { "FluentAssertions" }.Concat(additionalNamespaces).Select(additionalNamespace => $" {additionalNamespace}"))} + + """; + + private static IEnumerable<(string Key, string Value)> GetKeyValues(T driverConfig) + where T : notnull + { + return driverConfig.GetType().GetProperties() + .Where(propertyInfo => propertyInfo is { CanRead: true, CanWrite: true }) + .Select(propertyInfo => (propertyInfo.Name, ValueToString(propertyInfo))); + + string ValueToString(PropertyInfo propertyInfo) + => propertyInfo.GetValue(driverConfig) switch + { + null => string.Empty, + bool v => v ? "true" : "false", + IConvertible v => v.ToString(CultureInfo.InvariantCulture), + _ => throw new NotSupportedException($"Could not convert {propertyInfo.Name} of type {propertyInfo.PropertyType} to string") + }; + } +} + diff --git a/Tests/OpenApiLINQPadDriverTests/Utils/EnumInlineData.cs b/Tests/OpenApiLINQPadDriverTests/Utils/EnumInlineData.cs new file mode 100644 index 0000000..6513c85 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/Utils/EnumInlineData.cs @@ -0,0 +1,17 @@ +namespace OpenApiLINQPadDriverTests.Utils; +internal static class EnumInlineData +{ + public static IEnumerable Data + { + get + { + foreach (var jsonLibrary in Enum.GetValues()) + { + foreach (var classStyle in Enum.GetValues()) + { + yield return new object[] { jsonLibrary, classStyle }; + } + } + } + } +} diff --git a/Tests/OpenApiLINQPadDriverTests/Utils/LinqPadHelper.cs b/Tests/OpenApiLINQPadDriverTests/Utils/LinqPadHelper.cs new file mode 100644 index 0000000..35a4871 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/Utils/LinqPadHelper.cs @@ -0,0 +1,16 @@ +namespace OpenApiLINQPadDriverTests.Utils; +internal static class LinqPadHelper +{ + private static readonly string LocalLinqPadPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPad"); + public static void ThrowIfDriverExists(string driverName) + { + var nugetDriverPath = Path.Combine(LocalLinqPadPath, "NuGet.Drivers", driverName); + var folderDriverPath = Path.Combine(LocalLinqPadPath, "Drivers", "DataContext", "NetCore", driverName); + + if (Directory.Exists(nugetDriverPath)) + throw new InvalidOperationException($"Driver already exists inside \"{nugetDriverPath}\", please remove it via LINQPad"); + + if (Directory.Exists(folderDriverPath)) + throw new InvalidOperationException($"Driver already exists inside \"{folderDriverPath}\", please remove it via LINQPad"); + } +} diff --git a/Tests/OpenApiLINQPadDriverTests/Utils/QueryExecutor.cs b/Tests/OpenApiLINQPadDriverTests/Utils/QueryExecutor.cs new file mode 100644 index 0000000..aa4ef0d --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/Utils/QueryExecutor.cs @@ -0,0 +1,106 @@ +using LINQPad.Extensibility.DataContext; +using Moq; +using OpenApiLINQPadDriver; +using System.Text; +using System.Xml.Linq; +using LINQPad; +using System.Globalization; +using FluentAssertions; + +namespace OpenApiLINQPadDriverTests.Utils; +internal sealed class QueryExecutor : IDisposable +{ + private const string DriverNameSpace = nameof(OpenApiLINQPadDriver); + private const string DriverTypeName = nameof(OpenApiContextDriver); + private const string TempScriptDirectory = "TempScripts"; + + private readonly string _uniqueFileName = Guid.NewGuid().ToString(); + private bool _wasRun; + + public async Task RunAsync(string apiUri, string script, EndpointGrouping endpointGrouping, JsonLibrary jsonLibrary, ClassStyle classStyle) + { + //otherwise LINQPad errors are localized + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; + + if (_wasRun) + throw new InvalidOperationException("Multiple script execution per test is not supported (to support it create list of unique file names and then dispose of them"); + _wasRun = true; + + var queryConfig = GetQueryHeaders(apiUri, script, endpointGrouping, jsonLibrary, classStyle) + .Aggregate(new StringBuilder(), static (stringBuilder, header) => + { + stringBuilder.AppendLine(header); + stringBuilder.AppendLine(); + + return stringBuilder; + }).ToString(); + + Directory.CreateDirectory(TempScriptDirectory); + var filePath = GetFilePath(); + await File.WriteAllTextAsync(filePath, queryConfig); + + try + { + using var compilation = await Util.CompileAsync(filePath); + var queryExecuter = compilation.Run(QueryResultFormat.Html); + //var exception = await queryExecuter.ExceptionAsync; + var returnValue = await queryExecuter.ReturnValueAsync; + + //exception.Should().BeNull(); + return ""; + + } + catch (Exception exception) + { + Console.WriteLine(exception); + throw; + } + } + + private static IEnumerable GetQueryHeaders(string apiUri, string script, EndpointGrouping endpointGrouping, JsonLibrary jsonLibrary, ClassStyle classStyle) + { + var connectionInfoMock = new Mock(); + + var swaggerPath = Path.Join(apiUri, "/swagger/v1/swagger.json"); + + connectionInfoMock.Setup(s => s.DriverData).Returns(XDocument.Parse( + $""" + + <{nameof(OpenApiContextDriverProperties.OpenApiDocumentUri)}>{swaggerPath} + <{nameof(OpenApiContextDriverProperties.ApiUri)}>{apiUri} + <{nameof(OpenApiContextDriverProperties.EndpointGrouping)}>{endpointGrouping} + <{nameof(OpenApiContextDriverProperties.DebugInfo)}>true + <{nameof(OpenApiContextDriverProperties.JsonLibrary)}>{jsonLibrary} + <{nameof(OpenApiContextDriverProperties.ClassStyle)}>{classStyle} + + """).Root!); + + var driverProperties = new OpenApiContextDriverProperties(connectionInfoMock.Object); + + yield return ConnectionHeader.Get( + DriverNameSpace, + $"{DriverNameSpace}.{DriverTypeName}", + driverProperties, + "System.Runtime.CompilerServices", "FluentAssertions.Specialized", "System.Threading.Tasks"); + + //todo add some ifs Task/Task + yield return "async Task Main (object[] args)"; + yield return "{"; + yield return script; + yield return "return null;"; + yield return "}"; + yield return + "string Reason([CallerLineNumber] int sourceLineNumber = 0) =>" + + @" $""something went wrong at line #{sourceLineNumber}"";"; + } + + private string GetFilePath() => Path.Join(TempScriptDirectory, _uniqueFileName + ".linq"); + + public void Dispose() + { + //#if DEBUG // on CI Github runners are purged after each run so we don't care about leaving these files + File.Delete(GetFilePath()); + //#endif + } +} diff --git a/Tests/OpenApiLINQPadDriverTests/Utils/RunnerResultExtensions.cs b/Tests/OpenApiLINQPadDriverTests/Utils/RunnerResultExtensions.cs new file mode 100644 index 0000000..6f77f10 --- /dev/null +++ b/Tests/OpenApiLINQPadDriverTests/Utils/RunnerResultExtensions.cs @@ -0,0 +1,32 @@ +////using FluentAssertions; +////using LPRun; + +////namespace OpenApiLINQPadDriverTests.Utils; +////internal static class RunnerResultExtensions +////{ +//// public static async Task ShouldSucceedAsync(this Task resultAsync) +//// { +//// var result = await resultAsync; +//// result.ExitCode.Should().Be(0); +//// result.Error.Should().BeEmpty(); + +//// return result; +//// } +////} + +//using FluentAssertions; +//using System.Text; + +//namespace OpenApiLINQPadDriverTests.Utils; +//internal static class RunnerResultExtensions +//{ +// public static async Task ShouldSucceedAsync(this Task resultAsync) +// { +// var result = await resultAsync; +// result.ExitCode.Should().Be(0); +// result.Error.Should().BeEmpty(); + +// return result; +// } +//} +