Files
Telegram-Panel/tests/TelegramPanel.Web.Tests/ProxyApiValidationTests.cs
meoacgx adcbdee7f3 feat: 导入账号复用已有 WARP 池
移除导入流程按账号创建独立 WARP,新增 warp_pool 自动选择现有 active WARP,并同步前后端合同、测试和文档。\n\n遇到错误: 2\n调试迭代: 6\n详见反思报告: docs/workflows/2026-07/26_reflection_feature_warp-pool.md
2026-07-26 04:22:09 +08:00

377 lines
14 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using TelegramPanel.Core.Interfaces;
using TelegramPanel.Core.Models;
using TelegramPanel.Core.Services.Proxy;
using TelegramPanel.Data;
using TelegramPanel.Data.Entities;
using TelegramPanel.Web.Api;
using TelegramPanel.Web.Services;
using WTelegram;
using Xunit;
namespace TelegramPanel.Web.Tests;
public sealed class ProxyApiValidationTests
{
[Fact]
public async Task ()
{
var warpUsageGuard = new AccountLoginProxyStateStore();
var temporaryWarpClaims = new TemporaryWarpClaimStore();
var enabled = await ProxyApiEndpoints.BatchSetEnabledAsync(
new ProxyBatchEnabledRequestDto(Array.Empty<int>(), true),
null!,
warpUsageGuard,
temporaryWarpClaims,
CancellationToken.None);
var tested = await ProxyApiEndpoints.BatchTestAsync(
new ProxyBatchIdsRequestDto(Array.Empty<int>()),
null!,
CancellationToken.None);
var deleted = await ProxyApiEndpoints.BatchDeleteAsync(
new ProxyBatchIdsRequestDto(Array.Empty<int>()),
null!,
warpUsageGuard,
temporaryWarpClaims,
CancellationToken.None);
Assert.Equal(StatusCodes.Status400BadRequest, StatusCode(enabled));
Assert.Equal(StatusCodes.Status400BadRequest, StatusCode(tested));
Assert.Equal(StatusCodes.Status400BadRequest, StatusCode(deleted));
}
[Fact]
public async Task 使()
{
await using var fixture = await WarpProxyFixture.CreateAsync();
var result = await ProxyApiEndpoints.ListAsync(
"invalid",
null,
fixture.Service,
CancellationToken.None);
Assert.Equal(StatusCodes.Status400BadRequest, StatusCode(result));
}
[Fact]
public async Task WARP时批量停用和删除均失败且数据库不变()
{
await using var fixture = await WarpProxyFixture.CreateAsync();
using var usageLease = fixture.WarpUsageGuard.TryAcquireUsage(fixture.ProxyId);
Assert.NotNull(usageLease);
await AssertBatchMutationsBlockedAsync(fixture);
}
[Fact]
public async Task WARP时批量停用和删除均失败且数据库不变()
{
await using var fixture = await WarpProxyFixture.CreateAsync();
using var requestClaim = fixture.TemporaryWarpClaims.ClaimRequest(fixture.RequestId);
await AssertBatchMutationsBlockedAsync(fixture);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void (string? strategy)
{
Assert.Null(PanelAdminApiEndpoints.ParseImportProxyBinding(strategy, "1"));
}
[Theory]
[InlineData("proxy_per_account")]
[InlineData(" PROXY_PER_ACCOUNT ")]
public void Zip导入入口解析(string strategy)
{
Assert.Null(PanelAdminApiEndpoints.ParseImportProxyBinding(strategy, "1"));
}
[Fact]
public void WARP池策略解析为无固定代理引用()
{
var binding = PanelAdminApiEndpoints.ParseImportProxyBinding("warp_pool", null);
Assert.NotNull(binding);
Assert.Equal("warp_pool", binding!.Strategy);
Assert.Null(binding.ProxyId);
}
private static async Task AssertBatchMutationsBlockedAsync(WarpProxyFixture fixture)
{
var expected = await fixture.ReadSnapshotAsync();
var disabled = await ProxyApiEndpoints.BatchSetEnabledAsync(
new ProxyBatchEnabledRequestDto(new[] { fixture.ProxyId }, false),
fixture.Service,
fixture.WarpUsageGuard,
fixture.TemporaryWarpClaims,
CancellationToken.None);
AssertBatchFailure(disabled, fixture.ProxyId);
Assert.Equal(expected, await fixture.ReadSnapshotAsync());
var deleted = await ProxyApiEndpoints.BatchDeleteAsync(
new ProxyBatchIdsRequestDto(new[] { fixture.ProxyId }),
fixture.Service,
fixture.WarpUsageGuard,
fixture.TemporaryWarpClaims,
CancellationToken.None);
AssertBatchFailure(deleted, fixture.ProxyId);
Assert.Equal(expected, await fixture.ReadSnapshotAsync());
}
private static void AssertBatchFailure(IResult result, int proxyId)
{
Assert.Equal(StatusCodes.Status200OK, StatusCode(result));
var valueResult = Assert.IsAssignableFrom<IValueHttpResult>(result);
var response = Assert.IsType<ProxyBatchResultDto>(valueResult.Value);
Assert.Equal(0, response.Success);
Assert.Equal(1, response.Failed);
var item = Assert.Single(response.Items);
Assert.Equal(proxyId, item.ProxyId);
Assert.False(item.Success);
Assert.Contains("WARP", item.Error);
Assert.Contains("首次连接", item.Error);
}
private static int? StatusCode(IResult result) =>
Assert.IsAssignableFrom<IStatusCodeHttpResult>(result).StatusCode;
private sealed record WarpDatabaseSnapshot(
int ProxyCount,
string ProxyName,
bool ProxyEnabled,
string ProxyTestStatus,
string? ProxyLastError,
DateTime ProxyUpdatedAtUtc,
int ProfileCount,
string ProfileStatus,
bool ProfileDesiredEnabled,
string? ProfileContainerId,
DateTime ProfileUpdatedAtUtc);
private sealed class WarpProxyFixture : IAsyncDisposable
{
private readonly SqliteConnection _connection;
private WarpProxyFixture(
SqliteConnection connection,
AppDbContext db,
ProxyManagementService service,
AccountLoginProxyStateStore warpUsageGuard,
TemporaryWarpClaimStore temporaryWarpClaims,
int proxyId,
string requestId)
{
_connection = connection;
Db = db;
Service = service;
WarpUsageGuard = warpUsageGuard;
TemporaryWarpClaims = temporaryWarpClaims;
ProxyId = proxyId;
RequestId = requestId;
}
public AppDbContext Db { get; }
public ProxyManagementService Service { get; }
public AccountLoginProxyStateStore WarpUsageGuard { get; }
public TemporaryWarpClaimStore TemporaryWarpClaims { get; }
public int ProxyId { get; }
public string RequestId { get; }
public static async Task<WarpProxyFixture> CreateAsync()
{
var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var db = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options);
await db.Database.EnsureCreatedAsync();
const string requestId = "import-account:proxy-api-validation";
var proxy = new OutboundProxy
{
Name = "WARP 回归测试代理",
Kind = OutboundProxyKinds.Warp,
Protocol = OutboundProxyProtocols.Http,
Host = "warp-proxy-api-validation",
Port = 1080,
IsEnabled = true,
TestStatus = "ok",
UpdatedAtUtc = DateTime.UtcNow.AddMinutes(-1)
};
var profile = new WarpProfile
{
ProfileId = "proxy-api-validation",
RequestId = requestId,
ContainerName = "telegram-panel-warp-proxy-api-validation",
ContainerId = "container-proxy-api-validation",
VolumeName = "telegram-panel-warp-data-proxy-api-validation",
HostPort = 42080,
Status = "active",
DesiredEnabled = true,
UpdatedAtUtc = DateTime.UtcNow.AddMinutes(-1),
Proxy = proxy
};
proxy.WarpProfile = profile;
db.OutboundProxies.Add(proxy);
db.WarpProfiles.Add(profile);
await db.SaveChangesAsync();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Proxy:Warp:Enabled"] = "true",
["Proxy:Warp:DockerSocketPath"] = Path.Combine(
Path.GetTempPath(),
"proxy-api-validation.sock")
})
.Build();
var probe = new ProxyEgressProbeService();
var warpManager = new WarpContainerManager(
db,
configuration,
probe,
NullLogger<WarpContainerManager>.Instance,
new NoopWarpDockerClientFactory());
var warpUsageGuard = new AccountLoginProxyStateStore();
var temporaryWarpClaims = new TemporaryWarpClaimStore();
var service = new ProxyManagementService(
db,
new NoopClientPool(),
probe,
warpManager,
NullLogger<ProxyManagementService>.Instance,
configuration,
temporaryWarpClaims,
warpUsageGuard);
return new WarpProxyFixture(
connection,
db,
service,
warpUsageGuard,
temporaryWarpClaims,
proxy.Id,
requestId);
}
public async Task<WarpDatabaseSnapshot> ReadSnapshotAsync()
{
Db.ChangeTracker.Clear();
var proxy = await Db.OutboundProxies.AsNoTracking().SingleAsync();
var profile = await Db.WarpProfiles.AsNoTracking().SingleAsync();
return new WarpDatabaseSnapshot(
await Db.OutboundProxies.CountAsync(),
proxy.Name,
proxy.IsEnabled,
proxy.TestStatus,
proxy.LastError,
proxy.UpdatedAtUtc,
await Db.WarpProfiles.CountAsync(),
profile.Status,
profile.DesiredEnabled,
profile.ContainerId,
profile.UpdatedAtUtc);
}
public async ValueTask DisposeAsync()
{
await Db.DisposeAsync();
await _connection.DisposeAsync();
}
}
private sealed class NoopClientPool : ITelegramClientPool
{
public int ActiveClientCount => 0;
public Task<Client> GetOrCreateClientAsync(
int accountId,
int apiId,
string apiHash,
string sessionPath,
string? sessionKey = null,
string? phoneNumber = null,
long? userId = null) =>
throw new NotSupportedException();
public Client? GetClient(int accountId) => null;
public Task RemoveClientAsync(int accountId) => Task.CompletedTask;
public Task RemoveAllClientsAsync() => Task.CompletedTask;
public bool IsClientConnected(int accountId) => false;
}
private sealed class NoopWarpDockerClientFactory : WarpContainerManager.IWarpDockerClientFactory
{
public bool PlatformSupported => true;
public WarpContainerManager.IWarpDockerClient Create(string socketPath) =>
new NoopWarpDockerClient();
}
private sealed class NoopWarpDockerClient : WarpContainerManager.IWarpDockerClient
{
public Task<string?> GetVersionAsync(CancellationToken cancellationToken) =>
Task.FromResult<string?>("test");
public Task EnsureImageAsync(
string image,
bool pullIfMissing,
CancellationToken cancellationToken) => Task.CompletedTask;
public Task CreateVolumeAsync(
string volumeName,
string profileId,
CancellationToken cancellationToken) => Task.CompletedTask;
public Task<string> CreateContainerAsync(
WarpContainerManager.WarpSettings settings,
string profileId,
string containerName,
string volumeName,
int hostPort,
CancellationToken cancellationToken) => Task.FromResult("test-container");
public Task StartContainerAsync(
string containerId,
CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopContainerAsync(
string containerId,
CancellationToken cancellationToken) => Task.CompletedTask;
public Task RestartContainerAsync(
string containerId,
CancellationToken cancellationToken) => Task.CompletedTask;
public Task<bool> VerifyContainerOwnershipAsync(
string containerId,
string profileId,
CancellationToken cancellationToken) => Task.FromResult(true);
public Task<bool> VerifyVolumeOwnershipAsync(
string volumeName,
string profileId,
CancellationToken cancellationToken) => Task.FromResult(true);
public Task RemoveContainerAsync(
string containerId,
CancellationToken cancellationToken) => Task.CompletedTask;
public Task RemoveVolumeAsync(
string volumeName,
CancellationToken cancellationToken) => Task.CompletedTask;
public void Dispose()
{
}
}
}