节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
8.75 KiB
Stardust
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using NewLife;
using NewLife.Remoting.Models;
using NewLife.Security;
using Stardust.Data.Nodes;
using Stardust.Models;
using Stardust.Server;
using Xunit;

namespace ServerTest.Controllers;

/// <summary>引导安装接口(node/bootstrap)端到端测试。
/// 语义:只有原生 Agent 返回引导信息;安装包来源补全为绝对地址并附加下载令牌;
/// .NET 版 Agent 自身即完整版,接口返回空。</summary>
[Collection(StarTestServerCollection.Name)]
public class NodeBootstrapEndpointTests : StarTestBase
{
    public NodeBootstrapEndpointTests(StarTestServer fixture) : base(fixture) { }

    /// <summary>测试用发布版本、安装包与运行时包,结束后清理。</summary>
    private sealed class BootstrapScope : IDisposable
    {
        private readonly ProductRelease _release;
        private readonly ProductPackage _package;
        private readonly DotNetPackage _runtime;

        public BootstrapScope(String agentSource, String runtimeSource)
        {
            _release = new ProductRelease
            {
                Version = "9.9.9999." + Guid.NewGuid().ToString("n")[..8],
                ProductCode = "StarAgent",
                Enable = true,
                Force = false,
                Channel = NodeChannels.Release,
            };
            _release.Insert();

            _package = new ProductPackage
            {
                ReleaseId = _release.Id,
                OSKind = OSKind.Linux,
                Architecture = CpuArch.X64,
                TargetRuntime = "*",
                FileName = "staragent-endpoint.tar.gz",
                Source = agentSource,
                Size = 2048,
                FileHash = "0123456789abcdef0123456789abcdef",
            };
            _package.Insert();

            _runtime = new DotNetPackage
            {
                Version = "9.9." + Guid.NewGuid().ToString("n")[..8],
                Kind = "runtime",
                OSKind = OSKind.Linux,
                Architecture = CpuArch.X64,
                FileName = "dotnet-runtime-endpoint.tar.gz",
                Source = runtimeSource,
                Size = 1024,
                FileHash = "0123456789abcdef0123456789abcdef",
                Enable = true,
                Channel = NodeChannels.Release,
            };
            _runtime.Insert();
        }

        public void Dispose()
        {
            _runtime.Delete();
            _package.Delete();
            _release.Delete();
        }
    }

    private async Task<(String token, Node node)> LoginAsync(String productCode)
    {
        var client = _server.CreateClient();
        var macs = "AA-BB-CC-" + Rand.NextBytes(3).ToHex("-");
        var body = new
        {
            Code = "",
            Secret = "",
            ProductCode = productCode,
            ClientId = "127.0.0.1@1",
            Node = new
            {
                Version = "1.0.2026.1017",
                Compile = "2026-11-02 08:00:00",
                OSName = "Ubuntu",
                OSVersion = "24.04",
                Architecture = "X64",
                MachineName = "bootstrap_" + Rand.NextString(6),
                UserName = "root",
                Macs = macs,
                Runtime = productCode == "StarAgent-Cpp" ? "cpp17" : "10.0.0",
                Framework = productCode == "StarAgent-Cpp" ? "cpp17" : "10.0.0",
                TimeZone = "Asia/Shanghai",
                Time = "2026-11-02 10:00:00",
                Plugins = new[] { "agent:cpp", "cap:bootstrap=true", "os:linux" },
            },
        };

        var resp = await client.PostAsync("node/login", JsonContent.Create(body));
        var text = await resp.Content.ReadAsStringAsync();
        Assert.True(resp.IsSuccessStatusCode, "登录应成功:" + text);

        var rs = ParseData<LoginResponse>(text);
        Assert.NotNull(rs);
        Assert.False(rs!.Token.IsNullOrEmpty(), "登录响应缺少令牌");

        var node = Node.FindByCode(rs.Code);
        Assert.NotNull(node);

        return (rs.Token!, node!);
    }

    /// <summary>解析裸对象或 {code,data,msg} 包装的响应。</summary>
    private static T? ParseData<T>(String text)
    {
        using var doc = JsonDocument.Parse(text);
        var root = doc.RootElement;
        if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("data", out var data))
            return data.Deserialize<T>(new JsonSerializerOptions(JsonSerializerDefaults.Web));

