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; + } +}