Files
YMhut-box-C-/src/YMhut.Box.Tests/FeedbackServiceTests.cs
T
QWQLwToo 7ecc6a8923
build-winui / winui (push) Has been cancelled
Add WinUI and core source
2026-06-26 13:27:13 +08:00

410 lines
17 KiB
C#

using System.IO.Compression;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Feedback;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.System;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class FeedbackServiceTests
{
[TestMethod]
public void FeedbackCodeCreatesServerCompatibleCode()
{
var code = FeedbackCode.Create(DateTimeOffset.Parse("2026-06-04T10:00:00+08:00"));
Assert.IsTrue(Regex.IsMatch(code, "^FB-20260604-[A-F0-9]{6}$"));
Assert.AreEqual("FB-20260604-ABC123", FeedbackCode.Normalize(" fb-20260604-abc123 "));
Assert.IsTrue(FeedbackCode.IsValid("fb-20260604-abc123"));
Assert.IsFalse(FeedbackCode.IsValid("FB-20260604-XYZ123"));
}
[TestMethod]
public async Task FeedbackPackageIncludesSelectedAttachmentsAndLocalizedLogs()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-tests", Guid.NewGuid().ToString("N"));
var paths = AppPaths.ForCurrentUser(tempRoot);
var logs = new JsonFileLogService(paths);
var service = new FeedbackPackageService(logs, new FakeSystemMetricsService(), new ToolCatalog());
await logs.WriteAsync("Information", "navigation", "Open toolbox", "https://example.com/private?token=secret");
var package = await service.BuildPackageAsync(new FeedbackRequest(
"同步状态卡住",
"issue",
"major",
"dev@example.com",
"首页一直显示后台校验中。",
IncludeTodayLogs: true,
IncludeToolStatus: true,
IncludeSystemSummary: true,
Language: "zh-CN",
FeedbackCode: "FB-20260604-ABC123"));
try
{
Assert.IsTrue(File.Exists(package.PackagePath));
Assert.IsGreaterThan(0, package.PackageBytes);
Assert.AreEqual(
Path.GetFullPath(FeedbackPackageService.FeedbackPackagesRoot()),
Path.GetFullPath(Path.GetDirectoryName(package.PackagePath)!));
Assert.IsTrue(package.IncludedFiles.ContainsKey("feedback.json"));
Assert.IsTrue(package.IncludedFiles.ContainsKey("tool-status.json"));
Assert.IsTrue(package.IncludedFiles.ContainsKey("system-summary.json"));
Assert.IsTrue(package.IncludedFiles.Keys.Any(name => name.StartsWith("logs-", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(package.SummaryText.Contains("首页一直显示后台校验中。", StringComparison.Ordinal));
Assert.IsTrue(package.SummaryText.Contains("FB-20260604-ABC123", StringComparison.Ordinal));
using var archive = ZipFile.OpenRead(package.PackagePath);
Assert.IsNotNull(archive.GetEntry("feedback.json"));
Assert.IsNotNull(archive.GetEntry("summary.txt"));
var feedbackJson = ReadEntry(archive.GetEntry("feedback.json")!);
using var feedbackDocument = JsonDocument.Parse(feedbackJson);
Assert.AreEqual("FB-20260604-ABC123", feedbackDocument.RootElement.GetProperty("feedbackCode").GetString());
Assert.AreEqual("FB-20260604-ABC123", feedbackDocument.RootElement.GetProperty("request").GetProperty("feedbackCode").GetString());
var logsEntry = archive.Entries.Single(entry => entry.FullName.StartsWith("logs-", StringComparison.OrdinalIgnoreCase));
var logsJson = ReadEntry(logsEntry);
using var logsDocument = JsonDocument.Parse(logsJson);
var firstLog = logsDocument.RootElement[0];
Assert.AreEqual("导航", firstLog.GetProperty("display").GetProperty("category").GetString());
Assert.AreEqual("打开工具箱", firstLog.GetProperty("display").GetProperty("message").GetString());
Assert.AreEqual("远程服务", firstLog.GetProperty("display").GetProperty("detail").GetString());
Assert.AreEqual("远程服务", firstLog.GetProperty("raw").GetProperty("detail").GetString());
Assert.IsFalse(logsJson.Contains("example.com/private", StringComparison.OrdinalIgnoreCase));
var toolStatus = ReadEntry(archive.GetEntry("tool-status.json")!);
StringAssert.Contains(toolStatus, "\"id\": \"compression_codec\"");
StringAssert.Contains(toolStatus, "\"exists\": true");
}
finally
{
DeleteIfExists(package.PackagePath);
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public async Task FeedbackPackageCryptoRoundTripsLocalZip()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var sourceZip = Path.Combine(tempRoot, "feedback.zip");
var decryptedZip = Path.Combine(tempRoot, "feedback-decrypted.zip");
try
{
using (var archive = ZipFile.Open(sourceZip, ZipArchiveMode.Create))
{
var entry = archive.CreateEntry("summary.txt");
await using var stream = entry.Open();
await using var writer = new StreamWriter(stream);
await writer.WriteAsync("反馈包加密测试");
}
var encrypted = await FeedbackPackageCrypto.EncryptPackageAsync(sourceZip);
Assert.IsTrue(File.Exists(encrypted.PackagePath));
Assert.AreEqual(".ymfb", Path.GetExtension(encrypted.PackagePath));
Assert.IsGreaterThan(new FileInfo(sourceZip).Length, encrypted.PackageBytes);
await FeedbackPackageCrypto.DecryptPackageAsync(encrypted.PackagePath, decryptedZip);
CollectionAssert.AreEqual(
await File.ReadAllBytesAsync(sourceZip),
await File.ReadAllBytesAsync(decryptedZip));
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task FeedbackPackageRejectsScreenshotOverFiveMegabytes()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-tests", Guid.NewGuid().ToString("N"));
var paths = AppPaths.ForCurrentUser(tempRoot);
var logs = new JsonFileLogService(paths);
var service = new FeedbackPackageService(logs, new FakeSystemMetricsService(), new ToolCatalog());
var screenshot = Path.Combine(tempRoot, "large.png");
await File.WriteAllBytesAsync(screenshot, new byte[FeedbackPackageService.MaxScreenshotBytes + 1]);
try
{
await Assert.ThrowsExactlyAsync<InvalidOperationException>(() =>
service.BuildPackageAsync(new FeedbackRequest(
"截图过大",
"issue",
"normal",
string.Empty,
"测试截图限制。",
IncludeTodayLogs: false,
IncludeToolStatus: false,
IncludeSystemSummary: false,
ScreenshotPath: screenshot)));
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public void FeedbackSubmissionSignatureIsStable()
{
var signature = FeedbackSubmissionService.Sign("1760000000", "abc123", new string('a', 64), "{\"ok\":true}");
Assert.AreEqual(64, signature.Length);
Assert.IsTrue(signature.All(character => Uri.IsHexDigit(character) && !char.IsUpper(character)));
Assert.AreEqual(signature, FeedbackSubmissionService.Sign("1760000000", "abc123", new string('a', 64), "{\"ok\":true}"));
}
[TestMethod]
public void FeedbackStatusResponseParsesExtendedServerFields()
{
var response = ParseStatusResponse("""
{
"ok": true,
"code": "FB-20260604-ABC123",
"status": "investigating",
"statusLabel": "处理中",
"statusDetail": "已收到日志,正在定位。",
"category": "issue",
"priority": "major",
"hasReply": true,
"reply": "请先保留现场文件。",
"receivedAt": "2026-06-04T10:00:00+08:00",
"updatedAt": "2026-06-04T10:30:00+08:00",
"mailSent": true
}
""");
Assert.IsTrue(response.Ok);
Assert.AreEqual("FB-20260604-ABC123", response.Code);
Assert.AreEqual("investigating", response.Status);
Assert.AreEqual("处理中", response.StatusLabel);
Assert.AreEqual("已收到日志,正在定位。", response.StatusDetail);
Assert.AreEqual("issue", response.Category);
Assert.AreEqual("major", response.Priority);
Assert.IsTrue(response.HasReply);
Assert.AreEqual("请先保留现场文件。", response.Reply);
Assert.AreEqual("2026-06-04T10:30:00+08:00", response.UpdatedAt);
Assert.IsTrue(response.MailSent);
}
[TestMethod]
public async Task FeedbackRecordStoreReadsOldRecordsWithoutExtendedFields()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-record-tests", Guid.NewGuid().ToString("N"));
var storagePath = Path.Combine(tempRoot, "feedback-records.json");
Directory.CreateDirectory(tempRoot);
await File.WriteAllTextAsync(storagePath, """
[
{
"code": "FB-20260604-ABC123",
"title": "Old record",
"type": "issue",
"severity": "normal",
"createdAtUtc": "2026-06-04T02:00:00+00:00",
"updatedAtUtc": "2026-06-04T02:10:00+00:00",
"state": "Sent",
"statusLabel": "Sent",
"lastMessage": "The service received it."
}
]
""");
try
{
var record = await new FeedbackRecordStore(storagePath).FindAsync("fb-20260604-abc123");
Assert.IsNotNull(record);
Assert.AreEqual("FB-20260604-ABC123", record.Code);
Assert.AreEqual("Sent", record.StatusLabel);
Assert.IsNull(record.ServiceStatusDetail);
Assert.IsNull(record.ServiceCategory);
Assert.IsNull(record.ServicePriority);
Assert.IsNull(record.ServiceMailSent);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task FeedbackRecordStoreKeepsStatusMessageAndPublicReplySeparate()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-record-tests", Guid.NewGuid().ToString("N"));
var storagePath = Path.Combine(tempRoot, "feedback-records.json");
var store = new FeedbackRecordStore(storagePath);
var now = DateTimeOffset.Parse("2026-06-04T03:00:00Z");
try
{
await store.UpsertAsync(new FeedbackRecord(
"FB-20260604-ABC123",
"Status query",
"issue",
"major",
now,
now,
FeedbackRecordState.StatusUpdated,
"处理中",
"已收到日志,正在定位。",
LastQueryAtUtc: now,
ServiceStatus: "investigating",
PublicReply: "请先保留现场文件。",
ServiceStatusDetail: "已收到日志,正在定位。",
ServiceCategory: "issue",
ServicePriority: "major",
ServiceMailSent: true,
ServiceReceivedAt: "2026-06-04T10:00:00+08:00",
ServiceUpdatedAt: "2026-06-04T10:30:00+08:00"));
var record = await store.FindAsync("FB-20260604-ABC123");
Assert.IsNotNull(record);
Assert.AreEqual("已收到日志,正在定位。", record.LastMessage);
Assert.IsFalse(record.LastMessage.Contains("请先保留现场文件。", StringComparison.Ordinal));
Assert.AreEqual("请先保留现场文件。", record.PublicReply);
Assert.AreEqual("investigating", record.ServiceStatus);
Assert.AreEqual("issue", record.ServiceCategory);
Assert.AreEqual("major", record.ServicePriority);
Assert.IsTrue(record.ServiceMailSent);
Assert.AreEqual("2026-06-04T10:30:00+08:00", record.ServiceUpdatedAt);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task FeedbackRecordStoreArchivesOldRecordsAndKeepsThemQueryable()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-record-tests", Guid.NewGuid().ToString("N"));
var storagePath = Path.Combine(tempRoot, "feedback-records.json");
var store = new FeedbackRecordStore(storagePath);
var now = DateTimeOffset.Parse("2026-06-05T12:00:00Z");
var oldCreated = now.AddDays(-11);
try
{
await store.UpsertAsync(new FeedbackRecord(
"FB-20260525-ABC123",
"旧反馈",
"issue",
"normal",
oldCreated,
oldCreated,
FeedbackRecordState.Sent,
"发送成功",
"服务端已接收。",
PackagePath: Path.Combine(tempRoot, "feedback.zip"),
PackageSha256: new string('a', 64),
PackageBytes: 128,
SubmittedAtUtc: oldCreated));
var archived = await store.ArchiveOlderThanAsync(TimeSpan.FromDays(10), now);
var record = archived.Single();
Assert.IsTrue(record.IsArchived);
Assert.AreEqual(FeedbackRecordState.Archived, record.State);
Assert.AreEqual("已归档", record.StatusLabel);
Assert.IsNotNull(record.ArchivedAtUtc);
var updated = await store.UpsertAsync(record with
{
State = FeedbackRecordState.StatusUpdated,
StatusLabel = "状态已更新",
LastMessage = "当前状态:已处理。",
UpdatedAtUtc = now.AddMinutes(5),
LastQueryAtUtc = now.AddMinutes(5)
});
Assert.IsTrue(updated.IsArchived);
Assert.AreEqual(FeedbackRecordState.StatusUpdated, updated.State);
Assert.AreEqual("状态已更新", updated.StatusLabel);
Assert.AreEqual("当前状态:已处理。", updated.LastMessage);
var reloaded = await new FeedbackRecordStore(storagePath).FindAsync("fb-20260525-abc123");
Assert.IsNotNull(reloaded);
Assert.IsTrue(reloaded.IsArchived);
Assert.AreEqual("FB-20260525-ABC123", reloaded.Code);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
private static string ReadEntry(ZipArchiveEntry entry)
{
using var stream = entry.Open();
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
private static FeedbackStatusResponse ParseStatusResponse(string json)
{
var method = typeof(FeedbackSubmissionService).GetMethod("ParseStatusResponse", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(method);
return (FeedbackStatusResponse)method.Invoke(null, new object?[] { json })!;
}
private static void DeleteIfExists(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
private sealed class FakeSystemMetricsService : ISystemMetricsService
{
public SystemMetricsSnapshot Capture()
{
return new SystemMetricsSnapshot(
"TEST-PC",
"Windows Test",
8,
1024,
2048,
42.5,
16UL * 1024 * 1024 * 1024,
8UL * 1024 * 1024 * 1024,
8UL * 1024 * 1024 * 1024,
50,
1.2,
TimeSpan.FromMinutes(12),
24,
512,
256UL * 1024 * 1024 * 1024,
128UL * 1024 * 1024 * 1024,
128UL * 1024 * 1024 * 1024,
50,
true,
".NET 10.0",
DateTimeOffset.Parse("2026-06-04T10:00:00+08:00"));
}
}
}