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

namespace Stardust.McpApiTests;

/// <summary>通过真实 WebApi(6681) 平台 API 创建 MCP 令牌(与浏览器 POST /Platform/McpToken 同路径)。</summary>
internal static class McpPlatformApiHelper
{
    private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

    /// <summary>管理员登录,返回 JWT;失败返回 null(测试应 Skip 或 Assert)。</summary>
    public static async Task<String?> TryLoginAdminAsync()
    {
        using var client = new HttpClient { BaseAddress = new Uri(McpTestDataInitializer.BaseUrl) };
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var body = new StringContent(
            JsonSerializer.Serialize(new { username = McpTestDataInitializer.AdminUser, password = McpTestDataInitializer.AdminPassword }),
            Encoding.UTF8,
            "application/json");
        var resp = await client.PostAsync("/Auth/Login", body);
        var text = await resp.Content.ReadAsStringAsync();
        if (!resp.IsSuccessStatusCode) return null;
        return ParseAccessToken(text);
    }

    /// <summary>POST /api/Platform/McpToken,走 McpTokenController.OnInsert(含默认 scope)。</summary>
    public static async Task<(Boolean Ok, String Message, Int32 Id, String Token)> CreateMcpTokenAsync(
        String accessToken,
        String name,
        Int32 userId,
        Int32 projectId,
        String scopesCsv = "")
    {
        using var client = new HttpClient { BaseAddress = new Uri(McpTestDataInitializer.BaseUrl) };
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        var payload = new
        {
            name,
            enable = true,
            userId,
            projectId,
            scopes = scopesCsv,
            remark = "McpApiTests create-flow",
        };
        var content = new StringContent(JsonSerializer.Serialize(payload, JsonOpts), Encoding.UTF8, "application/json");
        var resp = await client.PostAsync("/api/Platform/McpToken", content);
        var text = await resp.Content.ReadAsStringAsync();
        if (!resp.IsSuccessStatusCode)
            return (false, $"HTTP {(Int32)resp.StatusCode}: {text.Cut(400)}", 0, "");

        using var doc = JsonDocument.Parse(text);
        var root = doc.RootElement;
        if (root.TryGetProperty("code", out var codeEl) && codeEl.GetInt32() != 0)
            return (false, root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() ?? "code!=0" : "code!=0", 0, "");

        if (!root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object)
            return (false, "响应缺少 data", 0, "");

        var id = data.TryGetProperty("id", out var idEl) ? idEl.GetInt32()
            : data.TryGetProperty("Id", out var idEl2) ? idEl2.GetInt32() : 0;
        var token = data.TryGetProperty("token", out var tokEl) ? tokEl.GetString() ?? ""
            : data.TryGetProperty("Token", out var tokEl2) ? tokEl2.GetString() ?? "" : "";
        if (id <= 0 || token.IsNullOrEmpty())
            return (false, $"data 缺 Id/Token:{text.Cut(400)}", id, token);

        return (true, "ok", id, token);
    }

    /// <summary>POST /api/Platform/ProjectUser,把用户加入项目(Enable=true)。</summary>
    public static async Task<(Boolean Ok, String Message, Int32 Id)> CreateProjectUserAsync(
        String accessToken, Int32 projectId, Int32 userId)
    {
        using var client = new HttpClient { BaseAddress = new Uri(McpTestDataInitializer.BaseUrl) };
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var payload = new { projectId, userId, enable = true, remark = "McpApiTests member-flow" };
        var content = new StringContent(JsonSerializer.Serialize(payload, JsonOpts), Encoding.UTF8, "application/json");
        var resp = await client.PostAsync("/api/Platform/ProjectUser", content);
        var text = await resp.Content.ReadAsStringAsync();
        if (!resp.IsSuccessStatusCode)
            return (false, $"HTTP {(Int32)resp.StatusCode}: {text.Cut(400)}", 0);
        return ParseEntityId(text);
    }

