using System.Text.Json;
using Stardust.Data;
using Stardust.Data.Configs;
using Stardust.Models;
using Stardust.WebApiTests.Fixtures;
using Xunit;
using Xunit.Abstractions;
namespace Stardust.WebApiTests.Areas;
/// <summary>
/// Config 区进程内 HTTP 冒烟。
/// 宿主è¯ä¹‰å®žè¯ï¼ˆ2026-11-09 三轮实验:å•类冷å¯åЍ / Meta+Config ç»„åˆ / å…¨é‡ 179 例):
/// 1. 登录门(ConfigController L29):ä¾èµ–åŒå®¿ä¸» Cube 管é“预çƒâ€”—未跑过任何 Cube 区域 Bearer
/// è¯·æ±‚æ—¶æ’ 401「未登录ï¼ã€ï¼›è·‘过åŽå¸¦ Bearer 的请求放行。匿åï¼ˆæ— å¤´ï¼‰æ’ 401ï¼Œé¡ºåºæ— 关。
/// 2. 密钥门(AppTokenService.Authorize L50-65):密钥ä¸åŒ¹é…按 app.ReRegister è£å†³â€”—
/// è·Ÿéšå…¨å±€æŒ‰å…¨å±€é»˜è®¤è§£æžï¼ˆå®žæµ‹ä¸´æ—¶æ”¾è¡Œï¼‰ï¼Œæ˜¾å¼ Off 403ã€Œéžæ³•访问ã€ã€‚
/// 3. ç¦ç”¨é—¨ï¼ˆL46ï¼‰ï¼šå…ˆäºŽå¯†é’¥æ ¡éªŒï¼Œç¦ç”¨åº”ç”¨æ’ 403「已ç¦ç”¨ã€ã€‚
/// å› æ¤éœ€ç™»å½•æ€çš„ç”¨ä¾‹è‡ªå¸¦é¢„çƒæ¥éª¤ï¼ˆBearer è°ƒ Cube 区域 meta),æ–言与进程å¯åŠ¨é¡ºåºæ— 关。
/// C02/C03/C05 完整è¯ä¹‰ä»¥è¿›ç¨‹å†…真值固化;5188 预览链窗å£è´Ÿè´£æµè§ˆå™¨ä¾§ UI 走查。
/// </summary>
[Collection(WebApiCollection.Name)]
public class ConfigSmokeTests
{
private readonly WebApiTestFactory _factory;
private readonly ApiClient _api;
private readonly ITestOutputHelper _output;
public ConfigSmokeTests(WebApiTestFactory factory, ITestOutputHelper output)
{
_factory = factory;
_api = new ApiClient(factory, output);
_output = output;
}
private const String AppName = "cfgsmoke";
private const String AppSecret = "cfgsmoke-secret";
private static async Task<JsonElement> BodyOf(HttpResponseMessage resp)
{
var body = await resp.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<JsonElement>(body, new JsonSerializerOptions(JsonSerializerDefaults.Web));
}
private async Task<HttpResponseMessage> RawGetAsync(String endpoint, Boolean auth = true)
{
var resp = await _api.GetAsync(endpoint, auth);
var body = await resp.Content.ReadAsStringAsync();
_output.WriteLine("GET " + endpoint + " HTTP " + (Int32)resp.StatusCode + " body=" + body);
return resp;
}
/// <summary>登录门预çƒï¼šBearer è°ƒ Cube 区域 meta 端点,触å‘其授æƒè¿‡æ»¤å™¨çš„ TryLogin。</summary>
private async Task WarmUpAsync()
{
var warm = await _api.GetJsonAsync("Registry/AppService/meta");
Assert.True(warm.Code == 0, "登录门预çƒå¤±è´¥ï¼šmeta 应 code=0,实际 " + warm.Code + " " + warm.Message);
}
/// <summary>幂ç‰åˆ›å»º/æ¢å¤å†’烟应用 + é…置(仅进程内临时 SQLite åº“ï¼Œç¬¦åˆæ•°æ®é“律)。</summary>
private void EnsureApp()
{
var app = App.FindByName(AppName);
if (app == null)
{
app = new App { Name = AppName, DisplayName = "é…置冒烟", Secret = AppSecret, Enable = true };
app.Insert();
}
else if (app.Secret != AppSecret || !app.Enable || app.ReRegister != ReRegisterModes.Default)
{
app.Secret = AppSecret;
app.Enable = true;
app.ReRegister = ReRegisterModes.Default;
app.Update();
}
var cfg = AppConfig.FindByName(AppName);
if (cfg == null)
{
cfg = new AppConfig { Name = AppName, AppId = app.Id, Enable = true, Version = 1 };
cfg.Insert();
}
}
private static void DisableApp()
{
var app = App.FindByName(AppName);
if (app != null && app.Enable)
{
app.Enable = false;
app.Update();
}
}
/// <summary>C01(进程内事实):ConfigController 公开动作仅 GetAll/GetAllByApp 两个(GetAllByApp=刀1 预览链改é“端点,2026-11-12 åˆå…¥ï¼‰ï¼›
/// æ–°å‰ç«¯æ— æ ¹ Config 管ç†é¡µï¼ŒUI æ–言归 5188 窗å£ã€‚</summary>
[Fact]
public void ConfigController_HasOnlyGetAll_PublicAction()
{
var t = typeof(Stardust.Web.Controllers.ConfigController);
var actions = t.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(m => m.DeclaringType == t)
.Select(m => m.Name)
.OrderBy(n => n)
.ToArray();
_output.WriteLine("[C01] ConfigController actions: " + String.Join(", ", actions));
Assert.Equal(new[] { "GetAll", "GetAllByApp" }, actions);
}
/// <summary>C12/C04:空 appId → 傿•°é”™è¯¯ä¿¡å°ï¼ˆç™»å½•é—¨ä¹‹å‰æŠ›å‡ºï¼ŒHTTP 200 + code éž 0 + message å« appId)。</summary>
[Fact]
public async Task GetAll_EmptyAppId_ReturnsParamError()
{
var resp = await RawGetAsync("Config/GetAll");
var body = await BodyOf(resp);
Assert.True(body.TryGetProperty("code", out var c) && c.GetInt32() != 0, "空 appId 应返回错误信å°ï¼š" + body);
Assert.True(body.TryGetProperty("message", out var m) && m.GetString()!.Contains("appId", StringComparison.OrdinalIgnoreCase), "错误信å°åº”指明 appId:" + body);
}
/// <summary>C11:匿åï¼ˆæ— Bearer)→ 登录门 code=401「未登录ï¼ã€ã€‚三轮实è¯é¡ºåºæ— 关。</summary>
[Fact]
public async Task GetAll_Anonymous_ReturnsUnauthorizedSemantic()
{
var resp = await RawGetAsync("Config/GetAll?appId=" + AppName + "&secret=" + AppSecret, auth: false);
var body = await BodyOf(resp);
var http401 = (Int32)resp.StatusCode == 401;
var code401 = body.TryGetProperty("code", out var c) && c.GetInt32() == 401;
Assert.True(http401 || code401, "匿å请求应得 401 è¯ä¹‰ï¼ˆHTTP=" + (Int32)resp.StatusCode + ",body=" + body + ")");
}
/// <summary>
/// C02/C03(进程内真值):预çƒåŽ Bearer 用户 JWT + æ£ç¡®åº”用密钥 → code=0 ä¸ƒå—æ®µä¿¡å°
/// (version/updateTime 必在;cfgsmoke æ— é…ç½®æ•°æ®æ•… configs 为空对象)。
/// </summary>
[Fact]
public async Task GetAll_BearerUserToken_ReturnsSevenFieldEnvelope()
{
EnsureApp();
await WarmUpAsync();
var resp = await RawGetAsync("Config/GetAll?appId=" + AppName + "&secret=" + AppSecret, auth: true);
var body = await BodyOf(resp);
Assert.True(body.TryGetProperty("code", out var c) && c.GetInt32() == 0, "预çƒåŽ Bearer+æ£ç¡®å¯†é’¥åº”放行 code=0(HTTP=" + (Int32)resp.StatusCode + ",body=" + body + ")");
Assert.True(body.TryGetProperty("data", out var data) && data.TryGetProperty("version", out var ver) && ver.TryGetInt32(out var verN) && verN >= 0, "data.version 应å˜åœ¨ï¼š" + body);
Assert.True(data.TryGetProperty("updateTime", out var upd) && !String.IsNullOrEmpty(upd.GetString()), "data.updateTime 应å˜åœ¨ï¼š" + body);
}
/// <summary>
/// C13(进程内真值):预çƒåŽï¼Œæ˜¾å¼ ReRegister=Off 的应用 + 错误密钥 →
/// 密钥门按「关é—ã€è£å†³ → 403ã€Œéžæ³•访问ã€ï¼ˆAppTokenService 安全纵深 V-1/V-2)。finally æ¢å¤ã€‚
/// </summary>
[Fact]
public async Task GetAll_WrongSecret_RejectedInOffMode()
{
EnsureApp();
var app = App.FindByName(AppName);
app.ReRegister = ReRegisterModes.Off;
app.Update();
try
{
await WarmUpAsync();
var resp = await RawGetAsync("Config/GetAll?appId=" + AppName + "&secret=wrong-secret", auth: true);
var body = await BodyOf(resp);
Assert.True(body.TryGetProperty("code", out var c) && c.GetInt32() == 403, "显å¼å…³é—+错密钥应 403 éžæ³•访问(HTTP=" + (Int32)resp.StatusCode + ",body=" + body + ")");
Assert.True(body.TryGetProperty("message", out var m) && m.GetString()!.Contains("éžæ³•访问", StringComparison.Ordinal), "403 消æ¯åº”æŒ‡æ˜Žéžæ³•访问:" + body);
}
finally
{
EnsureApp();
}
}
/// <summary>C10(进程内真值):预çƒåŽï¼Œç¦ç”¨åº”用 → ç¦ç”¨é—¨ï¼ˆå…ˆäºŽå¯†é’¥æ ¡éªŒï¼‰â†’ 403「应用[cfgsmoke]å·²ç¦ç”¨ï¼ã€ã€‚finally æ¢å¤å¯ç”¨ã€‚</summary>
[Fact]
public async Task GetAll_DisabledApp_Returns403Disabled()
{
EnsureApp();
DisableApp();
try
{
await WarmUpAsync();
var resp = await RawGetAsync("Config/GetAll?appId=" + AppName + "&secret=" + AppSecret, auth: true);
var body = await BodyOf(resp);
Assert.True(body.TryGetProperty("code", out var c) && c.GetInt32() == 403, "ç¦ç”¨åº”用应 403(HTTP=" + (Int32)resp.StatusCode + ",body=" + body + ")");
Assert.True(body.TryGetProperty("message", out var m) && m.GetString()!.Contains("ç¦ç”¨", StringComparison.Ordinal), "403 消æ¯åº”指明ç¦ç”¨ï¼š" + body);
}
finally
{
EnsureApp();
}
}
}
|