assertSame('hello', JsonCanonicalizer::canonicalize('hello')); } /** * @return iterable */ public static function scalarProvider(): iterable { yield 'int' => [42]; yield 'float' => [3.14]; yield 'bool true' => [true]; yield 'bool false' => [false]; yield 'null' => [null]; } /** * @dataProvider scalarProvider */ public function test_scalar_passthrough(int|float|bool|null $value): void { // assertSame across the provider keeps PHPStan from narrowing // both sides to a single literal type per call. $this->assertSame($value, JsonCanonicalizer::canonicalize($value)); } public function test_empty_array(): void { $this->assertSame([], JsonCanonicalizer::canonicalize([])); } public function test_numeric_list_preserves_order(): void { $list = ['c', 'a', 'b']; $this->assertSame(['c', 'a', 'b'], JsonCanonicalizer::canonicalize($list)); } public function test_associative_array_keys_sorted(): void { $assoc = ['c' => 1, 'a' => 2, 'b' => 3]; $result = JsonCanonicalizer::canonicalize($assoc); $this->assertSame(['a' => 2, 'b' => 3, 'c' => 1], $result); $this->assertSame(['a', 'b', 'c'], array_keys($result)); } public function test_nested_associative_sorted_recursively(): void { $input = [ 'z' => ['c' => 1, 'a' => 2], 'a' => 'first', ]; $result = JsonCanonicalizer::canonicalize($input); $this->assertSame(['a', 'z'], array_keys($result)); $this->assertSame(['a', 'c'], array_keys($result['z'])); } public function test_list_of_dicts_each_dict_sorted_list_order_preserved(): void { $input = [ ['c' => 1, 'a' => 2], ['z' => 9, 'a' => 8], ]; $result = JsonCanonicalizer::canonicalize($input); // List preserved $this->assertCount(2, $result); $this->assertSame(2, $result[0]['a']); $this->assertSame(8, $result[1]['a']); // Each dict sorted $this->assertSame(['a', 'c'], array_keys($result[0])); $this->assertSame(['a', 'z'], array_keys($result[1])); } public function test_encode_byte_identical_for_inputs_differing_only_in_key_order(): void { $a = ['name' => 'Alice', 'age' => 30, 'role' => 'admin']; $b = ['role' => 'admin', 'age' => 30, 'name' => 'Alice']; $this->assertSame( JsonCanonicalizer::encode($a), JsonCanonicalizer::encode($b), ); } public function test_encode_uses_unescaped_unicode_and_slashes(): void { $value = ['url' => 'https://example.com/path', 'name' => 'café']; $encoded = JsonCanonicalizer::encode($value); $this->assertStringContainsString('café', $encoded, 'unicode must be unescaped'); $this->assertStringNotContainsString('\\/', $encoded, 'slashes must be unescaped'); } public function test_mixed_key_array_treated_as_associative(): void { // Mixed keys: keys 0, 1 plus 'foo'. Not a list per array_is_list(); // ksort applies, putting numeric-string keys before alphabetic. $input = [0 => 'first', 'foo' => 'bar', 1 => 'second']; $result = JsonCanonicalizer::canonicalize($input); $this->assertSame([0, 1, 'foo'], array_keys($result)); } public function test_empty_dict_inside_canonical_structure(): void { $input = ['z' => [], 'a' => 'x']; $result = JsonCanonicalizer::canonicalize($input); $this->assertSame(['a', 'z'], array_keys($result)); $this->assertSame([], $result['z']); } }