diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..0b8cc13 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,11 @@ + + + latest + enable + enable + true + latest + true + true + + diff --git a/src/MxGateway.Contracts/GatewayContractInfo.cs b/src/MxGateway.Contracts/GatewayContractInfo.cs new file mode 100644 index 0000000..22bcb20 --- /dev/null +++ b/src/MxGateway.Contracts/GatewayContractInfo.cs @@ -0,0 +1,12 @@ +namespace MxGateway.Contracts; + +/// +/// Exposes version metadata shared by gateway components before generated +/// protobuf contracts are introduced. +/// +public static class GatewayContractInfo +{ + public const uint WorkerProtocolVersion = 1; + + public const string DefaultBackendName = "mxaccess-worker"; +} diff --git a/src/MxGateway.Contracts/MxGateway.Contracts.csproj b/src/MxGateway.Contracts/MxGateway.Contracts.csproj new file mode 100644 index 0000000..00b9359 --- /dev/null +++ b/src/MxGateway.Contracts/MxGateway.Contracts.csproj @@ -0,0 +1,7 @@ + + + + net10.0 + + + diff --git a/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs b/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs new file mode 100644 index 0000000..608101a --- /dev/null +++ b/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs @@ -0,0 +1,12 @@ +namespace MxGateway.IntegrationTests; + +public static class IntegrationTestEnvironment +{ + public const string LiveMxAccessVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS"; + + public static bool LiveMxAccessTestsEnabled => + string.Equals( + Environment.GetEnvironmentVariable(LiveMxAccessVariableName), + "1", + StringComparison.Ordinal); +} diff --git a/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs b/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs new file mode 100644 index 0000000..b199231 --- /dev/null +++ b/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs @@ -0,0 +1,12 @@ +namespace MxGateway.IntegrationTests; + +public sealed class IntegrationTestEnvironmentTests +{ + [Fact] + public void LiveMxAccessTests_AreOptInByEnvironmentVariable() + { + Assert.Equal( + "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS", + IntegrationTestEnvironment.LiveMxAccessVariableName); + } +} diff --git a/src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj b/src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj new file mode 100644 index 0000000..6a9cfe2 --- /dev/null +++ b/src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + false + + + + + + + + + + + + + + + + + + diff --git a/src/MxGateway.Server/GatewayApplication.cs b/src/MxGateway.Server/GatewayApplication.cs new file mode 100644 index 0000000..648b537 --- /dev/null +++ b/src/MxGateway.Server/GatewayApplication.cs @@ -0,0 +1,40 @@ +using MxGateway.Contracts; + +namespace MxGateway.Server; + +public static class GatewayApplication +{ + public static WebApplication Build(string[] args) + { + WebApplicationBuilder builder = CreateBuilder(args); + WebApplication app = builder.Build(); + + app.MapGatewayEndpoints(); + + return app; + } + + public static WebApplicationBuilder CreateBuilder(string[] args) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + + builder.Services.AddHealthChecks(); + + return builder; + } + + public static IEndpointRouteBuilder MapGatewayEndpoints(this IEndpointRouteBuilder endpoints) + { + endpoints.MapGet("/", () => Results.Redirect("/health/live")); + + endpoints.MapGet( + "/health/live", + () => Results.Ok(new GatewayHealthReply( + Status: "Healthy", + DefaultBackend: GatewayContractInfo.DefaultBackendName, + WorkerProtocolVersion: GatewayContractInfo.WorkerProtocolVersion))) + .WithName("LiveHealth"); + + return endpoints; + } +} diff --git a/src/MxGateway.Server/GatewayHealthReply.cs b/src/MxGateway.Server/GatewayHealthReply.cs new file mode 100644 index 0000000..c4a7ad7 --- /dev/null +++ b/src/MxGateway.Server/GatewayHealthReply.cs @@ -0,0 +1,6 @@ +namespace MxGateway.Server; + +public sealed record GatewayHealthReply( + string Status, + string DefaultBackend, + uint WorkerProtocolVersion); diff --git a/src/MxGateway.Server/MxGateway.Server.csproj b/src/MxGateway.Server/MxGateway.Server.csproj new file mode 100644 index 0000000..dc18b08 --- /dev/null +++ b/src/MxGateway.Server/MxGateway.Server.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + + + + + + + diff --git a/src/MxGateway.Server/Program.cs b/src/MxGateway.Server/Program.cs new file mode 100644 index 0000000..1dd9812 --- /dev/null +++ b/src/MxGateway.Server/Program.cs @@ -0,0 +1,7 @@ +using MxGateway.Server; + +var app = GatewayApplication.Build(args); + +app.Run(); + +public partial class Program; diff --git a/src/MxGateway.Server/Properties/launchSettings.json b/src/MxGateway.Server/Properties/launchSettings.json new file mode 100644 index 0000000..94f07e8 --- /dev/null +++ b/src/MxGateway.Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5120", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7121;http://localhost:5120", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/MxGateway.Server/appsettings.Development.json b/src/MxGateway.Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/MxGateway.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/MxGateway.Server/appsettings.json b/src/MxGateway.Server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/MxGateway.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/MxGateway.Tests/Contracts/GatewayContractInfoTests.cs b/src/MxGateway.Tests/Contracts/GatewayContractInfoTests.cs new file mode 100644 index 0000000..93c0acf --- /dev/null +++ b/src/MxGateway.Tests/Contracts/GatewayContractInfoTests.cs @@ -0,0 +1,18 @@ +using MxGateway.Contracts; + +namespace MxGateway.Tests.Contracts; + +public sealed class GatewayContractInfoTests +{ + [Fact] + public void DefaultBackendName_IsMxAccessWorker() + { + Assert.Equal("mxaccess-worker", GatewayContractInfo.DefaultBackendName); + } + + [Fact] + public void WorkerProtocolVersion_StartsAtVersionOne() + { + Assert.Equal(1u, GatewayContractInfo.WorkerProtocolVersion); + } +} diff --git a/src/MxGateway.Tests/Gateway/GatewayApplicationTests.cs b/src/MxGateway.Tests/Gateway/GatewayApplicationTests.cs new file mode 100644 index 0000000..006798f --- /dev/null +++ b/src/MxGateway.Tests/Gateway/GatewayApplicationTests.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using MxGateway.Server; + +namespace MxGateway.Tests.Gateway; + +public sealed class GatewayApplicationTests +{ + [Fact] + public void Build_MapsLiveHealthEndpoint() + { + WebApplication app = GatewayApplication.Build([]); + + RouteEndpoint endpoint = Assert.Single( + ((IEndpointRouteBuilder)app).DataSources + .SelectMany(dataSource => dataSource.Endpoints) + .OfType(), + candidate => candidate.RoutePattern.RawText == "/health/live"); + + Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata()?.EndpointName); + } +} diff --git a/src/MxGateway.Tests/MxGateway.Tests.csproj b/src/MxGateway.Tests/MxGateway.Tests.csproj new file mode 100644 index 0000000..27e10c0 --- /dev/null +++ b/src/MxGateway.Tests/MxGateway.Tests.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + false + + + + + + + + + + + + + + + + + + + diff --git a/src/MxGateway.Tests/ProjectStructure/GatewayProjectReferenceTests.cs b/src/MxGateway.Tests/ProjectStructure/GatewayProjectReferenceTests.cs new file mode 100644 index 0000000..b9711e8 --- /dev/null +++ b/src/MxGateway.Tests/ProjectStructure/GatewayProjectReferenceTests.cs @@ -0,0 +1,64 @@ +using System.Xml.Linq; + +namespace MxGateway.Tests.ProjectStructure; + +public sealed class GatewayProjectReferenceTests +{ + [Fact] + public void GatewayProject_TargetsNet10() + { + XDocument project = LoadProject("MxGateway.Server"); + + Assert.Equal("net10.0", ElementValue(project, "TargetFramework")); + } + + [Fact] + public void GatewayProject_DoesNotReferenceMxAccessCom() + { + XDocument project = LoadProject("MxGateway.Server"); + + IReadOnlyList referenceNames = project + .Descendants() + .Where(element => element.Name.LocalName is "Reference" or "COMReference" or "COMFileReference" or "PackageReference") + .Select(element => (string?)element.Attribute("Include") ?? string.Empty) + .ToArray(); + + Assert.DoesNotContain(referenceNames, reference => + reference.Contains("MxAccess", StringComparison.OrdinalIgnoreCase) + || reference.Contains("ArchestrA.MXAccess", StringComparison.OrdinalIgnoreCase) + || reference.Contains("LMXProxy", StringComparison.OrdinalIgnoreCase)); + } + + private static XDocument LoadProject(string projectName) + { + DirectoryInfo repositoryRoot = FindRepositoryRoot(); + string projectPath = Path.Combine(repositoryRoot.FullName, projectName, $"{projectName}.csproj"); + + return XDocument.Load(projectPath); + } + + private static string ElementValue(XDocument project, string elementName) + { + return project + .Descendants() + .Single(element => element.Name.LocalName == elementName) + .Value; + } + + private static DirectoryInfo FindRepositoryRoot() + { + DirectoryInfo? current = new(AppContext.BaseDirectory); + + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, "MxGateway.sln"))) + { + return current; + } + + current = current.Parent; + } + + throw new DirectoryNotFoundException("Could not locate src/MxGateway.sln from the test output directory."); + } +} diff --git a/src/MxGateway.sln b/src/MxGateway.sln new file mode 100644 index 0000000..4426136 --- /dev/null +++ b/src/MxGateway.sln @@ -0,0 +1,76 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Contracts", "MxGateway.Contracts\MxGateway.Contracts.csproj", "{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Server", "MxGateway.Server\MxGateway.Server.csproj", "{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Tests", "MxGateway.Tests\MxGateway.Tests.csproj", "{6E069780-A892-487E-AEED-051E26C829A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.IntegrationTests", "MxGateway.IntegrationTests\MxGateway.IntegrationTests.csproj", "{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x64.ActiveCfg = Debug|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x64.Build.0 = Debug|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x86.ActiveCfg = Debug|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x86.Build.0 = Debug|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|Any CPU.Build.0 = Release|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x64.ActiveCfg = Release|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x64.Build.0 = Release|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x86.ActiveCfg = Release|Any CPU + {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x86.Build.0 = Release|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x64.Build.0 = Debug|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x86.Build.0 = Debug|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|Any CPU.Build.0 = Release|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x64.ActiveCfg = Release|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x64.Build.0 = Release|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x86.ActiveCfg = Release|Any CPU + {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x86.Build.0 = Release|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x64.Build.0 = Debug|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x86.Build.0 = Debug|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Release|Any CPU.Build.0 = Release|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Release|x64.ActiveCfg = Release|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Release|x64.Build.0 = Release|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Release|x86.ActiveCfg = Release|Any CPU + {6E069780-A892-487E-AEED-051E26C829A4}.Release|x86.Build.0 = Release|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x64.Build.0 = Debug|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x86.Build.0 = Debug|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|Any CPU.Build.0 = Release|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x64.ActiveCfg = Release|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x64.Build.0 = Release|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x86.ActiveCfg = Release|Any CPU + {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal