0954876295
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
153 lines
9.5 KiB
C#
153 lines
9.5 KiB
C#
using System.Data.Common;
|
|
using System.Net;
|
|
using System.Text.Json;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Services.JobImport;
|
|
using JobTrackerApi.Services.JobImport.Translation;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
|
|
namespace IndependentAuditValidation;
|
|
|
|
public static class Entry
|
|
{
|
|
public static async Task Main(string[] args)
|
|
{
|
|
if (args.Any(arg => arg.StartsWith("--print-to-pdf=")))
|
|
{
|
|
// Inert browser substitute. It opens no network connection and renders no HTML.
|
|
var root = Environment.GetEnvironmentVariable("VALIDATION_FAKE_BROWSER_ROOT")!;
|
|
await File.WriteAllTextAsync(Path.Combine(root, Environment.ProcessId + ".started"), "started");
|
|
for (var i = 0; i < 100 && !File.Exists(Path.Combine(root, "release")); i++) await Task.Delay(50);
|
|
await File.WriteAllTextAsync(args.Single(arg => arg.StartsWith("--print-to-pdf="))[15..], "%PDF-synthetic-launch-probe");
|
|
return;
|
|
}
|
|
var output = new List<object>();
|
|
foreach (var target in new[] { "http://10.1.2.3/", "http://[::ffff:10.1.2.3]/", "http://169.254.169.254/", "http://[::ffff:169.254.169.254]/", "http://192.168.1.1/", "http://[::ffff:192.168.1.1]/", "http://[::1]/", "http://[::ffff:127.0.0.1]/" })
|
|
{
|
|
var handler = new RecordingHandler();
|
|
var clientFactory = new Mock<IHttpClientFactory>();
|
|
clientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(new HttpClient(handler));
|
|
var resolver = new Mock<IHostAddressResolver>();
|
|
resolver.Setup(x => x.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync([IPAddress.Parse("93.184.216.34")]);
|
|
var service = new JobImportService(clientFactory.Object, new UniversalJobParser(), Array.Empty<IJobSitePlugin>(), new NoOpTranslationService(), resolver.Object);
|
|
var result = await service.PreviewAsync(target, default);
|
|
output.Add(new { probe = "URL guard with non-network HTTP fake", target, handler.Calls, result.Parser, result.Error });
|
|
}
|
|
var folder = Path.Combine(Path.GetTempPath(), "jobjakt-boundary-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(folder);
|
|
var fakeRoot = Path.Combine(folder, "fake-browser");
|
|
Directory.CreateDirectory(fakeRoot);
|
|
var executable = Path.ChangeExtension(typeof(Entry).Assembly.Location, OperatingSystem.IsWindows() ? ".exe" : null);
|
|
Environment.SetEnvironmentVariable("CV_PDF_BROWSER_PATH", executable);
|
|
Environment.SetEnvironmentVariable("VALIDATION_FAKE_BROWSER_ROOT", fakeRoot);
|
|
var pdfConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["Data:Root"] = folder }).Build();
|
|
var environment = new Mock<IHostEnvironment>();
|
|
environment.SetupGet(x => x.ContentRootPath).Returns(folder);
|
|
var pdf = new PlaywrightCvPdfExporter(new AppPaths(pdfConfig, environment.Object), Microsoft.Extensions.Logging.Abstractions.NullLogger<PlaywrightCvPdfExporter>.Instance, pdfConfig);
|
|
var renders = Enumerable.Range(0, 3).Select(i => pdf.ExportAsync("synthetic-owner", new TailoredCvRenderResult("synthetic", "synthetic.pdf", "<p>Inert</p>"), default)).ToArray();
|
|
for (var i = 0; i < 100 && Directory.GetFiles(fakeRoot, "*.started").Length < 3; i++) await Task.Delay(50);
|
|
var concurrentLaunches = Directory.GetFiles(fakeRoot, "*.started").Length;
|
|
await File.WriteAllTextAsync(Path.Combine(fakeRoot, "release"), "release");
|
|
await Task.WhenAll(renders);
|
|
output.Add(new { probe = "actual PDF exporter with three inert child processes", concurrentLaunches });
|
|
var connectionString = "Data Source=" + Path.Combine(folder, "independent.db");
|
|
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connectionString).Options;
|
|
using (var db = new JobTrackerContext(options, new FixedOwner()))
|
|
{
|
|
await db.Database.EnsureCreatedAsync();
|
|
db.Users.Add(new ApplicationUser { Id = "synthetic-owner", UserName = "owner@example.test", NormalizedUserName = "OWNER@EXAMPLE.TEST" });
|
|
await db.SaveChangesAsync();
|
|
}
|
|
var career = new Mock<ICareerProfileService>().Object;
|
|
var renderer = new Mock<IThemedCvRenderer>().Object;
|
|
int id;
|
|
using (var db = new JobTrackerContext(options, new FixedOwner()))
|
|
id = (await new CvVariantService(db, career, renderer).CreateAsync("synthetic-owner", "Initial", null, new CvVariantSettings(), default)).Id;
|
|
using (var dbA = new JobTrackerContext(options, new FixedOwner()))
|
|
using (var dbB = new JobTrackerContext(options, new FixedOwner()))
|
|
{
|
|
var a = new CvVariantService(dbA, career, renderer);
|
|
var b = new CvVariantService(dbB, career, renderer);
|
|
await a.GetAsync("synthetic-owner", id, default);
|
|
await b.GetAsync("synthetic-owner", id, default);
|
|
await a.SaveAsync("synthetic-owner", id, "Writer A", new CvVariantSettings(), "a", default);
|
|
await b.SaveAsync("synthetic-owner", id, "Writer B", new CvVariantSettings(), "b", default);
|
|
}
|
|
using (var db = new JobTrackerContext(options, new FixedOwner()))
|
|
{
|
|
var current = await db.CvVariants.SingleAsync();
|
|
var versions = await db.CvVariantVersions.OrderBy(v => v.Id).Select(v => v.Version).ToListAsync();
|
|
output.Add(new { probe = "two preloaded real SQLite contexts", current.Name, current.Version, historyVersions = versions });
|
|
}
|
|
foreach (var concurrency in new[] { 1, 2 })
|
|
{
|
|
var fault = new OneShotDbFailure();
|
|
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Workers:AiOperationsEnabled"] = "true", ["AiQueue:WorkerConcurrency"] = concurrency.ToString(),
|
|
["AiQueue:IdleDelayMs"] = "100"
|
|
}).Build();
|
|
using var host = new HostBuilder().ConfigureLogging(l => l.ClearProviders()).ConfigureServices(services =>
|
|
{
|
|
services.AddSingleton<IConfiguration>(config);
|
|
services.AddSingleton<TimeProvider>(TimeProvider.System);
|
|
services.AddScoped<ICurrentUserService, NeutralOwner>();
|
|
services.AddDbContext<JobTrackerContext>(o => o.UseSqlite(connectionString).AddInterceptors(fault));
|
|
services.AddScoped<UserOperationStore>();
|
|
services.AddSingleton<AiPrivacyPolicy>();
|
|
services.AddSingleton<AiOperationExecutionScope>();
|
|
services.AddSingleton<IAiOperationHandler, UnusedHandler>();
|
|
services.AddSingleton<AiOperationWorker>();
|
|
services.AddSingleton<AiOperationHostedService>();
|
|
services.AddHostedService(p => p.GetRequiredService<AiOperationHostedService>());
|
|
}).Build();
|
|
await host.StartAsync();
|
|
await Task.Delay(150);
|
|
fault.Armed = true;
|
|
await Task.Delay(700);
|
|
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
|
|
var service = host.Services.GetRequiredService<AiOperationHostedService>();
|
|
output.Add(new { probe = "one transient database command failure", concurrency, fault.Failures, hostStopping = lifetime.ApplicationStopping.IsCancellationRequested, hostedTaskFaulted = service.ExecuteTask?.IsFaulted, error = service.ExecuteTask?.Exception?.GetBaseException().GetType().Name });
|
|
using var shutdown = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
|
await host.StopAsync(shutdown.Token);
|
|
}
|
|
Console.WriteLine(JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true }));
|
|
}
|
|
}
|
|
|
|
sealed class FixedOwner : ICurrentUserService { public string? UserId => "synthetic-owner"; }
|
|
sealed class NeutralOwner : ICurrentUserService { public string? UserId => null; }
|
|
sealed class RecordingHandler : HttpMessageHandler
|
|
{
|
|
public int Calls;
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
Calls++;
|
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("<html><body>Synthetic inert HTML; no sockets are opened.</body></html>") });
|
|
}
|
|
}
|
|
sealed class OneShotDbFailure : DbCommandInterceptor
|
|
{
|
|
public volatile bool Armed;
|
|
private int fired;
|
|
public int Failures => fired;
|
|
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
|
|
{
|
|
if (Armed && Interlocked.CompareExchange(ref fired, 1, 0) == 0) throw new TimeoutException("Synthetic one-shot database timeout");
|
|
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
|
|
}
|
|
}
|
|
sealed class UnusedHandler : IAiOperationHandler
|
|
{
|
|
public string TaskType => "synthetic.validation";
|
|
public Task<AiOperationExecutionResult> ExecuteAsync(AiOperationExecutionContext context, IServiceProvider services, CancellationToken cancellationToken) => throw new InvalidOperationException("No operations were queued in this probe.");
|
|
}
|