Kaynağa Gözat

fix(进件): Task 6 修复轮 1 - 连续点规则补独立钉子 + 拒绝文案按理由分别陈述

brief 要求「含 /、反斜杠、.. ⇒ 400」,但它给的两条输入都隔离不了 `..` 那一支:
../../etc/passwd.jpg 同时含 / 与 ..(分隔符那条先命中),a\b.jpg 根本不含 ..
⇒ 删掉 `..` 条件,原有 13 条用例全绿。本提交把它钉住。

新增 uploadImage_fileNameWithConsecutiveDots_rejected(输入 a..jpg:只含连续两个点、
不含任何分隔符),走 assertUploadRejected400 + verifyNoInteractions(client)。
它是**特征化钉子**(行为已存在),鉴别力由变异实测,不靠断言:
  删掉 `..` 那一支        ⇒ 本用例红(expected: <400> but was: <502>)
  只改文案措辞(逻辑不动)⇒ 仍绿
两向读数与三条编译证据见 task-6-fix1-report.md。

拒绝文案按理由拆开陈述:`..` 不是路径分隔符,对 a..jpg 说「不能含路径分隔符」是假话。
拆成两条判断(路径分隔符 / 连续两个点),**接受与拒绝的行为跟合写成一条时完全相同**,
只有「只含 .. 且不含分隔符」这类输入的文案变了。
requireUploadable 的 javadoc 第 5 项补上「这是**有意过宽**的近似 + 为什么宁可过宽」,
免得下一个人看到 contains("..") 以为作者没想到 a..jpg。

文案仍**不**被逐字钉住(助手只断言 400 / 文案非空 / 不含上游名号)—— 有意如此:
钉死会让以后每次改标点都要动测试,绿向变异就是为这条跑的。

测试:服务类 45 -> 46,全量见收尾绿跑。
alphaH 3 hafta önce
ebeveyn
işleme
5d2958faf7

+ 15 - 2
java/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingService.java

@@ -409,7 +409,14 @@ public class LsyOnboardingService {
      *       <b>我们的护栏</b>,上游文档没提。</li>
      *   <li><b>文件名不含 {@code /}、{@code \}、{@code ..}</b>。是**拒绝**、不是**清洗后照发** ——
      *       清洗会让日志里的名字与实际发出去的名字不一致:排障时看到的名字不是上游收到的那个,
-     *       那比直接拒更坏。<b>我们的护栏</b>。</li>
+     *       那比直接拒更坏。<b>我们的护栏</b>。
+     *       ★ {@code ..} 这一支是**有意过宽**的近似,不是没想到 {@code a..jpg}:
+     *       {@code /} 与 {@code \} 已被前一条拒掉,剩下的穿越 token 只有裸 {@code ..},
+     *       而上游拿到 {@code ..} 会怎么处理**我们未实测**。过宽的代价 = 「文件名里合法地含
+     *       连续两个点」被拒(用户看到一句中文 400,改个名即可);过窄的代价 = 一条我们
+     *       **无据可判**的路径风险 ⇒ 方向取「宁可误拒」。
+     *       两条判断**分开写**,是为了让每条文案**对它自己的触发输入都为真** ——
+     *       对 {@code a..jpg} 说「文件名不能含路径分隔符」是**假话**。</li>
      *   <li><b>扩展名在白名单内</b>(比较前先转小写,故 {@code a.JPG} 通过):
      *       {@link #ALLOWED_IMAGE_EXTENSIONS}。没有后缀({@code lastIndexOf} 为 -1)⇒ 空串
      *       ⇒ 不在白名单。<b>我们的收窄</b>。</li>
@@ -434,9 +441,15 @@ public class LsyOnboardingService {
                 throw new BusinessException(400, "图片文件名不能含控制字符");
             }
         }
-        if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) {
+        if (fileName.contains("/") || fileName.contains("\\")) {
             throw new BusinessException(400, "图片文件名不能含路径分隔符");
         }
+        // ★ 与上一条分开写,是为了让**每条文案对它自己的触发输入都为真**:
+        // 对 `a..jpg` 说「文件名不能含路径分隔符」是**假话** —— `..` 不是分隔符。
+        // 两条合写成一条判断时行为完全相同,分开只是为了文案可陈述为真。
+        if (fileName.contains("..")) {
+            throw new BusinessException(400, "图片文件名不能含连续两个点(..)");
+        }
         int dot = fileName.lastIndexOf('.');
         String ext = dot < 0 ? "" : fileName.substring(dot + 1);
         // Locale.ROOT:不跟随默认 locale。本白名单四个扩展名都不含字母 i,默认 locale 的

+ 27 - 1
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingServiceTest.java

@@ -1052,10 +1052,15 @@ class LsyOnboardingServiceTest {
     }
 
     /**
-     * 文件名含路径分隔符 / {@code ..} ⇒ 400。
+     * 文件名含路径分隔符({@code /} 或 {@code \})⇒ 400。
      *
      * <p>是**拒绝**、不是**清洗后照发**:清洗会让日志里的名字与实际发出去的名字不一致 ——
      * 排障时看到的名字不是上游收到的那个,那比直接拒更坏。
+     *
+     * <p>★ 本用例的两个输入一个**同时**含 {@code ..}({@code ../../etc/passwd.jpg})、
+     * 一个**不含**({@code a\b.jpg}),所以它**证明不了** {@code ..} 那一支存在 ——
+     * 分隔符那条排在前面,先命中。{@code ..} 那一支由
+     * {@link #uploadImage_fileNameWithConsecutiveDots_rejected} 单独钉住。
      */
     @Test
     void uploadImage_fileNameWithPathSeparator_rejected() {
@@ -1065,6 +1070,27 @@ class LsyOnboardingServiceTest {
         verifyNoInteractions(client);
     }
 
+    /**
+     * 文件名含**连续两个点**({@code ..})但**不含任何分隔符** ⇒ 400,且没有出海。
+     *
+     * <p>★ 为什么必须**独立**成一条:brief 给的两条输入都隔离不了这一支 ——
+     * {@code ../../etc/passwd.jpg} 同时含 {@code /} 与 {@code ..}(分隔符那条先命中),
+     * {@code a\b.jpg} 根本不含 {@code ..}。实测:删掉 {@code ..} 那一支,原有 13 条用例**全绿**。
+     * 输入 {@code a..jpg} 只含 {@code ..},是这一支唯一的钉子。
+     *
+     * <p>★ {@code contains("..")} 是**有意过宽**的近似(理由见
+     * {@code LsyOnboardingService#requireUploadable} 的 javadoc 第 5 项)。本用例钉的是
+     * 「这一支存在、且**在出站之前**拒」,**不是**「{@code a..jpg} 语义上该拒」。
+     *
+     * <p>★ 文案**没有**被逐字钉住({@code assertUploadRejected400} 只断言 400 / 非空 /
+     * 不含上游名号)—— 这是**有意**的:钉死文案会让以后每次改标点都要动测试。
+     */
+    @Test
+    void uploadImage_fileNameWithConsecutiveDots_rejected() {
+        assertUploadRejected400(IMG_BYTES, "a..jpg", "文件名含连续两个点: a..jpg");
+        verifyNoInteractions(client);
+    }
+
     /**
      * 文件名 {@code null} / 空串 / 全空白 ⇒ 400,且没有出海。
      *