diff --git a/.gitattributes b/.gitattributes index c9c7dbe..723273b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,4 +13,5 @@ *.docx diff=astextplain *.DOCX diff=astextplain +src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/** text eol=lf diff --git a/src/HuaJiBot.NET.UnitTest/FlueBridgeProtocolTest.cs b/src/HuaJiBot.NET.UnitTest/FlueBridgeProtocolTest.cs new file mode 100644 index 0000000..4f227c7 --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/FlueBridgeProtocolTest.cs @@ -0,0 +1,44 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using HuaJiBot.NET.Plugin.FlueBridge; + +namespace HuaJiBot.NET.UnitTest; + +public partial class FlueBridgeTest +{ + private static string ProtocolFixture(string name) => + File.ReadAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", "AgentProtocol", name)); + + [Test] + public void SharedProtocolExamplesRoundTripThroughActualBridgeRecords() + { + var ingressJson = ProtocolFixture("examples/ingress.json"); + var ingress = JsonSerializer.Deserialize(ingressJson, BridgeJson.Options)!; + var replyJson = ProtocolFixture("examples/reply-job.json"); + var reply = JsonSerializer.Deserialize(replyJson, BridgeJson.Options)!; + Assert.Multiple(() => + { + Assert.That(JsonNode.DeepEquals(JsonNode.Parse(ingressJson), JsonSerializer.SerializeToNode(ingress, BridgeJson.Options)), Is.True); + Assert.That(JsonNode.DeepEquals(JsonNode.Parse(replyJson), JsonSerializer.SerializeToNode(reply, BridgeJson.Options)), Is.True); + Assert.That(ingress.Kind, Is.EqualTo("chat")); + Assert.That(reply.Destination.ReplyToMessageId, Is.EqualTo(ingress.Message.MessageId)); + }); + } + + [Test] + public void SharedProtocolVectorsMatchRawBodySignatureAndIdentifierEncoding() + { + using var vectors = JsonDocument.Parse(ProtocolFixture("test-vectors.json")); + foreach (var vector in vectors.RootElement.GetProperty("hmac").EnumerateArray()) + { + var actual = "v1=" + IngressClient.Sign(vector.GetProperty("secret").GetString()!, + vector.GetProperty("timestamp").GetString()!, Encoding.UTF8.GetBytes(vector.GetProperty("body").GetString()!)); + Assert.That(actual, Is.EqualTo(vector.GetProperty("signature").GetString())); + } + foreach (var vector in vectors.RootElement.GetProperty("identifiers").EnumerateArray()) + { + Assert.That(Uri.EscapeDataString(vector.GetProperty("raw").GetString()!), Is.EqualTo(vector.GetProperty("encoded").GetString())); + } + } +} diff --git a/src/HuaJiBot.NET.UnitTest/FlueBridgeTest.cs b/src/HuaJiBot.NET.UnitTest/FlueBridgeTest.cs index 1b3132d..894f147 100644 --- a/src/HuaJiBot.NET.UnitTest/FlueBridgeTest.cs +++ b/src/HuaJiBot.NET.UnitTest/FlueBridgeTest.cs @@ -10,7 +10,7 @@ namespace HuaJiBot.NET.UnitTest; -public class FlueBridgeTest +public partial class FlueBridgeTest { private string _path = null!; private BridgeStore _store = null!; diff --git a/src/HuaJiBot.NET.UnitTest/HuaJiBot.NET.UnitTest.csproj b/src/HuaJiBot.NET.UnitTest/HuaJiBot.NET.UnitTest.csproj index 5103f79..7f04a91 100644 --- a/src/HuaJiBot.NET.UnitTest/HuaJiBot.NET.UnitTest.csproj +++ b/src/HuaJiBot.NET.UnitTest/HuaJiBot.NET.UnitTest.csproj @@ -29,6 +29,7 @@ + diff --git a/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/examples/ingress.json b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/examples/ingress.json new file mode 100644 index 0000000..67b802b --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/examples/ingress.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "eventId": "development:test-bot:test-group:root", + "conversationId": "huajibot:development:test-bot:test-group:root", + "destination": { + "bridgeInstance": "development", + "robotId": "test-bot", + "groupId": "test-group" + }, + "kind": "chat", + "message": { + "messageId": "root", + "senderId": "test-user", + "senderName": "开发测试", + "text": "中文问题", + "images": [], + "observedAt": 1791763200000 + }, + "replyTo": null +} diff --git a/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/examples/reply-job.json b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/examples/reply-job.json new file mode 100644 index 0000000..4b0eee9 --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/examples/reply-job.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "jobId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "conversationId": "huajibot:development:test-bot:test-group:root", + "destination": { + "bridgeInstance": "development", + "robotId": "test-bot", + "groupId": "test-group", + "replyToMessageId": "root" + }, + "content": { + "format": "markdown", + "text": "中文回复" + }, + "createdAt": "2026-10-12T00:00:00+00:00", + "kind": "chat" +} diff --git a/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/ingress.schema.json b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/ingress.schema.json new file mode 100644 index 0000000..13f4745 --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/ingress.schema.json @@ -0,0 +1,149 @@ +{ + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "eventId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "destination": { + "type": "object", + "properties": { + "bridgeInstance": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "robotId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "groupId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "required": [ + "bridgeInstance", + "robotId", + "groupId" + ], + "additionalProperties": false + }, + "kind": { + "enum": [ + "chat", + "archive", + "summary" + ], + "type": "string", + "default": "chat" + }, + "message": { + "type": "object", + "properties": { + "messageId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "senderId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "senderName": { + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + }, + "images": { + "type": "array", + "items": { + "type": "string", + "maxLength": 4096 + }, + "maxItems": 4, + "default": [] + }, + "observedAt": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "messageId", + "senderId", + "senderName", + "text" + ], + "additionalProperties": false + }, + "replyTo": { + "anyOf": [ + { + "type": "object", + "properties": { + "messageId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "senderId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "messageId", + "senderId", + "content" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "version", + "eventId", + "conversationId", + "destination", + "message", + "replyTo" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HuaJiBot local v1 ingress" +} diff --git a/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/reply-job.schema.json b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/reply-job.schema.json new file mode 100644 index 0000000..3697a1d --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/reply-job.schema.json @@ -0,0 +1,89 @@ +{ + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "jobId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "destination": { + "type": "object", + "properties": { + "bridgeInstance": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "robotId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "groupId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "replyToMessageId": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "required": [ + "bridgeInstance", + "robotId", + "groupId", + "replyToMessageId" + ], + "additionalProperties": false + }, + "content": { + "type": "object", + "properties": { + "format": { + "const": "markdown" + }, + "text": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "format", + "text" + ], + "additionalProperties": false + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "kind": { + "enum": [ + "chat", + "summary" + ], + "type": "string" + } + }, + "required": [ + "version", + "jobId", + "conversationId", + "destination", + "content", + "createdAt" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HuaJiBot local v1 reply-job" +} diff --git a/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/test-vectors.json b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/test-vectors.json new file mode 100644 index 0000000..4f45f46 --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/TestData/AgentProtocol/test-vectors.json @@ -0,0 +1,22 @@ +{ + "hmac": [ + { + "secret": "public-test-secret-only", + "timestamp": "1791763200", + "body": "{\"text\":\"中文\"}", + "signature": "v1=616a031110382efc2fb59dde68e941b8031d3e98e1b01ab4e6a83df26d2abf0e" + }, + { + "secret": "public-test-secret-only", + "timestamp": "1791763200", + "body": "{\"text\":\"line1\\nline2\",\"value\":1}", + "signature": "v1=cb0e1392168893429bc5a479a724bbbb2b89ccce1c9b9ffa479d72456c7210d1" + } + ], + "identifiers": [ + { + "raw": "topic:!'()*中文", + "encoded": "topic%3A%21%27%28%29%2A%E4%B8%AD%E6%96%87" + } + ] +}