From 239aee630f6f17c24db16f78a29d8d7badc57556 Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Tue, 17 Mar 2026 16:14:56 -0500 Subject: [PATCH] feat(test): add mock PVE API server for integration testing ASP.NET Minimal API project that simulates the Proxmox VE API using existing JSON fixtures. Supports ticket and API token auth, request tracking for assertions, self-signed HTTPS, and all major API endpoints. Designed for programmatic test startup via MockPveServer.Start(). Co-Authored-By: Claude Opus 4.6 (1M context) --- PSProxmoxVE.sln | 48 +++- tests/PSProxmoxVE.MockServer/MockPveServer.cs | 209 ++++++++++++++++++ .../PSProxmoxVE.MockServer.csproj | 9 + tests/PSProxmoxVE.MockServer/Program.cs | 22 ++ .../Routes/AuthRoutes.cs | 18 ++ .../Routes/ClusterRoutes.cs | 15 ++ .../Routes/ContainerRoutes.cs | 36 +++ .../Routes/NetworkRoutes.cs | 35 +++ .../Routes/NodeRoutes.cs | 18 ++ .../Routes/SnapshotRoutes.cs | 24 ++ .../Routes/StorageRoutes.cs | 28 +++ .../Routes/TaskRoutes.cs | 33 +++ .../Routes/UserRoutes.cs | 58 +++++ .../Routes/VersionRoutes.cs | 15 ++ .../PSProxmoxVE.MockServer/Routes/VmRoutes.cs | 77 +++++++ 15 files changed, 644 insertions(+), 1 deletion(-) create mode 100644 tests/PSProxmoxVE.MockServer/MockPveServer.cs create mode 100644 tests/PSProxmoxVE.MockServer/PSProxmoxVE.MockServer.csproj create mode 100644 tests/PSProxmoxVE.MockServer/Program.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/AuthRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/ClusterRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/ContainerRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/NetworkRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/NodeRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/SnapshotRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/StorageRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/TaskRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/UserRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/VersionRoutes.cs create mode 100644 tests/PSProxmoxVE.MockServer/Routes/VmRoutes.cs diff --git a/PSProxmoxVE.sln b/PSProxmoxVE.sln index 1b7a470..2c91f18 100644 --- a/PSProxmoxVE.sln +++ b/PSProxmoxVE.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -13,28 +13,74 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSProxmoxVE", "src\PSProxmo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSProxmoxVE.Core.Tests", "tests\PSProxmoxVE.Core.Tests\PSProxmoxVE.Core.Tests.csproj", "{E5F6A7B8-C9D0-1234-EF12-345678901234}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSProxmoxVE.MockServer", "tests\PSProxmoxVE.MockServer\PSProxmoxVE.MockServer.csproj", "{B7490D48-1665-41F4-B248-70051EC2F061}" +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 {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.Build.0 = Debug|Any CPU {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.Build.0 = Release|Any CPU {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|x64.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|x86.Build.0 = Debug|Any CPU {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|x64.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|x64.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|x86.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|x86.Build.0 = Release|Any CPU {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|x64.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|x86.Build.0 = Debug|Any CPU {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|x64.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|x64.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|x86.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|x86.Build.0 = Release|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Debug|x64.Build.0 = Debug|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Debug|x86.Build.0 = Debug|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Release|Any CPU.Build.0 = Release|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Release|x64.ActiveCfg = Release|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Release|x64.Build.0 = Release|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Release|x86.ActiveCfg = Release|Any CPU + {B7490D48-1665-41F4-B248-70051EC2F061}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {C3D4E5F6-A7B8-9012-CDEF-123456789012} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} {D4E5F6A7-B8C9-0123-DEF1-234567890123} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} {E5F6A7B8-C9D0-1234-EF12-345678901234} = {B2C3D4E5-F6A7-8901-BCDE-F12345678901} + {B7490D48-1665-41F4-B248-70051EC2F061} = {B2C3D4E5-F6A7-8901-BCDE-F12345678901} EndGlobalSection EndGlobal diff --git a/tests/PSProxmoxVE.MockServer/MockPveServer.cs b/tests/PSProxmoxVE.MockServer/MockPveServer.cs new file mode 100644 index 0000000..ff2e461 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/MockPveServer.cs @@ -0,0 +1,209 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using PSProxmoxVE.MockServer.Routes; + +namespace PSProxmoxVE.MockServer; + +public record RequestRecord(string Method, string Path, string? Body, DateTime Timestamp); + +public class MockPveServer : IDisposable +{ + private readonly WebApplication _app; + private bool _disposed; + + public string BaseUrl { get; } + public int Port { get; } + public List Requests { get; } = new(); + + private static readonly Dictionary _fixtures = new(); + private static readonly object _fixtureLock = new(); + + // Track task status calls per UPID for simulating running -> completed transitions + internal Dictionary TaskStatusCallCounts { get; } = new(); + + private MockPveServer(WebApplication app, int port) + { + _app = app; + Port = port; + BaseUrl = $"https://localhost:{port}"; + } + + public static MockPveServer Start(int port = 0) + { + LoadFixtures(); + + var cert = GenerateSelfSignedCert(); + + var builder = WebApplication.CreateBuilder(); + builder.WebHost.ConfigureKestrel(options => + { + options.ListenLocalhost(port, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + listenOptions.UseHttps(cert); + }); + }); + + // Suppress default logging noise in tests + builder.Logging.ClearProviders(); + + var app = builder.Build(); + + var server = new MockPveServer(app, port); + + // Request tracking middleware + app.Use(async (context, next) => + { + string? body = null; + if (context.Request.ContentLength > 0 || context.Request.Headers.ContainsKey("Transfer-Encoding")) + { + context.Request.EnableBuffering(); + using var reader = new StreamReader(context.Request.Body, leaveOpen: true); + body = await reader.ReadToEndAsync(); + context.Request.Body.Position = 0; + } + server.Requests.Add(new RequestRecord( + context.Request.Method, + context.Request.Path + context.Request.QueryString, + body, + DateTime.UtcNow)); + await next(); + }); + + // Auth middleware + app.Use(async (context, next) => + { + var path = context.Request.Path.Value ?? ""; + var method = context.Request.Method; + + // Skip auth for ticket creation and version endpoints + bool skipAuth = (path == "/api2/json/access/ticket" && method == "POST") + || (path == "/api2/json/access/ticket" && method == "DELETE") + || (path == "/api2/json/version" && method == "GET"); + + if (!skipAuth) + { + var hasCookie = context.Request.Headers["Cookie"].ToString().Contains("PVEAuthCookie="); + var hasToken = context.Request.Headers["Authorization"].ToString().StartsWith("PVEAPIToken="); + + if (!hasCookie && !hasToken) + { + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync("{\"data\":null,\"errors\":{\"authentication\":\"invalid credentials\"}}"); + return; + } + } + + await next(); + }); + + // Register all routes + app.MapAuthRoutes(); + app.MapVersionRoutes(); + app.MapNodeRoutes(); + app.MapVmRoutes(); + app.MapContainerRoutes(); + app.MapStorageRoutes(); + app.MapNetworkRoutes(); + app.MapUserRoutes(); + app.MapClusterRoutes(); + app.MapTaskRoutes(server); + app.MapSnapshotRoutes(); + + app.StartAsync().GetAwaiter().GetResult(); + + // Resolve the actual port (important when port=0 for random assignment) + var addresses = app.Urls; + var actualPort = port; + var serverInstance = app.Services.GetRequiredService(); + if (serverInstance != null) + { + var addressFeature = serverInstance.Features.Get(); + if (addressFeature != null) + { + var addr = addressFeature.Addresses.FirstOrDefault(); + if (addr != null) + { + var uri = new Uri(addr); + actualPort = uri.Port; + } + } + } + + return new MockPveServer(app, actualPort); + } + + public void Reset() + { + Requests.Clear(); + TaskStatusCallCounts.Clear(); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + _app.StopAsync().GetAwaiter().GetResult(); + _app.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + } + + public static string GetFixture(string name) + { + if (_fixtures.TryGetValue(name, out var content)) + return content; + return "{\"data\":null}"; + } + + public static string GenerateUpid(string node, string type, int vmid = 0) + => $"UPID:{node}:{Guid.NewGuid():N}:{DateTimeOffset.UtcNow.ToUnixTimeSeconds():X}:{type}:{vmid}:root@pam:"; + + private static X509Certificate2 GenerateSelfSignedCert() + { + var rsa = RSA.Create(2048); + var req = new CertificateRequest("CN=pve-mock", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + return cert; + } + + private static void LoadFixtures() + { + lock (_fixtureLock) + { + if (_fixtures.Count > 0) return; + + // Resolve fixtures path relative to the assembly location + var basePath = AppContext.BaseDirectory; + var fixturesPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..", "PSProxmoxVE.Core.Tests", "Fixtures")); + + // Fallback: try relative to the project source directory + if (!Directory.Exists(fixturesPath)) + { + fixturesPath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "PSProxmoxVE.Core.Tests", "Fixtures")); + } + + if (!Directory.Exists(fixturesPath)) + { + // Last resort: look from the working directory + fixturesPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "PSProxmoxVE.Core.Tests", "Fixtures")); + } + + if (Directory.Exists(fixturesPath)) + { + foreach (var file in Directory.GetFiles(fixturesPath, "*.json")) + { + var name = Path.GetFileNameWithoutExtension(file); + _fixtures[name] = File.ReadAllText(file); + } + } + } + } +} diff --git a/tests/PSProxmoxVE.MockServer/PSProxmoxVE.MockServer.csproj b/tests/PSProxmoxVE.MockServer/PSProxmoxVE.MockServer.csproj new file mode 100644 index 0000000..a3a34b6 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/PSProxmoxVE.MockServer.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/tests/PSProxmoxVE.MockServer/Program.cs b/tests/PSProxmoxVE.MockServer/Program.cs new file mode 100644 index 0000000..b4fcc6e --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Program.cs @@ -0,0 +1,22 @@ +using PSProxmoxVE.MockServer; + +// When run standalone, start on a fixed port +using var server = MockPveServer.Start(port: 8006); +Console.WriteLine($"Mock Proxmox VE API server running at {server.BaseUrl}"); +Console.WriteLine("Press Ctrl+C to stop."); + +var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); +}; + +try +{ + await Task.Delay(Timeout.Infinite, cts.Token); +} +catch (OperationCanceledException) +{ + // Graceful shutdown +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/AuthRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/AuthRoutes.cs new file mode 100644 index 0000000..bf56688 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/AuthRoutes.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class AuthRoutes +{ + public static WebApplication MapAuthRoutes(this WebApplication app) + { + app.MapPost("/api2/json/access/ticket", () => + Results.Content(MockPveServer.GetFixture("pve9_ticket_response"), "application/json")); + + app.MapDelete("/api2/json/access/ticket", () => + Results.Content("{\"data\":null}", "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/ClusterRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/ClusterRoutes.cs new file mode 100644 index 0000000..b7a839e --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/ClusterRoutes.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class ClusterRoutes +{ + public static WebApplication MapClusterRoutes(this WebApplication app) + { + app.MapGet("/api2/json/cluster/status", () => + Results.Content(MockPveServer.GetFixture("pve9_cluster_status"), "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/ContainerRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/ContainerRoutes.cs new file mode 100644 index 0000000..513090e --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/ContainerRoutes.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class ContainerRoutes +{ + public static WebApplication MapContainerRoutes(this WebApplication app) + { + app.MapGet("/api2/json/nodes/{node}/lxc", (string node) => + Results.Content(MockPveServer.GetFixture("pve9_containers"), "application/json")); + + app.MapGet("/api2/json/nodes/{node}/lxc/{vmid}/config", (string node, int vmid) => + Results.Content("{\"data\":{\"hostname\":\"test\",\"memory\":512,\"cores\":1}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/lxc", (string node) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "vzcreate")}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/lxc/{vmid}/status/start", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "vzstart", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/lxc/{vmid}/status/stop", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "vzstop", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/lxc/{vmid}/status/shutdown", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "vzshutdown", vmid)}\"}}", "application/json")); + + app.MapPut("/api2/json/nodes/{node}/lxc/{vmid}/config", (string node, int vmid) => + Results.Content("{\"data\":null}", "application/json")); + + app.MapDelete("/api2/json/nodes/{node}/lxc/{vmid}", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "vzdestroy", vmid)}\"}}", "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/NetworkRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/NetworkRoutes.cs new file mode 100644 index 0000000..58d42a8 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/NetworkRoutes.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class NetworkRoutes +{ + public static WebApplication MapNetworkRoutes(this WebApplication app) + { + app.MapGet("/api2/json/nodes/{node}/network", (string node) => + Results.Content(MockPveServer.GetFixture("pve9_networks"), "application/json")); + + app.MapPost("/api2/json/nodes/{node}/network", (string node) => + Results.Content("{\"data\":null}", "application/json")); + + app.MapPut("/api2/json/nodes/{node}/network/{iface}", (string node, string iface) => + Results.Content("{\"data\":null}", "application/json")); + + app.MapDelete("/api2/json/nodes/{node}/network/{iface}", (string node, string iface) => + Results.Content("{\"data\":null}", "application/json")); + + // Apply network config + app.MapPut("/api2/json/nodes/{node}/network", (string node) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "netapply")}\"}}", "application/json")); + + // SDN routes + app.MapGet("/api2/json/cluster/sdn/zones", () => + Results.Content(MockPveServer.GetFixture("pve9_sdn_zones"), "application/json")); + + app.MapGet("/api2/json/cluster/sdn/vnets", () => + Results.Content(MockPveServer.GetFixture("pve9_sdn_vnets"), "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/NodeRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/NodeRoutes.cs new file mode 100644 index 0000000..be3bdd2 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/NodeRoutes.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class NodeRoutes +{ + public static WebApplication MapNodeRoutes(this WebApplication app) + { + app.MapGet("/api2/json/nodes", () => + Results.Content(MockPveServer.GetFixture("pve9_nodes"), "application/json")); + + app.MapGet("/api2/json/nodes/{node}/status", (string node) => + Results.Content(MockPveServer.GetFixture("pve9_node_status"), "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/SnapshotRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/SnapshotRoutes.cs new file mode 100644 index 0000000..6fbdb29 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/SnapshotRoutes.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class SnapshotRoutes +{ + public static WebApplication MapSnapshotRoutes(this WebApplication app) + { + app.MapGet("/api2/json/nodes/{node}/qemu/{vmid}/snapshot", (string node, int vmid) => + Results.Content(MockPveServer.GetFixture("pve9_snapshots"), "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/snapshot", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmsnapshot", vmid)}\"}}", "application/json")); + + app.MapDelete("/api2/json/nodes/{node}/qemu/{vmid}/snapshot/{snapname}", (string node, int vmid, string snapname) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmdelsnapshot", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/snapshot/{snapname}/rollback", (string node, int vmid, string snapname) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmrollback", vmid)}\"}}", "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/StorageRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/StorageRoutes.cs new file mode 100644 index 0000000..44d075b --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/StorageRoutes.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class StorageRoutes +{ + public static WebApplication MapStorageRoutes(this WebApplication app) + { + app.MapGet("/api2/json/storage", () => + Results.Content(MockPveServer.GetFixture("pve9_storage"), "application/json")); + + app.MapGet("/api2/json/nodes/{node}/storage/{storage}/content", (string node, string storage) => + Results.Content(MockPveServer.GetFixture("pve9_storage_content"), "application/json")); + + app.MapPost("/api2/json/nodes/{node}/storage/{storage}/upload", (string node, string storage) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "upload")}\"}}", "application/json")) + .DisableAntiforgery(); + + app.MapPost("/api2/json/storage", () => + Results.Content("{\"data\":null}", "application/json")); + + app.MapDelete("/api2/json/storage/{storage}", (string storage) => + Results.Content("{\"data\":null}", "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/TaskRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/TaskRoutes.cs new file mode 100644 index 0000000..55a64c7 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/TaskRoutes.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class TaskRoutes +{ + public static WebApplication MapTaskRoutes(this WebApplication app, MockPveServer server) + { + app.MapGet("/api2/json/nodes/{node}/tasks/{upid}/status", (string node, string upid) => + { + // Track call count per UPID: first call returns running, subsequent return completed + int count; + lock (server.TaskStatusCallCounts) + { + if (!server.TaskStatusCallCounts.TryGetValue(upid, out count)) + count = 0; + count++; + server.TaskStatusCallCounts[upid] = count; + } + + if (count <= 1) + return Results.Content(MockPveServer.GetFixture("pve9_task_running"), "application/json"); + else + return Results.Content(MockPveServer.GetFixture("pve9_tasks"), "application/json"); + }); + + app.MapGet("/api2/json/nodes/{node}/tasks/{upid}/log", (string node, string upid) => + Results.Content("{\"data\":[{\"n\":1,\"t\":\"starting task\"}]}", "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/UserRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/UserRoutes.cs new file mode 100644 index 0000000..1d74b78 --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/UserRoutes.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class UserRoutes +{ + public static WebApplication MapUserRoutes(this WebApplication app) + { + app.MapGet("/api2/json/access/users", () => + Results.Content(MockPveServer.GetFixture("pve9_users"), "application/json")); + + app.MapGet("/api2/json/access/users/{userid}", (string userid) => + { + // Return first user from fixture + var fixture = MockPveServer.GetFixture("pve9_users"); + try + { + var doc = System.Text.Json.JsonDocument.Parse(fixture); + var dataArray = doc.RootElement.GetProperty("data"); + var first = dataArray[0]; + return Results.Content($"{{\"data\":{first.GetRawText()}}}", "application/json"); + } + catch + { + return Results.Content("{\"data\":null}", "application/json"); + } + }); + + app.MapPost("/api2/json/access/users", () => + Results.Content("{\"data\":null}", "application/json")); + + app.MapPut("/api2/json/access/users/{userid}", (string userid) => + Results.Content("{\"data\":null}", "application/json")); + + app.MapDelete("/api2/json/access/users/{userid}", (string userid) => + Results.Content("{\"data\":null}", "application/json")); + + // Roles + app.MapGet("/api2/json/access/roles", () => + Results.Content(MockPveServer.GetFixture("pve9_roles"), "application/json")); + + app.MapPost("/api2/json/access/roles", () => + Results.Content("{\"data\":null}", "application/json")); + + app.MapDelete("/api2/json/access/roles/{roleid}", (string roleid) => + Results.Content("{\"data\":null}", "application/json")); + + // ACL + app.MapGet("/api2/json/access/acl", () => + Results.Content("{\"data\":[]}", "application/json")); + + app.MapPut("/api2/json/access/acl", () => + Results.Content("{\"data\":null}", "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/VersionRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/VersionRoutes.cs new file mode 100644 index 0000000..0b34f5e --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/VersionRoutes.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class VersionRoutes +{ + public static WebApplication MapVersionRoutes(this WebApplication app) + { + app.MapGet("/api2/json/version", () => + Results.Content(MockPveServer.GetFixture("pve9_version"), "application/json")); + + return app; + } +} diff --git a/tests/PSProxmoxVE.MockServer/Routes/VmRoutes.cs b/tests/PSProxmoxVE.MockServer/Routes/VmRoutes.cs new file mode 100644 index 0000000..fd3a08c --- /dev/null +++ b/tests/PSProxmoxVE.MockServer/Routes/VmRoutes.cs @@ -0,0 +1,77 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace PSProxmoxVE.MockServer.Routes; + +public static class VmRoutes +{ + public static WebApplication MapVmRoutes(this WebApplication app) + { + app.MapGet("/api2/json/nodes/{node}/qemu", (string node) => + Results.Content(MockPveServer.GetFixture("pve9_vms"), "application/json")); + + app.MapGet("/api2/json/nodes/{node}/qemu/{vmid}/config", (string node, int vmid) => + Results.Content(MockPveServer.GetFixture("pve9_vm_config"), "application/json")); + + app.MapGet("/api2/json/nodes/{node}/qemu/{vmid}/status/current", (string node, int vmid) => + { + // Return first VM from fixture as a single-item response + var fixture = MockPveServer.GetFixture("pve9_vms"); + try + { + var doc = System.Text.Json.JsonDocument.Parse(fixture); + var dataArray = doc.RootElement.GetProperty("data"); + foreach (var item in dataArray.EnumerateArray()) + { + if (item.TryGetProperty("vmid", out var id) && id.GetInt32() == vmid) + return Results.Content($"{{\"data\":{item.GetRawText()}}}", "application/json"); + } + // If no match, return first item + var first = dataArray[0]; + return Results.Content($"{{\"data\":{first.GetRawText()}}}", "application/json"); + } + catch + { + return Results.Content("{\"data\":null}", "application/json"); + } + }); + + app.MapPost("/api2/json/nodes/{node}/qemu", (string node) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmcreate")}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/status/start", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmstart", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/status/stop", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmstop", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/status/shutdown", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmshutdown", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/status/reset", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmreset", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/status/suspend", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmsuspend", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/status/resume", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmresume", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/clone", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmclone", vmid)}\"}}", "application/json")); + + app.MapPost("/api2/json/nodes/{node}/qemu/{vmid}/migrate", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmigrate", vmid)}\"}}", "application/json")); + + app.MapPut("/api2/json/nodes/{node}/qemu/{vmid}/config", (string node, int vmid) => + Results.Content("{\"data\":null}", "application/json")); + + app.MapPut("/api2/json/nodes/{node}/qemu/{vmid}/resize", (string node, int vmid) => + Results.Content("{\"data\":null}", "application/json")); + + app.MapDelete("/api2/json/nodes/{node}/qemu/{vmid}", (string node, int vmid) => + Results.Content($"{{\"data\":\"{MockPveServer.GenerateUpid(node, "qmdestroy", vmid)}\"}}", "application/json")); + + return app; + } +}