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>
113 lines
4.3 KiB
C#
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);
|
|
}
|
|
}
|