Skip to content

Commit 4dee4bb

Browse files
committed
test: add comprehensive canonical JSON tests for RFC 8785 compliance
Covers key aspects of JCS (RFC 8785) that the digest depends on: - Bytewise-exact output for known inputs - Alphabetical key ordering - Short-form escapes (\b \f \n \r \t) vs \uXXXX for other control chars - Non-ASCII Unicode emitted verbatim (not \uXXXX escaped) - Lowercase hex payload - Empty string / empty payload edge cases - Idempotency across 100 invocations (guards against HashMap randomization) - No whitespace, valid JSON structure
1 parent d88d2c6 commit 4dee4bb

1 file changed

Lines changed: 84 additions & 4 deletions

File tree

cc-eventlog/src/runtime_events.rs

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,94 @@ mod tests {
255255
#[test]
256256
fn canonical_json_escapes_special_chars() {
257257
let canonical = canonical_event_json_v2("event\"with\\special\nchars", &[0xff]);
258-
// Verify it's valid JSON
259-
let parsed: serde_json::Value = serde_json::from_str(&canonical).unwrap();
258+
// Exact bytewise output — JCS must be deterministic
260259
assert_eq!(
261-
parsed["event"].as_str().unwrap(),
262-
"event\"with\\special\nchars"
260+
canonical,
261+
r#"{"event":"event\"with\\special\nchars","event_type":134217729,"payload":"ff","version":2}"#
262+
);
263+
}
264+
265+
#[test]
266+
fn canonical_json_keys_are_sorted_alphabetically() {
267+
// JCS RFC 8785 requires keys sorted by UTF-16 code unit order.
268+
// For ASCII keys, this is alphabetical.
269+
let canonical = canonical_event_json_v2("test", &[0x01]);
270+
let event_pos = canonical.find(r#""event":"#).unwrap();
271+
let event_type_pos = canonical.find(r#""event_type":"#).unwrap();
272+
let payload_pos = canonical.find(r#""payload":"#).unwrap();
273+
let version_pos = canonical.find(r#""version":"#).unwrap();
274+
assert!(event_pos < event_type_pos);
275+
assert!(event_type_pos < payload_pos);
276+
assert!(payload_pos < version_pos);
277+
}
278+
279+
#[test]
280+
fn canonical_json_empty_event_and_payload() {
281+
let canonical = canonical_event_json_v2("", &[]);
282+
assert_eq!(
283+
canonical,
284+
r#"{"event":"","event_type":134217729,"payload":"","version":2}"#
285+
);
286+
}
287+
288+
#[test]
289+
fn canonical_json_idempotent() {
290+
// Same input must always produce bytewise-identical output.
291+
// HashMap randomization internally shouldn't affect output.
292+
let reference = canonical_event_json_v2("compose-hash", &[0xde, 0xad, 0xbe, 0xef]);
293+
for _ in 0..100 {
294+
assert_eq!(
295+
canonical_event_json_v2("compose-hash", &[0xde, 0xad, 0xbe, 0xef]),
296+
reference
297+
);
298+
}
299+
}
300+
301+
#[test]
302+
fn canonical_json_non_ascii_unicode() {
303+
// JCS requires UTF-8 output; non-ASCII characters that don't need
304+
// escaping (i.e., not control chars, not " or \) must be emitted as-is.
305+
let canonical = canonical_event_json_v2("测试-emoji-🦀", &[]);
306+
// Event name should appear verbatim in the JSON (no \uXXXX escaping)
307+
assert!(canonical.contains("测试-emoji-🦀"), "got: {canonical}");
308+
// Must still be parseable and roundtrip
309+
let parsed: serde_json::Value = serde_json::from_str(&canonical).unwrap();
310+
assert_eq!(parsed["event"].as_str().unwrap(), "测试-emoji-🦀");
311+
}
312+
313+
#[test]
314+
fn canonical_json_control_character_escaping() {
315+
// JCS (via RFC 8259) uses short escapes for \b \f \n \r \t and \uXXXX for other controls.
316+
let canonical = canonical_event_json_v2("\x08\x0c\n\r\t\x01", &[]);
317+
assert!(
318+
canonical.contains(r#""event":"\b\f\n\r\t\u0001""#),
319+
"got: {canonical}"
263320
);
264321
}
265322

323+
#[test]
324+
fn canonical_json_payload_lowercase_hex() {
325+
// Payload must be hex-encoded lowercase for determinism.
326+
let canonical = canonical_event_json_v2("test", &[0xAB, 0xCD, 0xEF]);
327+
assert!(
328+
canonical.contains(r#""payload":"abcdef""#),
329+
"got: {canonical}"
330+
);
331+
}
332+
333+
#[test]
334+
fn canonical_json_is_valid_rfc8785_structure() {
335+
// No whitespace, no trailing commas, proper JSON
336+
let canonical = canonical_event_json_v2("x", &[0xff]);
337+
assert!(!canonical.contains(' '));
338+
assert!(!canonical.contains('\n'));
339+
assert!(!canonical.contains('\t'));
340+
assert!(canonical.starts_with('{'));
341+
assert!(canonical.ends_with('}'));
342+
// Must parse back
343+
let _: serde_json::Value = serde_json::from_str(&canonical).unwrap();
344+
}
345+
266346
#[test]
267347
fn mixed_v1_v2_replay() {
268348
let events = vec![

0 commit comments

Comments
 (0)