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

namespace Stardust.WebApiTests.Security;

/// <summary>
/// task-95 二期刀2(2026-11-12,盘点稿 Doc/安全-02 §4 刀2)HTTP 集成式迁移(2026-11-14 Q2 裁决):
/// App.Secret/WebHook 行级脱敏。前置:刀1 预览链已改道 /Config/GetAllByApp(行内 Secret 零消费方);
/// F3 先例:apps 编辑=partial-PUT,Secret 留空不提交、WebHook 不在 Vue 表单(不 Dirty)→ 行裁不破编辑流;
/// Valid "*****" 守卫(AppDeploy 先例)保编辑表单回填原样提交不写坏。
/// 与原 ServerTests 反射版差异:Valid GET 回显(MVC 编辑表单输入侧)非 API 面,不再断言;读回走实体 Find 直连查询。
/// 新管线(Cube 1105 字段集合投影)契约定谳(2026-11-14 首轮实测):
/// 列表行 Secret/WebHook、详情 Secret **键级缺席**(meta 字段集已裁——比键在值 null 更强防泄漏);
/// 详情 WebHook 键仍在(掩码值 null)。掩码机制=薄基类 GetRowMasks + OnProject 统一应用。
/// </summary>
[Collection(WebApiCollection.Name)]
public class MaskedRowsKnife2ApiTests
{
    private const String Mask = "*****";

    private readonly ApiClient _api;

    public MaskedRowsKnife2ApiTests(WebApiTestFactory factory, ITestOutputHelper output) =>
        _api = new(factory, output);

    [Fact(DisplayName = "task-95 刀2:App 列表行 Secret/WebHook 键级缺席(meta 已裁,明文零泄漏)")]
    public async Task App_Index_Row_Secret_WebHook_Should_Be_Masked()
    {
        var tag = "mw2a-" + Guid.NewGuid().ToString("N")[..8];
        var e = new App
        {
            Name = tag + "-app", DisplayName = "刀2造数", Category = "web",
            Secret = "sec-" + tag, WebHook = "http://hook/" + tag, Enable = true,
        };
        e.Insert();
        try
        {
            var rows = await ApiRowAssert.GetRowsAsync(_api, "Registry/App?pageIndex=0&pageSize=200");
            var mine = ApiRowAssert.Mine(rows, "name", tag);
            Assert.True(mine.Count >= 1, $"应能查到造数行 {tag}*,实际行数={rows.Count}");

            ApiRowAssert.KeyAbsent(mine, "secret", "App");
            ApiRowAssert.KeyAbsent(mine, "webHook", "App");
        }
        finally { e.Delete(); }
    }

    [Fact(DisplayName = "task-95 刀2:App Detail——Secret 键缺席,WebHook 键在值 null")]
    public async Task App_Detail_Crops_Secret_WebHook()
    {
        var tag = "mw2b-" + Guid.NewGuid().ToString("N")[..8];
        var e = new App
        {
            Name = tag + "-app", DisplayName = "刀2造数", Category = "web",
            Secret = "sec-" + tag, WebHook = "http://hook/" + tag, Enable = true,
        };
        e.Insert();
        try
        {
            var api = await _api.GetJsonAsync($"Registry/App/{e.Id}");

            ApiRowAssert.DetailHidden(api.Data, "secret", "App");
            ApiRowAssert.DetailMasked(api.Data, "webHook", "App");
        }
        finally { e.Delete(); }
    }

    [Fact(DisplayName = "task-95 刀2:App F1 提交——PUT '*****' 守卫恢复原值并真写读回不变;WebHook 原值提交不受影响")]
    public async Task App_Put_MaskedSecret_Restores_Old()
    {
        var tag = "mw2c-" + Guid.NewGuid().ToString("N")[..8];
        var e = new App
        {
            Name = tag + "-app", DisplayName = "刀2造数", Category = "web",
            Secret = "real-" + tag, WebHook = "http://keep/" + tag, Enable = true,
        };
        e.Insert();
        try
        {
            // 模拟 MVC 编辑表单:Secret 回显 "*****" 原样提交;WebHook 服务端表单原值提交(非掩码)
            var put = new
            {
                id = e.Id,
                name = e.Name,
                displayName = e.DisplayName,
                category = e.Category,
                secret = Mask,
                webHook = e.WebHook,
                enable = true,
            };
            var api = await _api.PutApiAsync<JsonElement>($"Registry/App/{e.Id}", put);

            var back = App.FindById(e.Id);
            Assert.NotNull(back);
            Assert.Equal("real-" + tag, back.Secret);
            Assert.Equal("http://keep/" + tag, back.WebHook);
        }
        finally { e.Delete(); }
    }
}