节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
9.42 KiB
Stardust
using System;
using System.Linq;
using System.Text.Json;
using Stardust.Data;
using Stardust.Data.Configs;
using Stardust.Mcp;
using Stardust.Mcp.Tools.Config;
using Xunit;

namespace ServerTest.Mcp;

/// <summary>配置读工具复用 MVC/WebApi 已有查询。
/// 列表按应用定位走 AppConfig.FindByAppId,不在工具里拼 Where,也不像管理页那样缺配置就插入。
/// 配置项走 ConfigData.Search(配置, 空键, 空作用域, MinValue, MinValue, 无关键字),与两边 ConfigDataController.Search 相同。
/// 取值走 ConfigData.Search,键条件在 Biz 里。</summary>
public class ConfigReadReuseTests
{
    private const Int32 ProjectId = 999776;

    [Fact(DisplayName = "config_list 用 FindByAppId 定位,配置项与空条件 ConfigData.Search 同结果且不返回值;config_get 同结果并脱敏")]
    public async Task ListAndGet_ReuseBizQueries()
    {
        var suffix = Guid.NewGuid().ToString("n")[..8];
        var plain = "plain-" + suffix;
        var secret = "secret-" + suffix;
        var app = new App { Name = "cfg-read-" + suffix, ProjectId = ProjectId, Enable = true };
        app.Insert();
        var otherApp = new App { Name = "cfg-read-other-" + suffix, ProjectId = ProjectId, Enable = true };
        otherApp.Insert();

        AppConfig cfg = null;
        AppConfig otherCfg = null;
        var ctx = new McpContext { TokenId = 999999, ProjectId = ProjectId };
        try
        {
            var before = await new ConfigListTool().InvokeAsync(JsonSerializer.SerializeToElement(new { app_id = app.Id }), ctx);
            using (var beforeDoc = JsonDocument.Parse(JsonSerializer.Serialize(before)))
            {
                Assert.Contains("还没有配置", beforeDoc.RootElement.GetProperty("summary").GetString());
                Assert.Equal(0, beforeDoc.RootElement.GetProperty("data").GetProperty("records").GetArrayLength());
            }

            cfg = new AppConfig { Name = app.Name, AppId = app.Id, ProjectId = ProjectId, Enable = true, Version = 3 };
            cfg.Insert();
            otherCfg = new AppConfig { Name = otherApp.Name, AppId = otherApp.Id, ProjectId = ProjectId, Enable = true, Version = 1 };
            otherCfg.Insert();

            var plainRow = new ConfigData { ConfigId = cfg.Id, Key = "ServerName", Value = plain, Enable = true, Version = 2 };
            var secretRow = new ConfigData { ConfigId = cfg.Id, Key = "Password", Value = secret, Enable = false, Version = 4 };
            var scopedValue = "scoped-" + suffix;
            var scopedRow = new ConfigData { ConfigId = cfg.Id, Key = "Region", Scope = "prod", Value = scopedValue, Enable = true, Version = 9 };
            var otherRow = new ConfigData { ConfigId = otherCfg.Id, Key = "OtherOnly" + suffix, Value = "other-" + suffix, Enable = true, Version = 1 };
            plainRow.Insert();
            secretRow.Insert();
            scopedRow.Insert();
            otherRow.Insert();

            var located = AppConfig.FindByAppId(app.Id);
            Assert.Equal(cfg.Id, located.Id);

            var listed = await new ConfigListTool().InvokeAsync(JsonSerializer.SerializeToElement(new { app_id = app.Id }), ctx);
            var listedJson = JsonSerializer.Serialize(listed);
            // 空键、空作用域、时间 MinValue、不按关键字。精确键、作用域、关键字都会让 Search 少返回。
            var searched = ConfigData.Search(cfg.Id, "", "", DateTime.MinValue, DateTime.MinValue, null, null)
                .OrderBy(e => e.Id)
                .Select(e => (Id: e.Id, Key: e.Key ?? "", Version: e.Version, Enable: e.Enable, Secret: McpGuard.Secret(e.Key)))
                .ToArray();
            using (var listedDoc = JsonDocument.Parse(listedJson))
            {
                var data = listedDoc.RootElement.GetProperty("data");
                Assert.Equal(cfg.Id, data.GetProperty("config_id").GetInt32());
                Assert.Equal(3, data.GetProperty("version").GetInt32());
                var records = data.GetProperty("records");
                var listedRows = records.EnumerateArray().Select(e =>
                {
                    Assert.Equal(5, e.EnumerateObject().Count());
                    Assert.False(e.TryGetProperty("Value", out _));
                    return (
                        Id: e.GetProperty("Id").GetInt32(),
                        Key: e.GetProperty("Key").GetString() ?? "",
                        Version: e.GetProperty("Version").GetInt32(),
                        Enable: e.GetProperty("Enable").GetBoolean(),
                        Secret: e.GetProperty("secret").GetBoolean());
                }).OrderBy(e => e.Id).ToArray();
                Assert.Equal(searched, listedRows);
            }
            Assert.DoesNotContain(plain, listedJson);
            Assert.DoesNotContain(secret, listedJson);
            Assert.DoesNotContain(scopedValue, listedJson);
            Assert.DoesNotContain("OtherOnly" + suffix, listedJson);
            Assert.True(searched.Length > ConfigData.Search(cfg.Id, "ServerName", "", DateTime.MinValue, DateTime.MinValue, null, null).Count);
            Assert.True(searched.Length > ConfigData.Search(cfg.Id, "", "prod", DateTime.MinValue, DateTime.MinValue, null, null).Count);
            Assert.True(searched.Length > ConfigData.Search(cfg.Id, "", "", DateTime.MinValue, DateTime.MinValue, scopedValue, null).Count);

            var all = ConfigData.Search(cfg.Id, null, "", DateTime.MinValue, DateTime.MinValue, null, null);
            var gotAll = await new ConfigGetTool().InvokeAsync(JsonSerializer.SerializeToElement(new { config_id = cfg.Id }), ctx);
            Assert.Equal(all.Count, RecordCount(gotAll));

            var one = ConfigData.Search(cfg.Id, "ServerName", "", DateTime.MinValue, DateTime.MinValue, null, null);
            var gotOne = await new ConfigGetTool().InvokeAsync(JsonSerializer.SerializeToElement(new { config_id = cfg.Id, key = "ServerName" }), ctx);
            var gotOneJson = JsonSerializer.Serialize(gotOne);
            Assert.Equal(one.Count, RecordCount(gotOne));
            Assert.Contains(plain, gotOneJson);
            Assert.DoesNotContain(secret, gotOneJson);

            // 半截键只能命中 Search 的精确键匹配。若工具把 key 当成关键字 Contains,这里会多返回 ServerName。
            var partial = ConfigData.Search(cfg.Id, "Server", "", DateTime.MinValue, DateTime.MinValue, null, null);
            var gotPartial = await new ConfigGetTool().InvokeAsync(JsonSerializer.SerializeToElement(new { config_id = cfg.Id, key = "Server" }), ctx);
            Assert.Equal(partial.Count, RecordCount(gotPartial));

            var gotSecret = await new ConfigGetTool().InvokeAsync(JsonSerializer.SerializeToElement(new { config_id = cfg.Id, key = "Password" }), ctx);
            var gotSecretJson = JsonSerializer.Serialize(gotSecret);
            Assert.Contains("\"value\":\"***\"", gotSecretJson);
            Assert.Contains("\"masked\":true", gotSecretJson);
            Assert.DoesNotContain(secret, gotSecretJson);
        }
        finally
        {
            Cleanup(cfg);
            Cleanup(otherCfg);
            otherApp.Delete();
            app.Delete();
        }
    }

