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

namespace Stardust.WebApiTests.Fixtures;

/// <summary>
/// 列表/详情行(ApiRow 投影 JSON)脱敏断言助手:ServerTests 反射刀组测试迁 HTTP 集成式(2026-11-14 Q2 裁决)。
/// 行键=OnProject camelCase 投影(首字母小写),值语义两型:值级置 null(键在值 null)与条件哨兵(键在值=哨兵串)。
/// </summary>
public static class ApiRowAssert
{
    /// <summary>GET 列表并解析行数组:信封 data 为 ApiRow 对象数组,逐行拍平为 Ordinal 字典。</summary>
    public static async Task<List<Dictionary<String, JsonElement>>> GetRowsAsync(ApiClient api, String endpoint)
    {
        var resp = await api.GetAsync(endpoint);
        var raw = await resp.Content.ReadAsStringAsync();
        Assert.True(resp.IsSuccessStatusCode, $"[{endpoint}] HTTP {(Int32)resp.StatusCode} 非成功:{raw}");

        var envelope = JsonSerializer.Deserialize<ApiResponse<JsonElement>>(raw, new JsonSerializerOptions(JsonSerializerDefaults.Web));
        Assert.NotNull(envelope);
        Assert.True(envelope!.Code == 0, $"[{endpoint}] 信封 Code={envelope.Code}(Message={envelope.Message}):{raw}");
        Assert.Equal(JsonValueKind.Array, envelope.Data.ValueKind);

        var rows = new List<Dictionary<String, JsonElement>>();
        foreach (var row in envelope.Data.EnumerateArray())
        {
            var dict = new Dictionary<String, JsonElement>(StringComparer.Ordinal);
            foreach (var prop in row.EnumerateObject()) dict[prop.Name] = prop.Value.Clone();
            rows.Add(dict);
        }
        return rows;
    }

    /// <summary>按行内键值前缀过滤本用例造数行(键如 name/domain/deployName)。</summary>
    public static List<Dictionary<String, JsonElement>> Mine(IEnumerable<Dictionary<String, JsonElement>> rows, String key, String prefix) =>
        rows.Where(r => r.TryGetValue(key, out var v) && (v.GetString() ?? "").StartsWith(prefix, StringComparison.Ordinal)).ToList();

    /// <summary>值级置 null 语义(task-94 惯例):键必须在(键不删)且值必须 JSON null(明文零落行)。</summary>
    public static void Masked(IEnumerable<Dictionary<String, JsonElement>> rows, String key, String ctx)
    {
        Assert.All(rows, r => Assert.True(r.ContainsKey(key),
            $"{ctx}: 行 JSON 缺键 '{key}'(值级置 null 语义要求键不删);实际键=[{String.Join(",", r.Keys)}]"));
        Assert.All(rows, r => Assert.True(r[key].ValueKind == JsonValueKind.Null,
            $"{ctx}: 行 JSON '{key}' 值未脱敏(期望 JSON null,实际 {r[key].ValueKind})"));
    }

    /// <summary>详情行值级置 null 语义:键必须在且值 null。</summary>
    public static void DetailMasked(JsonElement data, String key, String ctx)
    {
        Assert.True(data.TryGetProperty(key, out var v),
            $"{ctx}: 详情 JSON 缺键 '{key}'(值级置 null 语义要求键不删);实际键=[{String.Join(",", data.EnumerateObject().Select(p => p.Name))}]");
        Assert.True(v.ValueKind == JsonValueKind.Null, $"{ctx}: 详情 JSON '{key}' 值未脱敏(期望 JSON null,实际 {v.ValueKind})");
    }

    /// <summary>条件哨兵语义(刀3/4/8):键必须在且值=哨兵串(非 null——null 回填会经 PUT 静默清库)。</summary>
    public static void Sentinel(IEnumerable<Dictionary<String, JsonElement>> rows, String key, String mask, String ctx)
    {
        Assert.All(rows, r => Assert.True(r.ContainsKey(key), $"{ctx}: 行 JSON 缺键 '{key}'"));
        Assert.All(rows, r => Assert.True(r[key].ValueKind == JsonValueKind.String && r[key].GetString() == mask,
            $"{ctx}: 行 JSON '{key}' 非掩码哨兵(期望 \"{mask}\",实际 {r[key].ValueKind}:{r[key].GetString()})"));
    }

    /// <summary>详情行条件哨兵语义。</summary>
    public static void DetailSentinel(JsonElement data, String key, String mask, String ctx)
    {
        Assert.True(data.TryGetProperty(key, out var v), $"{ctx}: 详情 JSON 缺键 '{key}'");
        Assert.True(v.ValueKind == JsonValueKind.String && v.GetString() == mask,
            $"{ctx}: 详情 JSON '{key}' 非掩码哨兵(期望 \"{mask}\",实际 {v.ValueKind}:{v.GetString()})");
    }

    /// <summary>详情行键缺席断言(新管线字段集合投影:EditFormFields/DetailFields 已裁剪的键不得出现在详情 JSON)。</summary>
    public static void DetailHidden(JsonElement data, String key, String ctx)
    {
        Assert.True(!data.TryGetProperty(key, out var v),
            $"{ctx}: 详情 JSON 不应含键 '{key}'(meta 字段集已裁剪),实际值={v}");
    }

    /// <summary>行键在场断言(行自描述回归:值不拘)。</summary>
    public static void KeyPresent(IEnumerable<Dictionary<String, JsonElement>> rows, String key, String ctx)
    {
        Assert.All(rows, r => Assert.True(r.ContainsKey(key),
            $"{ctx}: 行 JSON 缺键 '{key}';实际键=[{String.Join(",", r.Keys)}]"));
    }

    /// <summary>行键缺席断言(新管线字段集合投影:被 meta 裁剪/TrimFields 的键不得出现在行 JSON——
    /// 比键在值 null 更强的防泄漏;2026-11-14 迁移定谳:敏感键清单与前端消费面见各测试类注释)。</summary>
    public static void KeyAbsent(IEnumerable<Dictionary<String, JsonElement>> rows, String key, String ctx)
    {
        Assert.All(rows, r =>
        {
            if (r.ContainsKey(key))
                Assert.Fail($"{ctx}: 行 JSON 不应含键 '{key}'(meta 字段集已裁剪),实际值={r[key]}");
        });
    }
}