@@ -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\n chars" , & [ 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\n chars"
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