    [Fact(DisplayName = "config_list 不返回其它项目挂在同一应用上的配置")]
    public async Task List_HidesConfigOutsideTokenProject()
    {
        var suffix = Guid.NewGuid().ToString("n")[..8];
        var app = new App { Name = "cfg-read-xproj-" + suffix, ProjectId = ProjectId, Enable = true };
        app.Insert();
        var cfg = new AppConfig { Name = app.Name, AppId = app.Id, ProjectId = ProjectId + 1, Enable = true, Version = 1 };
        cfg.Insert();
        var row = new ConfigData { ConfigId = cfg.Id, Key = "Leak" + suffix, Value = "leak-" + suffix, Enable = true, Version = 1 };
        row.Insert();
        var ctx = new McpContext { TokenId = 999999, ProjectId = ProjectId };
        try
        {
            var listed = await new ConfigListTool().InvokeAsync(JsonSerializer.SerializeToElement(new { app_id = app.Id }), ctx);
            var json = JsonSerializer.Serialize(listed);
            using (var listedDoc = JsonDocument.Parse(json))
            {
                var summary = listedDoc.RootElement.GetProperty("summary").GetString();
                Assert.Contains("还没有配置", summary);
                Assert.DoesNotContain(app.Name, summary);
                Assert.Equal(0, listedDoc.RootElement.GetProperty("data").GetProperty("records").GetArrayLength());
            }
            Assert.DoesNotContain(app.Name, json);
            Assert.DoesNotContain("Leak" + suffix, json);
            Assert.DoesNotContain("leak-" + suffix, json);
            Assert.Equal(cfg.Id, AppConfig.FindByAppId(app.Id).Id);
            Assert.Equal("Leak" + suffix, Assert.Single(ConfigData.Search(cfg.Id, "", "", DateTime.MinValue, DateTime.MinValue, null, null)).Key);
        }
        finally
        {
            Cleanup(cfg);
            app.Delete();
        }
    }

    private static Int32 RecordCount(Object content)
    {
        using var doc = JsonDocument.Parse(JsonSerializer.Serialize(content));
        return doc.RootElement.GetProperty("data").GetProperty("records").GetArrayLength();
    }

    private static void Cleanup(AppConfig cfg)
    {
        if (cfg == null || cfg.Id <= 0) return;
        foreach (var row in ConfigData.FindAllByApp(cfg.Id).ToList()) row.Delete();
        ConfigHistory.Delete(ConfigHistory._.ConfigId == cfg.Id);
        cfg.Delete();
    }
}