节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
5.00 KiB
Stardust
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using NewLife;
using Stardust.Data.Nodes;
using Xunit;

namespace ServerTest.Communication;

/// <summary>原生 Agent 到真实 StarServer 的端到端测试。仅在设置 STARAGENT_NATIVE_BIN 时运行。</summary>
/// <remarks>
/// 该测试启动真实 Kestrel 服务端,再启动外部原生 Agent 进程,验证登录、身份持久化与在线状态。
/// 通过 STARAGENT_NATIVE_PRODUCT 指定产品编码(默认 StarAgent-Go),因此可分别验证 Go/Rust/C++。
/// CI 中若未构建原生 Agent 可跳过;本地可用:
/// <code>$env:STARAGENT_NATIVE_BIN='C:\path\to\staragent.exe'; $env:STARAGENT_NATIVE_PRODUCT='StarAgent-Rust'; dotnet test ...</code>
/// </remarks>
[Collection("StarServer")]
public class NativeAgentIntegrationTests
{
    private readonly StarServerFixture _fixture;

    public NativeAgentIntegrationTests(StarServerFixture fixture) => _fixture = fixture;

    [Fact(DisplayName = "原生代理-真实服务端登录并上线")]
    public async Task NativeAgent_Login_And_Online()
    {
        var bin = Environment.GetEnvironmentVariable("STARAGENT_NATIVE_BIN");
        if (bin.IsNullOrEmpty()) bin = Environment.GetEnvironmentVariable("STARAGENT_GO_BIN");
        if (bin.IsNullOrEmpty() || !File.Exists(bin))
        {
            // 未提供原生 Agent 时跳过,不把环境缺失当作失败
            return;
        }

        var productCode = Environment.GetEnvironmentVariable("STARAGENT_NATIVE_PRODUCT");
        if (productCode.IsNullOrEmpty()) productCode = "StarAgent-Go";

        var dir = Path.Combine(Path.GetTempPath(), "staragent-go-" + Guid.NewGuid().ToString("n"));
        Directory.CreateDirectory(dir);
        var configPath = Path.Combine(dir, "staragent.json");
        await File.WriteAllTextAsync(configPath, JsonSerializer.Serialize(new
        {
            Server = _fixture.ServerUrl,
            ProductCode = productCode,
            Code = "",
            Secret = "",
            Project = "",
            Channel = "Release",
            DataDir = dir,
            LogLevel = "info",
            MetricsInterval = 5,
            AllowExec = false,
            AllowedCommands = Array.Empty<String>(),
            CommandTimeout = 30,
            TlsVerify = true,
        }));

        Node? node = null;
        var output = new StringBuilder();
        using var process = new Process
        {
            StartInfo = new ProcessStartInfo(bin)
            {
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
            },
        };
        process.OutputDataReceived += (_, e) => { if (e.Data != null) lock (output) output.AppendLine(e.Data); };
        process.ErrorDataReceived += (_, e) => { if (e.Data != null) lock (output) output.AppendLine(e.Data); };
        process.StartInfo.ArgumentList.Add("run");
        process.StartInfo.ArgumentList.Add("--config");
        process.StartInfo.ArgumentList.Add(configPath);

        try
        {
            Assert.True(process.Start(), "无法启动原生 Agent");
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            // Go Agent 登录成功后把服务端下发的 Code/Secret 写回配置文件
            String? code = null;
            for (var i = 0; i < 80 && code.IsNullOrEmpty(); i++)
            {
                await Task.Delay(250);
                try
                {
                    using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(configPath));
                    if (doc.RootElement.TryGetProperty("Code", out var value)) code = value.GetString();
                }
                catch (IOException)
                {
                    // 原子写入期间可能短暂读不到,继续重试
                }
            }

            String logs;
            lock (output) logs = output.ToString();
            Assert.False(code.IsNullOrEmpty(), "原生 Agent 未在超时时间内完成登录并持久化 Code。输出:\n" + logs);

            node = Node.FindByCode(code);
            Assert.NotNull(node);
            Assert.Equal(productCode, node!.ProductCode);
            Assert.True(node.Enable, "自动注册的节点应启用");

            NodeOnline? online = null;
            for (var i = 0; i < 40 && online == null; i++)
            {
                await Task.Delay(250);
                online = NodeOnline.FindByNodeId(node.ID);
            }
            Assert.NotNull(online);
        }
        finally
        {
            try
            {
                if (!process.HasExited) process.Kill(true);
            }
            catch { }

            if (node != null)
            {
                NodeOnline.FindByNodeId(node.ID)?.Delete();
                node.Delete();
            }

            try { Directory.Delete(dir, true); } catch { }
        }
    }
}