    public static async Task DeleteProjectUserAsync(String accessToken, Int32 id)
    {
        if (id <= 0) return;
        using var client = new HttpClient { BaseAddress = new Uri(McpTestDataInitializer.BaseUrl) };
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        // Cube 6.15:DELETE 的 id 在查询串。路径 /{id} 会被当成 action,现网返回 405。
        await client.DeleteAsync($"/api/Platform/ProjectUser?id={id}");
    }

    /// <summary>GET /api/Platform/ProjectUser?projectId= ,返回行 Id。用于确认 Search 按项目过滤。</summary>
    public static async Task<(Boolean Ok, String Message, List<Int32> Ids)> ListProjectUserIdsAsync(
        String accessToken, Int32 projectId)
    {
        using var client = new HttpClient { BaseAddress = new Uri(McpTestDataInitializer.BaseUrl) };
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var resp = await client.GetAsync($"/api/Platform/ProjectUser?projectId={projectId}&PageSize=500");
        var text = await resp.Content.ReadAsStringAsync();
        if (!resp.IsSuccessStatusCode)
            return (false, $"HTTP {(Int32)resp.StatusCode}: {text.Cut(400)}", []);
        using var doc = JsonDocument.Parse(text);
        var root = doc.RootElement;
        if (root.TryGetProperty("code", out var codeEl) && codeEl.ValueKind == JsonValueKind.Number && codeEl.GetInt32() != 0)
            return (false, root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() ?? "code!=0" : "code!=0", []);
        if (!root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Array)
            return (false, "响应 data 不是数组", []);
        var ids = new List<Int32>();
        foreach (var row in data.EnumerateArray())
        {
            if (row.ValueKind != JsonValueKind.Object) continue;
            if (row.TryGetProperty("id", out var idEl) && idEl.TryGetInt32(out var id)) ids.Add(id);
            else if (row.TryGetProperty("Id", out var idEl2) && idEl2.TryGetInt32(out var id2)) ids.Add(id2);
        }
        return (true, "ok", ids);
    }

    private static (Boolean Ok, String Message, Int32 Id) ParseEntityId(String text)
    {
        using var doc = JsonDocument.Parse(text);
        var root = doc.RootElement;
        if (root.TryGetProperty("code", out var codeEl) && codeEl.ValueKind == JsonValueKind.Number && codeEl.GetInt32() != 0)
            return (false, root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() ?? "code!=0" : "code!=0", 0);
        if (!root.TryGetProperty("data", out var data))
            return (false, "响应缺少 data", 0);
        if (data.ValueKind == JsonValueKind.Number && data.TryGetInt32(out var bare))
            return (true, "ok", bare);
        if (data.ValueKind != JsonValueKind.Object)
            return (false, "响应 data 不是实体", 0);
        var id = data.TryGetProperty("id", out var idEl) && idEl.TryGetInt32(out var i1) ? i1
            : data.TryGetProperty("Id", out var idEl2) && idEl2.TryGetInt32(out var i2) ? i2 : 0;
        if (id <= 0) return (false, $"data 缺 Id:{text.Cut(400)}", 0);
        return (true, "ok", id);
    }

    public static async Task DeleteMcpTokenAsync(String accessToken, Int32 id)
    {
        if (id <= 0) return;
        using var client = new HttpClient { BaseAddress = new Uri(McpTestDataInitializer.BaseUrl) };
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        await client.DeleteAsync($"/api/Platform/McpToken/{id}");
    }

    private static String? ParseAccessToken(String json)
    {
        using var doc = JsonDocument.Parse(json);
        if (!doc.RootElement.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object)
            return null;
        if (data.TryGetProperty("access_token", out var at1) && !String.IsNullOrEmpty(at1.GetString())) return at1.GetString();
        if (data.TryGetProperty("AccessToken", out var at2) && !String.IsNullOrEmpty(at2.GetString())) return at2.GetString();
        return null;
    }
}