using System.Text.Json;
using Stardust.Data.Nodes;
using Stardust.WebApiTests.Fixtures;
using Xunit;
using Xunit.Abstractions;
namespace Stardust.WebApiTests.Security;
/// <summary>
/// task-95 二期刀0(2026-11-12,盘点稿 Doc/安全-02 §4 刀0+N1,lead æ ¸å‡†ä»¤ï¼‰HTTP 集æˆå¼è¿ç§»ï¼ˆ2026-11-14 Q2 è£å†³ï¼‰ï¼š
/// MySqlNode/RedisNode æ•æ„Ÿä¸‰é”®ï¼ˆPassword/UserName/WebHookï¼‰è¡Œçº§è„±æ• + Valid "*****" 守å«ï¼›Node.WebHook 行/详情脱æ•(N1)。
/// é™é»˜æ¸…库三形æ€ä¸ F1(detail å…¨é‡å±•å¼€ PUT)的消解实è¯ï¼šæäº¤ "*****"(Detail è£å‰ªåŽå‰ç«¯åŽŸæ ·å›žä¼ ï¼‰â†’
/// Valid 从库æ¢å¤åŽŸå€¼å¹¶æ¸…è„æ ‡è®° → 真写读回ä¸å˜ã€‚
/// 与原 ServerTests å射版(SearchData→OnFillListValues→OnJsonSerialize 链)差异:
/// ①全部走真实 HTTP 管é“(TestServer 进程内);②Valid GET 回显(MVC 编辑表å•è¾“å…¥ä¾§ï¼‰éž API é¢ï¼Œä¸å†æ–言;
/// ③读回æ–言走实体 Find 直连查询(绕过实体缓å˜ï¼ŒCertPfx 先例)。
/// ④新管线(Cube 1105 å—æ®µé›†åˆæŠ•影)契约定谳(2026-11-14 首轮实测):meta å—æ®µé›†å·²è£å‰ªçš„æ•æ„Ÿé”®
/// (MySql/Redis çš„ Password/UserName/WebHookã€Node çš„ Secret/WebHook)在行 JSON ä¸**键级缺å¸**——
/// 比「键在值 nullã€æ›´å¼ºçš„防泄æ¼ï¼›æŽ©ç 层åªåœ¨é”®ä»åœ¨å—段集时生效(值 null/哨兵)。
/// æŽ©ç æœºåˆ¶ï¼ˆ2026-11-14 æ”¶æ•›åŽï¼‰ï¼šWebEntityController 薄基类 GetRowMasks 声明å¼è§„则 + OnProject 统一应用。
/// </summary>
[Collection(WebApiCollection.Name)]
public class MaskedRowsKnife0ApiTests
{
private readonly ApiClient _api;
private readonly ITestOutputHelper _output;
public MaskedRowsKnife0ApiTests(WebApiTestFactory factory, ITestOutputHelper output)
{
_api = new(factory, output);
_output = output;
}
private const String Mask = "*****";
[Fact(DisplayName = "task-95 刀0:MySqlNode 列表行 Password/UserName/WebHook 键级缺å¸ï¼ˆmeta å·²è£ï¼Œæ˜Žæ–‡é›¶æ³„æ¼ï¼‰")]
public async Task MySqlNode_Index_Row_Sensitive_Three_Should_Be_Masked()
{
var tag = "mw0my-" + Guid.NewGuid().ToString("N")[..8];
var e = new MySqlNode
{
Name = tag + "-n1", Server = "127.0.0.1", Port = 3306,
UserName = "root-" + tag, Password = "pw-" + tag, WebHook = "http://hook/" + tag,
DatabaseName = "mysql", Version = "8.0", Enable = true,
};
e.Insert();
try
{
var rows = await ApiRowAssert.GetRowsAsync(_api, $"MySql/MySqlNode?pageIndex=0&pageSize=200");
var mine = ApiRowAssert.Mine(rows, "name", tag);
Assert.True(mine.Count >= 1, $"åº”èƒ½æŸ¥åˆ°é€ æ•°è¡Œ {tag}*,实际行数={rows.Count}");
ApiRowAssert.KeyAbsent(mine, "password", "MySqlNode");
ApiRowAssert.KeyAbsent(mine, "userName", "MySqlNode");
ApiRowAssert.KeyAbsent(mine, "webHook", "MySqlNode");
}
finally { e.Delete(); }
}
[Fact(DisplayName = "task-95 刀0:MySqlNode Detail——Password 键缺å¸ï¼ˆå†™å…¥å£ä»¤ä¸æŠ•影),UserName/WebHook 键在值 null")]
public async Task MySqlNode_Detail_Crops_Sensitive_Three()
{
var tag = "mw0md-" + Guid.NewGuid().ToString("N")[..8];
var e = new MySqlNode
{
Name = tag + "-n1", Server = "127.0.0.1", Port = 3306,
UserName = "root-" + tag, Password = "pw-" + tag, WebHook = "http://hook/" + tag,
Enable = true,
};
e.Insert();
try
{
var api = await _api.GetJsonAsync($"MySql/MySqlNode/{e.Id}");
ApiRowAssert.DetailHidden(api.Data, "password", "MySqlNode");
ApiRowAssert.DetailMasked(api.Data, "userName", "MySqlNode");
ApiRowAssert.DetailMasked(api.Data, "webHook", "MySqlNode");
}
finally { e.Delete(); }
}
[Fact(DisplayName = "task-95 刀0:MySqlNode F1 æäº¤â€”—PUT '*****' å®ˆå«æ¢å¤åŽŸå€¼å¹¶çœŸå†™è¯»å›žä¸å˜ï¼ˆé™é»˜æ¸…库消解实è¯ï¼‰")]
public async Task MySqlNode_Put_MaskedValue_Restores_Old()
{
var tag = "mw0mg-" + Guid.NewGuid().ToString("N")[..8];
var e = new MySqlNode
{
Name = tag + "-n1", Server = "127.0.0.1", Port = 3306,
UserName = "root-" + tag, Password = "real-" + tag, WebHook = "http://keep/" + tag, Enable = true,
};
e.Insert();
try
{
// 模拟 F1 æäº¤ï¼šDetail å…¨é‡å±•开(Password/WebHook 已被è£ï¼‰+ è¡¨å•æœªæ”¹ → åŽŸæ ·å›žä¼ "*****"
var put = new
{
id = e.Id,
name = e.Name,
server = e.Server,
port = e.Port,
password = Mask,
webHook = Mask,
enable = true,
};
var api = await _api.PutApiAsync<JsonElement>($"MySql/MySqlNode/{e.Id}", put);
var back = MySqlNode.FindById(e.Id);
Assert.NotNull(back);
Assert.Equal("real-" + tag, back.Password);
Assert.Equal("http://keep/" + tag, back.WebHook);
Assert.Equal("root-" + tag, back.UserName); // PUT 未æäº¤ userName → ä¿æŒåº“值
}
finally { e.Delete(); }
}
[Fact(DisplayName = "task-95 刀0:RedisNode 列表行 Password/UserName/WebHook 键级缺å¸ï¼ˆmeta å·²è£ï¼‰")]
public async Task RedisNode_Index_Row_Sensitive_Three_Should_Be_Masked()
{
var tag = "mw0rd-" + Guid.NewGuid().ToString("N")[..8];
var e = new RedisNode
{
Name = tag + "-n1", Server = $"127.0.0.1:{Random.Shared.Next(20000, 60000)}", Category = "缓å˜",
UserName = "default-" + tag, Password = "pw-" + tag, WebHook = "http://hook/" + tag,
Version = "7.2", Mode = "standalone", Enable = true,
};
e.Insert();
try
{
var rows = await ApiRowAssert.GetRowsAsync(_api, $"Redis/RedisNode?pageIndex=0&pageSize=200");
var mine = ApiRowAssert.Mine(rows, "name", tag);
Assert.True(mine.Count >= 1, $"åº”èƒ½æŸ¥åˆ°é€ æ•°è¡Œ {tag}*,实际行数={rows.Count}");
ApiRowAssert.KeyAbsent(mine, "password", "RedisNode");
ApiRowAssert.KeyAbsent(mine, "userName", "RedisNode");
ApiRowAssert.KeyAbsent(mine, "webHook", "RedisNode");
}
finally { e.Delete(); }
}
[Fact(DisplayName = "task-95 刀0:RedisNode F1 æäº¤â€”—PUT '*****' å®ˆå«æ¢å¤åŽŸå€¼å¹¶çœŸå†™è¯»å›žä¸å˜")]
public async Task RedisNode_Put_MaskedValue_Restores_Old()
{
var tag = "mw0rg-" + Guid.NewGuid().ToString("N")[..8];
var server = $"127.0.0.1:{Random.Shared.Next(20000, 60000)}";
var e = new RedisNode
{
Name = tag + "-n1", Server = server, Category = "缓å˜",
Password = "real-" + tag, WebHook = "http://keep/" + tag, Enable = true,
};
e.Insert();
try
{
var put = new
{
id = e.Id,
name = e.Name,
server = server,
category = e.Category,
password = Mask,
webHook = Mask,
enable = true,
};
var api = await _api.PutApiAsync<JsonElement>($"Redis/RedisNode/{e.Id}", put);
var back = RedisNode.FindById(e.Id);
Assert.NotNull(back);
Assert.Equal("real-" + tag, back.Password);
Assert.Equal("http://keep/" + tag, back.WebHook);
}
finally { e.Delete(); }
}
[Fact(DisplayName = "task-95 刀0/N1:Node 列表行 Secret/WebHook 键级缺å¸ï¼›Detail Secret/WebHook 键级缺å¸+WebHook 值 null(注册æµç¨‹åœ¨ç”¨ï¼‰")]
public async Task Node_Row_Secret_WebHook_Masked_And_Detail_Keeps_Secret_Crops_WebHook()
{
var tag = "mw0nd-" + Guid.NewGuid().ToString("N")[..8];
var e = new Node
{
Name = tag + "-node", Code = tag + "-code", Secret = "sec-" + tag,
WebHook = "http://hook/" + tag, Enable = true,
};
e.Insert();
try
{
var rows = await ApiRowAssert.GetRowsAsync(_api, $"Nodes/Node?pageIndex=0&pageSize=200");
var mine = ApiRowAssert.Mine(rows, "name", tag);
Assert.True(mine.Count >= 1, $"åº”èƒ½æŸ¥åˆ°é€ æ•°è¡Œ {tag}*,实际行数={rows.Count}");
ApiRowAssert.KeyAbsent(mine, "secret", "Node");
ApiRowAssert.KeyAbsent(mine, "webHook", "Node");
// Detail:Secret 键级缺å¸ï¼ˆæ–°å—段集把 Secret 从详情é¢ä¹Ÿè£æŽ‰â€”—强于一期「ä¿å®ˆä¿ç•™ã€å¥‘约)ã€
// WebHook 键在值 null(N1)
var api = await _api.GetJsonAsync($"Nodes/Node/{e.ID}");
ApiRowAssert.DetailHidden(api.Data, "secret", "Node");
ApiRowAssert.DetailMasked(api.Data, "webHook", "Node");
}
finally { e.Delete(); }
}
}
|