Files
metin-launcher/tests/Metin2Launcher.Tests/UpdateOrchestratorFormatDispatchTests.cs
Jan Nedbal 0e95171e50 test: cover runtime key, release formats and telemetry
adds ~60 new tests across RuntimeKey parsing, EnvVarKeyDelivery, the
legacy and m2pack formats, ReleaseFormatFactory dispatch, manifest
loader tolerance of unknown top-level fields, orchestrator wiring and
the ClientAppliedReporter (disabled-by-default, success, 5xx, timeout,
connection refused). The telemetry tests spin up an in-process
HttpListener helper — no new NuGet dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:10:48 +02:00

113 lines
4.3 KiB
C#

using System.Text;
using System.Text.Json;
using Metin2Launcher.Config;
using Metin2Launcher.Formats;
using Metin2Launcher.Manifest;
using Metin2Launcher.Orchestration;
using Xunit;
using ManifestDto = Metin2Launcher.Manifest.Manifest;
namespace Metin2Launcher.Tests;
/// <summary>
/// Exercises the <see cref="UpdateOrchestrator"/>'s format-dispatch wiring.
///
/// The orchestrator short-circuits to a signature exception when the fetched
/// manifest isn't signed by the hardcoded <see cref="LauncherConfig.PublicKeyHex"/>.
/// We can't realistically override that constant in a test without widening
/// the surface area, so these tests focus on:
///
/// 1. The unit-level pieces the orchestrator relies on (factory, effective
/// format, file filtering, post-apply hook) — covered in sibling test
/// classes and asserted here for regression safety.
/// 2. End-to-end "manifest fetched but signature mismatches" showing the
/// orchestrator really reaches the verify stage against a synthetic
/// HTTP endpoint served by <see cref="TestHttpListener"/>.
/// </summary>
public class UpdateOrchestratorFormatDispatchTests
{
[Fact]
public async Task Bad_signature_throws_before_format_dispatch()
{
using var listener = new TestHttpListener();
var url = listener.Start();
var manifest = new ManifestDto
{
Format = "m2pack",
Version = "v1",
CreatedAt = "2026-04-14T00:00:00Z",
Launcher = new ManifestLauncherEntry { Path = "L.exe", Sha256 = "dead", Size = 1 },
};
var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest));
var badSig = new byte[64]; // deliberately wrong signature
listener.RespondBody(req => req.RawUrl!.EndsWith(".sig") ? badSig : body);
var clientRoot = Path.Combine(Path.GetTempPath(), "orch-" + Guid.NewGuid());
Directory.CreateDirectory(clientRoot);
try
{
using var http = new HttpClient();
var settings = new LauncherSettings
{
DevMode = true,
ManifestUrlOverride = url + "manifest.json",
};
var orchestrator = new UpdateOrchestrator(clientRoot, settings, http);
await Assert.ThrowsAsync<ManifestSignatureException>(async () =>
await orchestrator.RunAsync(new Progress<UpdateProgress>(_ => { }), CancellationToken.None));
}
finally { Directory.Delete(clientRoot, true); }
}
[Fact]
public async Task Manifest_fetch_failure_returns_offline_fallback()
{
using var listener = new TestHttpListener();
var url = listener.Start();
listener.RespondWith(System.Net.HttpStatusCode.NotFound);
var clientRoot = Path.Combine(Path.GetTempPath(), "orch-" + Guid.NewGuid());
Directory.CreateDirectory(clientRoot);
try
{
using var http = new HttpClient();
var settings = new LauncherSettings
{
DevMode = true,
ManifestUrlOverride = url + "manifest.json",
};
var orchestrator = new UpdateOrchestrator(clientRoot, settings, http);
var result = await orchestrator.RunAsync(
new Progress<UpdateProgress>(_ => { }),
CancellationToken.None);
Assert.False(result.Success);
Assert.Equal(LauncherState.OfflineFallback, result.FinalState);
Assert.Null(result.Format);
Assert.Null(result.RuntimeKey);
}
finally { Directory.Delete(clientRoot, true); }
}
[Fact]
public void Result_carries_format_and_key_slots()
{
// Contract test: the orchestrator Result exposes Format and RuntimeKey
// so the launcher can thread the runtime key into GameProcess.Launch.
var r = new UpdateOrchestrator.Result(true, LauncherState.UpToDate, null);
Assert.Null(r.Format);
Assert.Null(r.RuntimeKey);
}
[Fact]
public void Factory_falls_back_through_effective_format_on_empty_string()
{
// A manifest with format="" should resolve via EffectiveFormat -> legacy.
var m = new ManifestDto { Version = "v1", Format = "" };
var f = ReleaseFormatFactory.Resolve(m.EffectiveFormat);
Assert.IsType<LegacyJsonBlobFormat>(f);
}
}