FakeHttpServer.cs 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. using Microsoft.AspNetCore.Builder;
  2. using Microsoft.AspNetCore.Hosting;
  3. using Microsoft.AspNetCore.Http;
  4. using Microsoft.Extensions.Logging;
  5. namespace Tests.Mcp;
  6. /// <summary>
  7. /// A real Kestrel server on a random loopback port, serving captured API fixtures.
  8. /// The discovery tools construct their own HttpClients internally, so unlike the
  9. /// unit tests in Tests.Discovery a message-handler stub cannot reach them — the
  10. /// fake engine has to answer on an actual socket.
  11. /// </summary>
  12. internal sealed class FakeHttpServer : IAsyncDisposable {
  13. private readonly WebApplication _app;
  14. private FakeHttpServer(WebApplication app) => _app = app;
  15. /// <summary>e.g. http://127.0.0.1:49213 — no trailing slash.</summary>
  16. public string BaseUrl => _app.Urls.First();
  17. public string Host => new Uri(BaseUrl).Authority;
  18. public static async Task<FakeHttpServer> StartAsync(Action<WebApplication> map, bool ipv6 = false) {
  19. WebApplicationBuilder builder = WebApplication.CreateBuilder();
  20. builder.Logging.ClearProviders();
  21. // An IPv6-only endpoint is the one case where an engine answers but has no IPv4
  22. // address to record its services at.
  23. builder.WebHost.UseUrls(ipv6 ? "http://[::1]:0" : "http://127.0.0.1:0");
  24. WebApplication app = builder.Build();
  25. map(app);
  26. await app.StartAsync();
  27. return new FakeHttpServer(app);
  28. }
  29. /// <summary>A fake Docker Engine API with the shared captured fixtures.</summary>
  30. public static Task<FakeHttpServer> StartDockerEngineAsync(bool ipv6 = false) =>
  31. StartAsync(app => {
  32. app.MapGet("/containers/json", () => Results.Content(
  33. TestData.Fixture("docker-containers.json"), "application/json"));
  34. app.MapGet("/info", () => Results.Content(
  35. TestData.Fixture("docker-info.json"), "application/json"));
  36. }, ipv6);
  37. /// <summary>
  38. /// A fake Proxmox VE API. Both fixture nodes answer with the same guest lists,
  39. /// which doubles as the migration case: the mapper must dedupe guests by vmid.
  40. /// </summary>
  41. public static Task<FakeHttpServer> StartProxmoxAsync() =>
  42. StartAsync(app => {
  43. string Json(string name) => TestData.Fixture(name);
  44. app.MapGet("/api2/json/cluster/status", () => Results.Content(Json("pve-cluster-status.json"), "application/json"));
  45. app.MapGet("/api2/json/nodes", () => Results.Content(Json("pve-nodes-full.json"), "application/json"));
  46. app.MapGet("/api2/json/nodes/{node}/status", () => Results.Content(Json("pve-node-status.json"), "application/json"));
  47. app.MapGet("/api2/json/nodes/{node}/disks/list", () => Results.Content(Json("pve-disks.json"), "application/json"));
  48. app.MapGet("/api2/json/nodes/{node}/hardware/pci", () => Results.Content(Json("pve-hardware-pci.json"), "application/json"));
  49. app.MapGet("/api2/json/nodes/{node}/qemu", () => Results.Content(Json("pve-qemu.json"), "application/json"));
  50. app.MapGet("/api2/json/nodes/{node}/lxc", () => Results.Content(Json("pve-lxc.json"), "application/json"));
  51. app.MapGet("/api2/json/nodes/{node}/qemu/{vmid}/config", () => Results.Content(Json("pve-qemu-config.json"), "application/json"));
  52. app.MapGet("/api2/json/nodes/{node}/lxc/{vmid}/config", () => Results.Content(Json("pve-lxc-config.json"), "application/json"));
  53. });
  54. public async ValueTask DisposeAsync() => await _app.DisposeAsync();
  55. }