- ASP.NET Core 9 Razor Pages + Minimal API hybrid - 14 validators (CPF, CEP, CNPJ, email, phone, name, yes-no, birthdate, handoff, cancel-intent, company-name, plate-br, postal-code, validate_reply) - OAuth login (Google, Microsoft, GitHub) + cookie auth - MongoDB usage tracking + CEP cache collection - Stripe checkout with inline PriceData (no Price IDs) - MCP server for Claude Code / Cursor integration - Playground (10 calls/IP/day, no auth) - Docs: Quickstart, API Reference, N8N, MCP, Créditos, Erros, Fluxos - Credit system: 3 cr standard validators, 5 cr validate_reply - SmartSuggestion: contextual re-ask via IA when obtained=false - Per-IP rate limiting + daily cap + shared-IP abuse detection - Lightbox for comic images - Validadores page split: Brasileiros / Universais + Em breve Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 lines
7.5 KiB
C#
200 lines
7.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Xunit;
|
|
|
|
namespace Nalu.Tests;
|
|
|
|
public class McpServerTests : IClassFixture<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly HttpClient _client;
|
|
|
|
public McpServerTests(WebApplicationFactory<Program> factory)
|
|
{
|
|
_client = factory.CreateClient();
|
|
_client.DefaultRequestHeaders.Add("Authorization", "Bearer nalu-test-key-001");
|
|
}
|
|
|
|
private async Task<JsonNode?> PostMcp(object body)
|
|
{
|
|
var json = JsonSerializer.Serialize(body);
|
|
var response = await _client.PostAsync("/mcp",
|
|
new StringContent(json, Encoding.UTF8, "application/json"));
|
|
response.EnsureSuccessStatusCode();
|
|
var text = await response.Content.ReadAsStringAsync();
|
|
return JsonNode.Parse(text);
|
|
}
|
|
|
|
// ── Auth ──────────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Mcp_NoApiKey_Returns401()
|
|
{
|
|
using var client = new HttpClient { BaseAddress = _client.BaseAddress };
|
|
var response = await client.PostAsync("/mcp",
|
|
new StringContent("{}", Encoding.UTF8, "application/json"));
|
|
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
}
|
|
|
|
// ── initialize ────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Mcp_Initialize_ReturnsServerInfo()
|
|
{
|
|
var result = await PostMcp(new
|
|
{
|
|
jsonrpc = "2.0",
|
|
method = "initialize",
|
|
@params = new { protocolVersion = "2024-11-05", capabilities = new { } },
|
|
id = 1
|
|
});
|
|
|
|
result.Should().NotBeNull();
|
|
result!["result"]!["serverInfo"]!["name"]!.GetValue<string>().Should().Be("nalu-ai");
|
|
result["result"]!["protocolVersion"]!.GetValue<string>().Should().Be("2024-11-05");
|
|
result["result"]!["capabilities"]!["tools"].Should().NotBeNull();
|
|
}
|
|
|
|
// ── tools/list ────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Mcp_ToolsList_ContainsAllValidators()
|
|
{
|
|
var result = await PostMcp(new
|
|
{
|
|
jsonrpc = "2.0",
|
|
method = "tools/list",
|
|
@params = new { },
|
|
id = 2
|
|
});
|
|
|
|
var tools = result!["result"]!["tools"]!.AsArray();
|
|
tools.Should().NotBeEmpty();
|
|
|
|
var toolNames = tools.Select(t => t!["name"]!.GetValue<string>()).ToList();
|
|
toolNames.Should().Contain("nalu_extract_name");
|
|
toolNames.Should().Contain("nalu_extract_cpf");
|
|
toolNames.Should().Contain("nalu_extract_cep");
|
|
toolNames.Should().Contain("nalu_extract_phone");
|
|
toolNames.Should().Contain("nalu_extract_email");
|
|
toolNames.Should().Contain("nalu_extract_yes_no");
|
|
toolNames.Should().Contain("nalu_extract_postal_code");
|
|
// Universal — new
|
|
toolNames.Should().Contain("nalu_extract_birthdate");
|
|
toolNames.Should().Contain("nalu_extract_handoff");
|
|
toolNames.Should().Contain("nalu_extract_cancel_intent");
|
|
// Brasil — new
|
|
toolNames.Should().Contain("nalu_extract_cnpj");
|
|
toolNames.Should().Contain("nalu_extract_plate_br");
|
|
toolNames.Should().Contain("nalu_extract_company_name");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mcp_ToolsList_EachToolHasInputSchema()
|
|
{
|
|
var result = await PostMcp(new
|
|
{
|
|
jsonrpc = "2.0",
|
|
method = "tools/list",
|
|
@params = new { },
|
|
id = 3
|
|
});
|
|
|
|
var tools = result!["result"]!["tools"]!.AsArray();
|
|
foreach (var tool in tools)
|
|
{
|
|
var schema = tool!["inputSchema"];
|
|
schema.Should().NotBeNull($"tool {tool["name"]} should have inputSchema");
|
|
schema!["properties"]!["agent_input"].Should().NotBeNull();
|
|
schema["properties"]!["user_input"].Should().NotBeNull();
|
|
var required = schema["required"]!.AsArray().Select(r => r!.GetValue<string>()).ToList();
|
|
required.Should().Contain("agent_input");
|
|
required.Should().Contain("user_input");
|
|
}
|
|
}
|
|
|
|
// ── tools/call ────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Mcp_ToolsCall_ExtractName_WithGreeting_ObtainedFalse()
|
|
{
|
|
var result = await PostMcp(new
|
|
{
|
|
jsonrpc = "2.0",
|
|
method = "tools/call",
|
|
@params = new
|
|
{
|
|
name = "nalu_extract_name",
|
|
arguments = new
|
|
{
|
|
agent_input = "Qual seu nome completo?",
|
|
user_input = "Bom dia!"
|
|
}
|
|
},
|
|
id = 4
|
|
});
|
|
|
|
result!["result"]!["isError"]!.GetValue<bool>().Should().BeFalse();
|
|
|
|
var content = result["result"]!["content"]!.AsArray();
|
|
content.Should().HaveCount(1);
|
|
content[0]!["type"]!.GetValue<string>().Should().Be("text");
|
|
|
|
var text = content[0]!["text"]!.GetValue<string>();
|
|
var extracted = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(text)!;
|
|
extracted["obtained"].GetBoolean().Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mcp_ToolsCall_UnknownTool_ReturnsError()
|
|
{
|
|
var result = await PostMcp(new
|
|
{
|
|
jsonrpc = "2.0",
|
|
method = "tools/call",
|
|
@params = new { name = "nalu_extract_unknown", arguments = new { } },
|
|
id = 5
|
|
});
|
|
|
|
result!["error"].Should().NotBeNull();
|
|
result["error"]!["code"]!.GetValue<int>().Should().Be(-32602);
|
|
}
|
|
|
|
// ── New .md file → auto-appears in tools/list ─────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Mcp_ToolsList_AllLoadedValidatorsHaveMcpTool()
|
|
{
|
|
// Any validator without mcp_tool should not appear in tools/list
|
|
var result = await PostMcp(new
|
|
{
|
|
jsonrpc = "2.0",
|
|
method = "tools/list",
|
|
@params = new { },
|
|
id = 6
|
|
});
|
|
|
|
var tools = result!["result"]!["tools"]!.AsArray();
|
|
foreach (var tool in tools)
|
|
{
|
|
tool!["name"]!.GetValue<string>().Should().NotBeNullOrWhiteSpace();
|
|
tool!["description"]!.GetValue<string>().Should().NotBeNullOrWhiteSpace();
|
|
}
|
|
}
|
|
|
|
// ── notifications ─────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Mcp_Initialized_Notification_Returns202()
|
|
{
|
|
var json = JsonSerializer.Serialize(new { jsonrpc = "2.0", method = "initialized" });
|
|
var response = await _client.PostAsync("/mcp",
|
|
new StringContent(json, Encoding.UTF8, "application/json"));
|
|
response.StatusCode.Should().Be(HttpStatusCode.Accepted);
|
|
}
|
|
}
|