diff --git a/src/test/java/com/babelqueue/redis/PoisonRecordTest.java b/src/test/java/com/babelqueue/redis/PoisonRecordTest.java index fb3bf0e..bbf7825 100644 --- a/src/test/java/com/babelqueue/redis/PoisonRecordTest.java +++ b/src/test/java/com/babelqueue/redis/PoisonRecordTest.java @@ -53,4 +53,37 @@ void rejectsADeadLetterThatIsNotASingleJsonObject() { assertThrows(IllegalArgumentException.class, () -> PoisonRecord.build(new byte[] {'{', '}'}, bad), String.valueOf(bad)); } } + + @Test + void acceptsEveryStrictJsonShapeTheScannerKnows() { + String[] valid = { + " {\t\"a\" :\r\n 1 } ", + "{\"a\":\"\\\"\\\\\\/\\b\\f\\n\\r\\t\\u0041\\u00E9\\ud83d\\ude00 plain\"}", + "{\"a\":[true,false,null,-0,0,0.5,1E+2,-12.5e-3,2e5,[],{},\"\"]}", + "{\"a\":{\"b\":{\"c\":[1,[2,[3]]]}},\"d\":[{\"e\":1},{\"f\":[]}]}", + "{\"a\":{\"dead_letter\":1},\"b\":[{\"dead_letter\":2}]}", + "{\"a\":" + "[".repeat(100) + "]".repeat(100) + "}", + }; + for (String body : valid) { + assertTrue(PoisonRecord.isSpliceableObject(body), body); + } + } + + @Test + void rejectsEveryNonStrictOrUnspliceableObject() { + String[] invalid = { + "", " ", "{", "{\"a\"", "{\"a\":", "{\"a\":1", "{\"a\":1,", "{\"a\":1,}", "{a:1}", "{\"a\" 1}", + "{\"a\":1 \"b\":2}", "{\"a\":[1,]}", "{\"a\":[1}", "{\"a\":{\"b\":1]}", "{\"a\":[1,2}", + "{\"a\":tru}", "{\"a\":nul}", "{\"a\":fals}", "{\"a\":NaN}", "{\"a\":Infinity}", + "{\"a\":-}", "{\"a\":-", "{\"a\":01}", "{\"a\":1.}", "{\"a\":1e}", "{\"a\":1e+}", "{\"a\":+1}", "{\"a\":.5}", + "{\"a\":\"x}", "{\"a\":\"\\x\"}", "{\"a\":\"\\", "{\"a\":\"\\u12\"}", "{\"a\":\"\\u12G4\"}", + "{\"a\":\"\\u\uff10\uff10\uff14\uff11\"}", + "{\"a\":\"\\ud800\"}", "{\"a\":\"\\ud800x\"}", "{\"a\":\"\\ud800\\u0041\"}", "{\"a\":\"\\udc00\"}", + "{\"a\":\"line\nbreak\"}", "{\"a\":1}x", "{\"a\":1}{}", "[]", "1", + "{\"dead_letter\":1}", "{\"x\":1,\"dead_letter\":{}}", "{\"dead\\u005fletter\":1}", + }; + for (String body : invalid) { + assertFalse(PoisonRecord.isSpliceableObject(body), body); + } + } } diff --git a/src/test/java/com/babelqueue/redis/RedisLayoutConformanceTest.java b/src/test/java/com/babelqueue/redis/RedisLayoutConformanceTest.java index 5de5b98..524bc92 100644 --- a/src/test/java/com/babelqueue/redis/RedisLayoutConformanceTest.java +++ b/src/test/java/com/babelqueue/redis/RedisLayoutConformanceTest.java @@ -431,7 +431,11 @@ void legacyAbandonedReservationIsRecoveredByteForByte() throws Exception { RedisQueueOps dying = new RedisQueueOps(redis, queue, RedisLayout.LEGACY, null, 1L, -1, RedisQueueOpsClock.WALL); RedisQueueOps noDeadline = new RedisQueueOps(redis, stuck, RedisLayout.LEGACY, null, null, -1, RedisQueueOpsClock.WALL); try { - String raw = "{\"job\": \"" + URN + "\", \"attempts\": 2, \"data\": {\"id\": 1}}"; + // A conformant envelope (the consumer rejects one without trace_id / meta as poison) with + // deliberately non-canonical spacing, so a re-encode would be caught by the byte comparison. + String raw = "{\"job\": \"" + URN + "\", \"trace_id\": \"t-1\", \"attempts\": 2, \"data\": {\"id\": 1}," + + " \"meta\": {\"id\": \"f1e2d3c4-b5a6-4789-90ab-cdef01234567\", \"queue\": \"orders\"," + + " \"lang\": \"php\", \"schema_version\": 1, \"created_at\": 1749132727000}}"; redis.rpush(queue, raw); redis.rpush(stuck, raw); assertEquals(raw, dying.reserve().body());