        return root.Deserialize<T>(new JsonSerializerOptions(JsonSerializerDefaults.Web));
    }

    /// <summary>静态目录(/files)只补全地址,不加签;加签只针对 /cube/file 附件。</summary>
    [Fact(DisplayName = "原生 Agent 取到绝对地址的引导安装包")]
    public async Task NativeAgent_GetsAbsolutePackages()
    {
        using var scope = new BootstrapScope("/files/staragent-endpoint.tar.gz", "/files/dotnet-runtime-endpoint.tar.gz");
        var (token, node) = await LoginAsync("StarAgent-Cpp");
        try
        {
            var client = _server.CreateClient();
            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

            var resp = await client.PostAsync("node/bootstrap", JsonContent.Create(new { }));
            var text = await resp.Content.ReadAsStringAsync();
            Assert.True(resp.IsSuccessStatusCode, "引导安装接口应成功:" + text);

            var model = ParseData<BootstrapModel>(text);
            Assert.NotNull(model);
            Assert.Equal(node.Code, model!.Code);

            var agent = model.Agent;
            Assert.NotNull(agent);
            Assert.Equal("staragent-endpoint.tar.gz", agent!.FileName);
            Assert.NotNull(agent.Source);
            Assert.StartsWith("http", agent.Source, StringComparison.OrdinalIgnoreCase);
            // 静态目录靠 IP 白名单,不追加下载令牌
            Assert.DoesNotContain("token=", agent.Source);

            var runtime = model.Runtime;
            Assert.NotNull(runtime);
            Assert.Equal("runtime", runtime!.Kind);
            Assert.NotNull(runtime.Source);
            Assert.StartsWith("http", runtime.Source, StringComparison.OrdinalIgnoreCase);
            Assert.DoesNotContain("token=", runtime.Source);
        }
        finally
        {
            node.Delete();
        }
    }

    /// <summary>Cube 附件下载地址需要短时令牌,接口下发时必须现场签发。</summary>
    [Fact(DisplayName = "Cube 附件下载地址附带短时令牌")]
    public async Task AttachmentSource_GetsToken()
    {
        using var scope = new BootstrapScope("/cube/file?id=123.zip", "/cube/file?id=124.tar.gz");
        var (token, node) = await LoginAsync("StarAgent-Go");
        try
        {
            var client = _server.CreateClient();
            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

            var resp = await client.PostAsync("node/bootstrap", JsonContent.Create(new { }));
            var text = await resp.Content.ReadAsStringAsync();
            Assert.True(resp.IsSuccessStatusCode, "引导安装接口应成功:" + text);

            var model = ParseData<BootstrapModel>(text);
            Assert.NotNull(model);
            Assert.NotNull(model!.Agent);
            Assert.StartsWith("http", model.Agent.Source, StringComparison.OrdinalIgnoreCase);
            Assert.Contains("token=", model.Agent.Source);
            Assert.NotNull(model.Runtime);
            Assert.Contains("token=", model.Runtime!.Source);
        }
        finally
        {
            node.Delete();
        }
    }

    /// <summary>.NET 版 Agent 自身即完整版,接口返回空。</summary>
    [Fact(DisplayName = "非原生 Agent 不返回引导信息")]
    public async Task DotNetAgent_GetsNull()
    {
        using var scope = new BootstrapScope("/files/staragent-endpoint.tar.gz", "/files/dotnet-runtime-endpoint.tar.gz");
        var (token, node) = await LoginAsync("StarAgent");
        try
        {
            var client = _server.CreateClient();
            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

            var resp = await client.PostAsync("node/bootstrap", JsonContent.Create(new { }));
            var text = await resp.Content.ReadAsStringAsync();

            Assert.True(resp.IsSuccessStatusCode, "接口应成功返回空:" + text);
            Assert.True(text.IsNullOrEmpty() || text.Trim() == "null" || !text.Contains("Agent", StringComparison.OrdinalIgnoreCase),
                "非原生 Agent 不应返回安装包:" + text);
        }
        finally
        {
            node.Delete();
        }
    }
}