Files
wx-win-agent/tests/WxAgent.Core.Tests/WechatMergedChatTests.cs
T
rogee 30df7ae433 feat: expand merged chat records directly from read-only databases
Add db merged with exact account/chat/local-ID selection and shard ambiguity checks. Preserve Int64 composite message types and parse bounded, DTD-free embedded record XML, including duplicate messages and source metadata. Revalidate cached keys against current page-1 HMAC; replace the SQL write probe with sqlite3_db_readonly. Core tests: 129 passed. Windows Session 0 database validation: 5/5; all 16 text/sender/source-ID/timestamp entries matched independent XML parsing. Read-only UI baseline: 3/3; zero sends.
2026-09-06 20:10:04 +08:00

85 lines
4.9 KiB
C#

using System.Text;
using System.Text.Json;
using System.Xml.Linq;
using WxAgent.Core;
using Xunit;
using ZstdSharp;
namespace WxAgent.Core.Tests;
public sealed class WechatMergedChatTests
{
[Fact]
public void ParsesEmbeddedTextMetadataWithoutDeduplicatingRepeatedMessages()
{
var result = WechatMergedChatParser.Parse(Wrap(Record(Item("same\n\ntext"), Item("same\n\ntext"))));
Assert.Equal(2, result.Messages.Count);
Assert.Equal("0", result.Messages[0].Path);
Assert.Equal("1", result.Messages[1].Path);
Assert.Equal("same\n\ntext", result.Messages[0].Text);
Assert.Equal("Synthetic sender", result.Messages[0].SenderName);
Assert.Equal("opaque-hash", result.Messages[0].SenderHash);
Assert.Equal("18446744073709551615", result.Messages[0].SourceServerId);
Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1700000000), result.Messages[0].Timestamp);
}
[Fact]
public void SupportsNestedRecordsAndExplicitAttachmentMetadata()
{
var attachment = new XElement("dataitem", new XAttribute("datatype", 8),
new XElement("datatitle", "example.txt"), new XElement("datafmt", "txt"), new XElement("datasize", 42),
new XElement("cdn_dataurl", "must-not-be-exported"), new XElement("aeskey", "must-not-be-exported"));
var nested = new XElement("dataitem", new XAttribute("datatype", 17), Record(Item("nested")));
var result = WechatMergedChatParser.Parse(Record(attachment, nested).ToString());
Assert.Null(result.Messages[0].Text);
Assert.Equal(42, result.Messages[0].SizeBytes);
Assert.Equal("1/0", result.Messages[1].NestedRecord!.Messages[0].Path);
Assert.DoesNotContain("must-not-be-exported", JsonSerializer.Serialize(result));
}
[Fact]
public void RejectsTruncationMissingPayloadAndInvalidTimestamp()
{
var record = Record(Item("hello"));
record.Element("datalist")!.SetAttributeValue("count", 2);
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(record.ToString()));
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(Record(new XElement("dataitem", new XAttribute("datatype", 1))).ToString()));
var item = Item("hello");
item.Element("srcMsgCreateTime")!.Value = long.MaxValue.ToString();
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(Record(item).ToString()));
}
[Fact]
public void RejectsDtdAndBoundViolations()
{
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse("<!DOCTYPE recordinfo [<!ENTITY x SYSTEM 'file:///not-read'>]><recordinfo>&x;</recordinfo>"));
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(new string('x', WechatMergedChatParser.MaxXmlCharacters + 1)));
var nested = Record(Item("innermost"));
for (var i = 0; i < WechatMergedChatParser.MaxNesting; i++)
nested = Record(new XElement("dataitem", new XAttribute("datatype", 17), nested));
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(nested.ToString()));
Assert.Throws<InvalidDataException>(() => WechatMergedChatParser.Parse(Record(Enumerable.Range(0, WechatMergedChatParser.MaxMessages + 1).Select(_ => Item("x")).ToArray()).ToString()));
}
[Fact]
public void PreservesComposite64BitMessageTypeAndBoundsZstdDecoding()
{
var message = new DbMessage(149, 123, "filehelper", null, null, null, 81604378673L, "xml", DateTimeOffset.UnixEpoch, null);
using var json = JsonDocument.Parse(JsonSerializer.Serialize(message));
Assert.Equal(81604378673L, json.RootElement.GetProperty("Type").GetInt64());
using var output = new MemoryStream();
using (var compressed = new CompressionStream(output))
compressed.Write(Encoding.UTF8.GetBytes(new string('x', WechatMergedChatParser.MaxXmlCharacters + 1)));
Assert.Throws<InvalidDataException>(() => WechatDbMessage.DecodeContent(Convert.ToHexString(output.ToArray()), true));
}
private static XElement Item(string text) => new("dataitem", new XAttribute("datatype", 1), new XAttribute("dataid", "repeated-id"),
new XElement("datadesc", text), new XElement("sourcename", "Synthetic sender"), new XElement("srcMsgLocalId", "7"),
new XElement("srcMsgCreateTime", "1700000000"), new XElement("fromnewmsgid", "18446744073709551615"),
new XElement("dataitemsource", new XElement("hashusername", "opaque-hash")));
private static XElement Record(params XElement[] items) => new("recordinfo", new XElement("title", "Synthetic record"),
new XElement("datalist", new XAttribute("count", items.Length), items));
private static string Wrap(XElement record) => new XElement("msg", new XElement("appmsg", new XElement("type", 19),
new XElement("recorditem", new XCData(record.ToString(SaveOptions.DisableFormatting))))).ToString();
}