mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-02 10:35:00 +03:00
fix(webapi): preserve Unix environment names and isolate test fixtures (#10455)
* fix(webapi): preserve Unix environment names and isolate local test fixtures * test(webapi): isolate launcher fixtures from host process IDs * test(webapi): allow auth fixture scheduling headroom
This commit is contained in:
committed by
GitHub
parent
ebabf79017
commit
743eb323c2
@@ -109,7 +109,8 @@ public static class LauncherEnvironment
|
||||
public static Dictionary<string, string?> ReadCurrentEnvironment() =>
|
||||
Environment.GetEnvironmentVariables()
|
||||
.Cast<System.Collections.DictionaryEntry>()
|
||||
.ToDictionary(entry => (string)entry.Key, entry => entry.Value?.ToString(), StringComparer.OrdinalIgnoreCase);
|
||||
.ToDictionary(entry => (string)entry.Key, entry => entry.Value?.ToString(),
|
||||
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
|
||||
|
||||
private static bool HasValue(IReadOnlyDictionary<string, string?> environment, string key) =>
|
||||
environment.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value);
|
||||
|
||||
@@ -178,7 +178,8 @@ public class CoreProcessTrackerTests
|
||||
|
||||
private static string CopySleepExecutable(string directory)
|
||||
{
|
||||
var executable = Path.Combine(directory, "sleep-core-test");
|
||||
// Multicall implementations dispatch by basename, so preserve the sleep utility name.
|
||||
var executable = Path.Combine(directory, "sleep");
|
||||
File.Copy("/bin/sleep", executable);
|
||||
return executable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Collections;
|
||||
using v2rayN.WebAPI.Launcher;
|
||||
|
||||
namespace v2rayN.WebAPI.Tests;
|
||||
|
||||
[NotInParallel]
|
||||
public class LauncherEnvironmentTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments("same-value", "same-value")]
|
||||
[Arguments("upper-value", "lower-value")]
|
||||
public async Task CurrentEnvironmentPreservesPlatformVariableNameSemantics(string upperValue, string lowerValue)
|
||||
{
|
||||
var upperName = $"V2RAYN_ENVIRONMENT_TEST_{Guid.NewGuid():N}".ToUpperInvariant();
|
||||
var lowerName = upperName.ToLowerInvariant();
|
||||
using var environmentScope = new TemporaryEnvironment((upperName, upperValue), (lowerName, lowerValue));
|
||||
|
||||
var environment = LauncherEnvironment.ReadCurrentEnvironment();
|
||||
|
||||
await environment[upperName].Should().BeEqualTo(OperatingSystem.IsWindows() ? lowerValue : upperValue);
|
||||
await environment[lowerName].Should().BeEqualTo(lowerValue);
|
||||
await environment.Keys.Count(key => string.Equals(key, upperName, StringComparison.OrdinalIgnoreCase))
|
||||
.Should().BeEqualTo(OperatingSystem.IsWindows() ? 1 : 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("V2RAYN_WEB_API_KEY")]
|
||||
[Arguments("LC_ALL")]
|
||||
public async Task CurrentEnvironmentRequiresCanonicalSettingNamesOnUnix(string canonicalName)
|
||||
{
|
||||
var lowerName = canonicalName.ToLowerInvariant();
|
||||
using var environmentScope = new TemporaryEnvironment((canonicalName, null), (lowerName, "wrong-case-value"));
|
||||
|
||||
var environment = LauncherEnvironment.ReadCurrentEnvironment();
|
||||
|
||||
await environment.GetValueOrDefault(canonicalName).Should()
|
||||
.BeEqualTo(OperatingSystem.IsWindows() ? "wrong-case-value" : null);
|
||||
await environment[lowerName].Should().BeEqualTo("wrong-case-value");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CurrentEnvironmentKeepsCanonicalManagementKeyDistinctOnUnix()
|
||||
{
|
||||
using var environmentScope = new TemporaryEnvironment(
|
||||
("V2RAYN_WEB_API_KEY", "canonical-management-key-123456"),
|
||||
("v2rayn_web_api_key", "other-management-key-123456"));
|
||||
|
||||
var environment = LauncherEnvironment.ReadCurrentEnvironment();
|
||||
|
||||
await environment.GetValueOrDefault("V2RAYN_WEB_API_KEY").Should()
|
||||
.BeEqualTo(OperatingSystem.IsWindows() ? "other-management-key-123456" : "canonical-management-key-123456");
|
||||
await environment["v2rayn_web_api_key"].Should().BeEqualTo("other-management-key-123456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CurrentEnvironmentUsesCanonicalInteractiveLaunchMarkerOnUnix()
|
||||
{
|
||||
using var environmentScope = new TemporaryEnvironment(
|
||||
(LauncherEnvironment.InteractiveLaunchEnvironmentVariable, null),
|
||||
(LauncherEnvironment.InteractiveLaunchEnvironmentVariable.ToLowerInvariant(), "1"));
|
||||
|
||||
var environment = LauncherEnvironment.ReadCurrentEnvironment();
|
||||
|
||||
await LauncherEnvironment.IsInteractiveLaunchMarker(environment).Should().BeEqualTo(OperatingSystem.IsWindows());
|
||||
}
|
||||
|
||||
private sealed class TemporaryEnvironment : IDisposable
|
||||
{
|
||||
private readonly string[] _names;
|
||||
private readonly (string Name, string? Value)[] _originalValues;
|
||||
|
||||
public TemporaryEnvironment(params (string Name, string? Value)[] values)
|
||||
{
|
||||
_names = values.Select(value => value.Name).ToArray();
|
||||
var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||
_originalValues = Environment.GetEnvironmentVariables()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Where(entry => _names.Contains((string)entry.Key, comparer))
|
||||
.Select(entry => ((string)entry.Key, entry.Value?.ToString()))
|
||||
.ToArray();
|
||||
|
||||
foreach (var (name, value) in values)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var name in _names)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
foreach (var (name, value) in _originalValues)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,7 +495,8 @@ public class WebAuthEndpointTests
|
||||
var client = new HttpClient(new SocketsHttpHandler { UseProxy = false })
|
||||
{
|
||||
BaseAddress = new Uri(addresses.Single()),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
// Allow headroom for real authentication work under parallel test load.
|
||||
Timeout = TimeSpan.FromSeconds(20),
|
||||
};
|
||||
return new ApiHarness(app, client, app.Services.GetRequiredService<EventHub>());
|
||||
}
|
||||
|
||||
@@ -34,9 +34,7 @@ public class WebAuthRateLimitTests
|
||||
await app.StartAsync();
|
||||
try
|
||||
{
|
||||
var server = app.Services.GetRequiredService<IServer>();
|
||||
var address = server.Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
using var client = new HttpClient { BaseAddress = new Uri(address), Timeout = TimeSpan.FromSeconds(20) };
|
||||
using var client = CreateClient(app);
|
||||
var statuses = new List<HttpStatusCode>();
|
||||
int? retryAfterSeconds = null;
|
||||
|
||||
@@ -118,6 +116,38 @@ public class WebAuthRateLimitTests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[NotInParallel]
|
||||
public async Task LocalRateLimitRequestsDoNotUseTheDefaultProxy()
|
||||
{
|
||||
var proxyBuilder = WebApplication.CreateBuilder();
|
||||
proxyBuilder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, 0));
|
||||
await using var proxy = proxyBuilder.Build();
|
||||
proxy.Run(context =>
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status502BadGateway;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await proxy.StartAsync();
|
||||
await using var app = CreatePartitionTestApp();
|
||||
await app.StartAsync();
|
||||
var proxyAddress = proxy.Services.GetRequiredService<IServer>()
|
||||
.Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
var previousProxy = HttpClient.DefaultProxy;
|
||||
try
|
||||
{
|
||||
HttpClient.DefaultProxy = new FixedProxy(new Uri(proxyAddress));
|
||||
using var client = CreateClient(app);
|
||||
await (await GetLimitedStatusAsync(client, "/setup-limited", "192.0.2.1", 0))
|
||||
.Should().BeEqualTo(HttpStatusCode.OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClient.DefaultProxy = previousProxy;
|
||||
await app.StopAsync();
|
||||
await proxy.StopAsync();
|
||||
}
|
||||
}
|
||||
private static WebApplication CreatePartitionTestApp()
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
@@ -149,7 +179,11 @@ public class WebAuthRateLimitTests
|
||||
{
|
||||
var server = app.Services.GetRequiredService<IServer>();
|
||||
var address = server.Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
return new HttpClient { BaseAddress = new Uri(address), Timeout = TimeSpan.FromSeconds(20) };
|
||||
return new HttpClient(new SocketsHttpHandler { UseProxy = false })
|
||||
{
|
||||
BaseAddress = new Uri(address),
|
||||
Timeout = TimeSpan.FromSeconds(20),
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<HttpStatusCode> GetLimitedStatusAsync(HttpClient client, string endpoint, string remoteIp, int requestIndex)
|
||||
@@ -163,6 +197,12 @@ public class WebAuthRateLimitTests
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
private sealed class FixedProxy(Uri address) : IWebProxy
|
||||
{
|
||||
public ICredentials? Credentials { get; set; }
|
||||
public Uri GetProxy(Uri destination) => address;
|
||||
public bool IsBypassed(Uri host) => false;
|
||||
}
|
||||
private sealed class TemporaryDirectory : IDisposable
|
||||
{
|
||||
public TemporaryDirectory()
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace v2rayN.WebAPI.Tests;
|
||||
|
||||
public class WebLauncherTests
|
||||
{
|
||||
// Keep fixture PIDs outside Linux's assignable range so host services cannot affect stop tests.
|
||||
private const int FakeOwnerProcessId = int.MaxValue - 1;
|
||||
private const int OtherProcessId = int.MaxValue;
|
||||
|
||||
[Test]
|
||||
public async Task InstanceLockOwnerCanBeReadWhileAnotherProcessHoldsTheLock()
|
||||
{
|
||||
@@ -228,13 +232,13 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
await File.WriteAllTextAsync(lockPath, "123\n");
|
||||
await File.WriteAllTextAsync(lockPath, $"{FakeOwnerProcessId}\n");
|
||||
var signals = new FakeSignalSender();
|
||||
var stopper = new WebStopper(new FakeHealthProbe(true, 123), signals);
|
||||
var stopper = new WebStopper(new FakeHealthProbe(true, FakeOwnerProcessId), signals);
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.NotRunning).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.NotRunning);
|
||||
await (signals.ProcessIds.Count == 0).Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -245,11 +249,11 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
using (heldLock)
|
||||
{
|
||||
var signals = new FakeSignalSender();
|
||||
var health = new FakeHealthProbe(true, 123);
|
||||
var health = new FakeHealthProbe(true, FakeOwnerProcessId);
|
||||
signals.OnSignal = processId =>
|
||||
{
|
||||
_ = ReleaseLockAfterDelayAsync(heldLock, health);
|
||||
@@ -262,8 +266,8 @@ public class WebLauncherTests
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.Stopped).Should().BeTrue();
|
||||
await signals.ProcessIds.SequenceEqual([123]).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.Stopped);
|
||||
await signals.ProcessIds.SequenceEqual([FakeOwnerProcessId]).Should().BeTrue();
|
||||
await (health.ProbeCount >= 2).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -282,18 +286,18 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
using (heldLock)
|
||||
{
|
||||
var health = new FakeHealthProbe(true, 123, [Environment.ProcessId]);
|
||||
var health = new FakeHealthProbe(true, FakeOwnerProcessId, [Environment.ProcessId]);
|
||||
var signals = new FakeSignalSender();
|
||||
signals.OnSignal = processId => _ = ReleaseLockAfterDelayAsync(heldLock, health);
|
||||
var stopper = new WebStopper(health, signals, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(10));
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.CoreProcessStillRunning).Should().BeTrue();
|
||||
await signals.ProcessIds.SequenceEqual([123]).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.CoreProcessStillRunning);
|
||||
await signals.ProcessIds.SequenceEqual([FakeOwnerProcessId]).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,13 +308,13 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
var signals = new FakeSignalSender();
|
||||
var stopper = new WebStopper(new FakeHealthProbe(true, 456), signals);
|
||||
var stopper = new WebStopper(new FakeHealthProbe(true, OtherProcessId), signals);
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.IdentityUnverified).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.IdentityUnverified);
|
||||
await (signals.ProcessIds.Count == 0).Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -321,13 +325,13 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
var signals = new FakeSignalSender();
|
||||
var stopper = new WebStopper(new FakeHealthProbe(false, 123), signals);
|
||||
var stopper = new WebStopper(new FakeHealthProbe(false, FakeOwnerProcessId), signals);
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.IdentityUnverified).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.IdentityUnverified);
|
||||
await (signals.ProcessIds.Count == 0).Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -338,18 +342,18 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
var signals = new FakeSignalSender();
|
||||
var stopper = new WebStopper(new FakeHealthProbe(true, 123), signals);
|
||||
var stopper = new WebStopper(new FakeHealthProbe(true, FakeOwnerProcessId), signals);
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, healthUri: null);
|
||||
|
||||
await (result == WebStopResult.ProbeUnavailable).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.ProbeUnavailable);
|
||||
await (signals.ProcessIds.Count == 0).Should().BeTrue();
|
||||
|
||||
var notRunning = await stopper.StopAsync(
|
||||
Path.Combine(directory.Path, "missing-instance.lock"), healthUri: null);
|
||||
await (notRunning == WebStopResult.NotRunning).Should().BeTrue();
|
||||
await notRunning.Should().BeEqualTo(WebStopResult.NotRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -359,18 +363,18 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
var signals = new FakeSignalSender();
|
||||
var stopper = new WebStopper(
|
||||
new FakeHealthProbe(true, 123),
|
||||
new FakeHealthProbe(true, FakeOwnerProcessId),
|
||||
signals,
|
||||
stopTimeout: TimeSpan.FromMilliseconds(60),
|
||||
pollInterval: TimeSpan.FromMilliseconds(5));
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.TimedOut).Should().BeTrue();
|
||||
await signals.ProcessIds.SequenceEqual([123]).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.TimedOut);
|
||||
await signals.ProcessIds.SequenceEqual([FakeOwnerProcessId]).Should().BeTrue();
|
||||
await WebInstanceLock.IsHeld(lockPath).Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -381,8 +385,8 @@ public class WebLauncherTests
|
||||
|
||||
using var directory = new TemporaryDirectory();
|
||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, 123);
|
||||
var health = new FakeHealthProbe(true, 123);
|
||||
using var heldLock = await AcquireLockWithOwnerAsync(lockPath, FakeOwnerProcessId);
|
||||
var health = new FakeHealthProbe(true, FakeOwnerProcessId);
|
||||
health.SetShutdownStage("Core stop");
|
||||
var stopper = new WebStopper(
|
||||
health,
|
||||
@@ -392,7 +396,7 @@ public class WebLauncherTests
|
||||
|
||||
var result = await stopper.StopAsync(lockPath, new Uri("http://127.0.0.1:5080/api/health"));
|
||||
|
||||
await (result == WebStopResult.TimedOut).Should().BeTrue();
|
||||
await result.Should().BeEqualTo(WebStopResult.TimedOut);
|
||||
await (stopper.LastObservedShutdownStage == "Core stop").Should().BeTrue();
|
||||
await LauncherMessages.StopMessage(result, LauncherLocale.English, stopper.LastObservedShutdownStage)
|
||||
.Contains("Core stop", StringComparison.Ordinal).Should().BeTrue();
|
||||
|
||||
Reference in New Issue
Block a user