From 8a72c1582cf452caeebd7c6c4c4f23f113c51d2b Mon Sep 17 00:00:00 2001 From: biss Date: Sat, 22 Aug 2026 18:56:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E6=B8=85?= =?UTF-8?q?=E7=90=86=E6=94=B9=E4=B8=BA=E5=9B=BA=E5=AE=9A=E7=9A=84=20Window?= =?UTF-8?q?s=20=E9=9D=9E=E6=B3=95=E5=AD=97=E7=AC=A6=E9=9B=86=EF=BC=8CLinux?= =?UTF-8?q?=20CI=20=E7=8E=B0=E5=9C=A8=E4=B9=9F=E4=BC=9A=E5=B0=86=20:=20?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E4=B8=BA=20-=E3=80=82=20=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E4=BA=86=20DOCX=20=E6=B5=8B=E8=AF=95=E7=9A=84=E7=A9=BA?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AD=A6=E5=91=8A=EF=BC=8C=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=203=20=E5=A4=84=20xUnit=20Assert.Single=20=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=99=A8=E8=AD=A6=E5=91=8A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Infrastructure/Files/DownloadFileName.cs | 8 ++++++-- .../CourseSelectionsControllerTests.cs | 10 ++++++---- .../GradeAnalysisWordReportGeneratorTests.cs | 18 ++++++++++++------ .../Jiaowu.Api.Tests/OfficialDocumentTests.cs | 5 +++-- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Jiaowu.Api/Infrastructure/Files/DownloadFileName.cs b/src/Jiaowu.Api/Infrastructure/Files/DownloadFileName.cs index ff65cf7..86cd563 100644 --- a/src/Jiaowu.Api/Infrastructure/Files/DownloadFileName.cs +++ b/src/Jiaowu.Api/Infrastructure/Files/DownloadFileName.cs @@ -5,6 +5,11 @@ namespace Jiaowu.Api.Infrastructure.Files; /// public static class DownloadFileName { + // Do not use Path.GetInvalidFileNameChars(): its result is platform-specific, + // while downloaded names must remain usable on Windows regardless of where + // the API is built or hosted. + private static readonly char[] WindowsInvalidFileNameChars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']; + public static string Export(string title, string extension, params string?[] parts) => Build(title, extension, includeTimestamp: true, parts); @@ -33,9 +38,8 @@ public static class DownloadFileName private static string Clean(string? value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; - var invalid = Path.GetInvalidFileNameChars(); var cleaned = new string(value.Trim().Select(character => - invalid.Contains(character) ? '-' : character).ToArray()); + WindowsInvalidFileNameChars.Contains(character) || char.IsControl(character) ? '-' : character).ToArray()); return cleaned.Trim('.', ' '); } diff --git a/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs b/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs index f15cbae..d01220e 100644 --- a/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs @@ -110,12 +110,14 @@ public sealed class CourseSelectionsControllerTests .OrderBy(x => x.UserId) .ToListAsync(); Assert.Equal(2, resultNotifications.Count); - var selectedNotification = Assert.Single(resultNotifications - .Where(x => x.UserId == data.EnrolledUserId)); + var selectedNotification = Assert.Single( + resultNotifications, + x => x.UserId == data.EnrolledUserId); Assert.Contains("最终选中 1 门", selectedNotification.Content); Assert.Contains("《程序设计基础》", selectedNotification.Content); - var waitlistNotification = Assert.Single(resultNotifications - .Where(x => x.UserId == data.FirstWaiterUserId)); + var waitlistNotification = Assert.Single( + resultNotifications, + x => x.UserId == data.FirstWaiterUserId); Assert.Contains("本轮未选中课程", waitlistNotification.Content); Assert.Contains("候补未成功 1 门", waitlistNotification.Content); Assert.Equal(NotificationCategory.CourseSelection, waitlistNotification.Category); diff --git a/tests/Jiaowu.Api.Tests/GradeAnalysisWordReportGeneratorTests.cs b/tests/Jiaowu.Api.Tests/GradeAnalysisWordReportGeneratorTests.cs index d619ad7..72ef19c 100644 --- a/tests/Jiaowu.Api.Tests/GradeAnalysisWordReportGeneratorTests.cs +++ b/tests/Jiaowu.Api.Tests/GradeAnalysisWordReportGeneratorTests.cs @@ -43,16 +43,19 @@ public sealed class GradeAnalysisWordReportGeneratorTests Assert.True(bytes.Length > 10_000); using var stream = new MemoryStream(bytes); using var document = WordprocessingDocument.Open(stream, false); - var text = document.MainDocumentPart!.Document.InnerText; + var mainDocumentPart = document.MainDocumentPart + ?? throw new InvalidOperationException("Generated document has no main document part."); + var text = mainDocumentPart.Document?.InnerText + ?? throw new InvalidOperationException("Generated document has no document body."); Assert.Contains("成绩分析报告", text); Assert.Contains("同课程教学班对比", text); Assert.Contains("各范围基准", text); Assert.Contains("历年成绩趋势", text); Assert.Contains("行政班组成", text); Assert.Contains("计科一班", text); - Assert.Equal(3, document.MainDocumentPart.ImageParts.Count()); - Assert.Equal(2, document.MainDocumentPart.HeaderParts.Count()); - Assert.Equal(2, document.MainDocumentPart.FooterParts.Count()); + Assert.Equal(3, mainDocumentPart.ImageParts.Count()); + Assert.Equal(2, mainDocumentPart.HeaderParts.Count()); + Assert.Equal(2, mainDocumentPart.FooterParts.Count()); } [Fact] @@ -76,10 +79,13 @@ public sealed class GradeAnalysisWordReportGeneratorTests Assert.True(bytes.Length > 8_000); using var stream = new MemoryStream(bytes); using var document = WordprocessingDocument.Open(stream, false); - var text = document.MainDocumentPart!.Document.InnerText; + var mainDocumentPart = document.MainDocumentPart + ?? throw new InvalidOperationException("Generated document has no main document part."); + var text = mainDocumentPart.Document?.InnerText + ?? throw new InvalidOperationException("Generated document has no document body."); Assert.Contains("科目总体成绩分析报告", text); Assert.Contains("学生来源学院对比", text); Assert.Contains("教学班横向比较", text); - Assert.Single(document.MainDocumentPart.ImageParts); + Assert.Single(mainDocumentPart.ImageParts); } } diff --git a/tests/Jiaowu.Api.Tests/OfficialDocumentTests.cs b/tests/Jiaowu.Api.Tests/OfficialDocumentTests.cs index 67ef210..dbd4af9 100644 --- a/tests/Jiaowu.Api.Tests/OfficialDocumentTests.cs +++ b/tests/Jiaowu.Api.Tests/OfficialDocumentTests.cs @@ -28,8 +28,9 @@ public sealed class OfficialDocumentTests string parameterName) { var constructor = Assert.Single(requestType.GetConstructors()); - var parameter = Assert.Single(constructor.GetParameters() - .Where(x => x.Name == parameterName)); + var parameter = Assert.Single( + constructor.GetParameters(), + x => x.Name == parameterName); Assert.NotEmpty(parameter.GetCustomAttributes(typeof(ValidationAttribute), false)); var property = requestType.GetProperty(parameterName); From b7b7c25d786715e3bcf8ff51f844df27e8bd4db2 Mon Sep 17 00:00:00 2001 From: biss Date: Sat, 22 Aug 2026 18:57:43 +0800 Subject: [PATCH 2/2] v2.5.0 patch