From 970936fca2848174ff9ee344bb3429b07d5d1785 Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 17:38:22 +0700 Subject: [PATCH 01/10] Fix for https://github.com/brefphp/laravel-bridge/issues/189 --- src/Event/Http/Psr7Bridge.php | 64 ++++++++++++++++++- tests/Event/Http/CommonHttpTest.php | 35 ++++++++++ ...body-form-multipart-structured-arrays.json | 53 +++++++++++++++ ...body-form-multipart-structured-arrays.json | 41 ++++++++++++ 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json create mode 100644 tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php index 1745fdd86..b0344cd97 100644 --- a/src/Event/Http/Psr7Bridge.php +++ b/src/Event/Http/Psr7Bridge.php @@ -146,8 +146,68 @@ private static function parseKeyAndInsertValueInArray(array &$array, string $key parse_str(urlencode($key) . '=mock', $parsed); // Replace `mock` with the actual value array_walk_recursive($parsed, fn (&$v) => $v = $value); - // Merge recursively into the main array to avoid overwriting existing values - $array = array_merge_recursive($array, $parsed); + + // Use a custom merge that handles both structured arrays and regular arrays + $array = self::mergeRecursivePreserveNumeric($array, $parsed); + } + + private static function mergeRecursivePreserveNumeric(array $a, array $b): array + { + foreach ($b as $key => $bVal) { + if (!array_key_exists($key, $a)) { + $a[$key] = $bVal; + continue; + } + + $aVal = $a[$key]; + + if (is_array($aVal) && is_array($bVal)) { + $aIsList = array_is_list($aVal); + $bIsList = array_is_list($bVal); + + if ($aIsList && $bIsList) { + // Determine whether list items are arrays (objects) -> merge-by-index + $mergeByIndex = false; + foreach ($aVal as $item) { if (is_array($item)) { $mergeByIndex = true; break; } } + if (!$mergeByIndex) { + foreach ($bVal as $item) { if (is_array($item)) { $mergeByIndex = true; break; } } + } + + if ($mergeByIndex) { + $max = max(count($aVal), count($bVal)); + $merged = []; + for ($i = 0; $i < $max; $i++) { + $hasA = array_key_exists($i, $aVal); + $hasB = array_key_exists($i, $bVal); + if ($hasA && $hasB) { + if (is_array($aVal[$i]) && is_array($bVal[$i])) { + $merged[$i] = self::mergeRecursivePreserveNumeric($aVal[$i], $bVal[$i]); + } else { + // if one is scalar, b wins + $merged[$i] = $bVal[$i]; + } + } elseif ($hasA) { + $merged[$i] = $aVal[$i]; + } else { + $merged[$i] = $bVal[$i]; + } + } + $a[$key] = $merged; + } else { + // both lists of scalars -> append + $a[$key] = array_merge($aVal, $bVal); + } + } else { + // At least one side is associative -> merge recursively by key + $a[$key] = self::mergeRecursivePreserveNumeric($aVal, $bVal); + } + } else { + // Non-array or conflicting types -> b wins + $a[$key] = $bVal; + } + } + + return $a; } /** diff --git a/tests/Event/Http/CommonHttpTest.php b/tests/Event/Http/CommonHttpTest.php index 1c4805901..bb9f393d9 100644 --- a/tests/Event/Http/CommonHttpTest.php +++ b/tests/Event/Http/CommonHttpTest.php @@ -420,6 +420,41 @@ public function test POST request with multipart file uploads(int $version ); } + /** + * @dataProvider provide API Gateway versions + */ + public function test POST request with multipart form data containing structured arrays(int $version) + { + var_dump($version); + $this->fromFixture(__DIR__ . "/Fixture/ag-v$version-body-form-multipart-structured-arrays.json"); + + $this->assertContentType('multipart/form-data; boundary=testBoundary'); + $this->assertMethod('POST'); + $this->assertParsedBody([ + 'content' => '

Test content

', + 'some_id' => '3034', + 'references' => [ + [ + 'other_id' => '4390954279', + 'url' => '', + ], + [ + 'other_id' => '4313323164', + 'url' => '', + ], + [ + 'other_id' => '', + 'url' => 'https://someurl.com/node/745911', + ], + ], + 'tags' => [ + 'public health', + 'public finance', + ], + '_method' => 'PATCH', + ]); + } + /** * @dataProvider provide API Gateway versions */ diff --git a/tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json b/tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json new file mode 100644 index 000000000..60158768e --- /dev/null +++ b/tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json @@ -0,0 +1,53 @@ +{ + "version": "1.0", + "resource": "/path", + "path": "/path", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Cache-Control": "no-cache", + "Content-Type": "multipart/form-data; boundary=testBoundary", + "Host": "example.org", + "User-Agent": "PostmanRuntime/7.20.1", + "X-Amzn-Trace-Id": "Root=1-ffffffff-ffffffffffffffffffffffff", + "X-Forwarded-For": "1.1.1.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "queryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "xxxxxx", + "resourcePath": "/path", + "httpMethod": "POST", + "extendedRequestId": "XXXXXX-xxxxxxxx=", + "requestTime": "24/Nov/2019:18:55:08 +0000", + "path": "/path", + "accountId": "123400000000", + "protocol": "HTTP/1.1", + "stage": "dev", + "domainPrefix": "dev", + "requestTimeEpoch": 1574621708700, + "requestId": "ffffffff-ffff-4fff-ffff-ffffffffffff", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "1.1.1.1", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "PostmanRuntime/7.20.1", + "user": null + }, + "domainName": "example.org", + "apiId": "xxxxxxxxxx" + }, + "body": "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n", + "isBase64Encoded": false +} diff --git a/tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json b/tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json new file mode 100644 index 000000000..6adf2e606 --- /dev/null +++ b/tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json @@ -0,0 +1,41 @@ +{ + "version": "2.0", + "routeKey": "ANY /path", + "rawPath": "/path", + "rawQueryString": "", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Cache-Control": "no-cache", + "Content-Type": "multipart/form-data; boundary=testBoundary", + "Host": "example.org", + "User-Agent": "PostmanRuntime/7.20.1", + "X-Amzn-Trace-Id": "Root=1-ffffffff-ffffffffffffffffffffffff", + "X-Forwarded-For": "1.1.1.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "queryStringParameters": null, + "stageVariables": null, + "requestContext": { + "accountId": "123400000000", + "apiId": "xxxxxxxxxx", + "domainName": "example.org", + "domainPrefix": "0000000000", + "http": { + "method": "POST", + "path": "/path", + "protocol": "HTTP/1.1", + "sourceIp": "1.1.1.1", + "userAgent": "PostmanRuntime/7.20.1" + }, + "requestId": "JTHoQgr2oAMEPMg=", + "routeId": "47matwk", + "routeKey": "ANY /path", + "stage": "$default", + "time": "24/Nov/2019:18:55:08 +0000", + "timeEpoch": 1574621708700 + }, + "body": "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n", + "isBase64Encoded": false +} From 08fc28b2f762d3e192327a3b04f6c01836ad9b51 Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 18:56:57 +0700 Subject: [PATCH 02/10] Fixed code style, php versions support --- .php-cs-fixer.cache | 1 + composer.json | 3 ++- src/Event/Http/Psr7Bridge.php | 21 ++++++++++++++++----- 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 .php-cs-fixer.cache diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 000000000..7811e5b0e --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.4.11","version":"3.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\/Event\/Http\/Psr7Bridge.php":"52cfdf8941ac1795e80ef89a9934ea28"}} \ No newline at end of file diff --git a/composer.json b/composer.json index 9b407da22..34b31d9de 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,8 @@ "phpstan/phpstan": "^1.10.26", "phpunit/phpunit": "^9.6.10", "symfony/console": "^4.4|^5.0|^6.0|^7.0", - "symfony/yaml": "^4.4|^5.0|^6.0|^7.0" + "symfony/yaml": "^4.4|^5.0|^6.0|^7.0", + "friendsofphp/php-cs-fixer": "^3.89" }, "scripts": { "test": [ diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php index b0344cd97..a257f09bc 100644 --- a/src/Event/Http/Psr7Bridge.php +++ b/src/Event/Http/Psr7Bridge.php @@ -12,6 +12,7 @@ use RuntimeException; use function str_starts_with; +use function array_is_list; /** * Bridges PSR-7 requests and responses with API Gateway or ALB event/response formats. @@ -146,7 +147,7 @@ private static function parseKeyAndInsertValueInArray(array &$array, string $key parse_str(urlencode($key) . '=mock', $parsed); // Replace `mock` with the actual value array_walk_recursive($parsed, fn (&$v) => $v = $value); - + // Use a custom merge that handles both structured arrays and regular arrays $array = self::mergeRecursivePreserveNumeric($array, $parsed); } @@ -154,7 +155,7 @@ private static function parseKeyAndInsertValueInArray(array &$array, string $key private static function mergeRecursivePreserveNumeric(array $a, array $b): array { foreach ($b as $key => $bVal) { - if (!array_key_exists($key, $a)) { + if (! array_key_exists($key, $a)) { $a[$key] = $bVal; continue; } @@ -168,9 +169,19 @@ private static function mergeRecursivePreserveNumeric(array $a, array $b): array if ($aIsList && $bIsList) { // Determine whether list items are arrays (objects) -> merge-by-index $mergeByIndex = false; - foreach ($aVal as $item) { if (is_array($item)) { $mergeByIndex = true; break; } } - if (!$mergeByIndex) { - foreach ($bVal as $item) { if (is_array($item)) { $mergeByIndex = true; break; } } + foreach ($aVal as $item) { + if (is_array($item)) { + $mergeByIndex = true; + break; + } + } + if (! $mergeByIndex) { + foreach ($bVal as $item) { + if (is_array($item)) { + $mergeByIndex = true; + break; + } + } } if ($mergeByIndex) { From 05cee47bb2ee5bbd03148c2d43a3e89c9ae4a091 Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 19:10:03 +0700 Subject: [PATCH 03/10] Remove phpcs fixer --- .php-cs-fixer.cache | 1 - composer.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 .php-cs-fixer.cache diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache deleted file mode 100644 index 7811e5b0e..000000000 --- a/.php-cs-fixer.cache +++ /dev/null @@ -1 +0,0 @@ -{"php":"8.4.11","version":"3.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\/Event\/Http\/Psr7Bridge.php":"52cfdf8941ac1795e80ef89a9934ea28"}} \ No newline at end of file diff --git a/composer.json b/composer.json index 34b31d9de..9b407da22 100644 --- a/composer.json +++ b/composer.json @@ -43,8 +43,7 @@ "phpstan/phpstan": "^1.10.26", "phpunit/phpunit": "^9.6.10", "symfony/console": "^4.4|^5.0|^6.0|^7.0", - "symfony/yaml": "^4.4|^5.0|^6.0|^7.0", - "friendsofphp/php-cs-fixer": "^3.89" + "symfony/yaml": "^4.4|^5.0|^6.0|^7.0" }, "scripts": { "test": [ From 66dafd0a5e690e3944830a11c3ff926447b0692a Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 19:22:42 +0700 Subject: [PATCH 04/10] Fixed code style --- src/Event/Http/Psr7Bridge.php | 2 +- tests/Event/Http/Psr7BridgeTest.php | 41 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php index a257f09bc..4935b693a 100644 --- a/src/Event/Http/Psr7Bridge.php +++ b/src/Event/Http/Psr7Bridge.php @@ -11,8 +11,8 @@ use Riverline\MultiPartParser\Part; use RuntimeException; -use function str_starts_with; use function array_is_list; +use function str_starts_with; /** * Bridges PSR-7 requests and responses with API Gateway or ALB event/response formats. diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index a6d0f921c..e4c954f3a 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -32,6 +32,47 @@ public function test I can create a response from a PSR7 response() ], $response->toApiGatewayFormat()); } + public function test I can convert a request from a PSR7 response form data type() + { + $event = new HttpRequestEvent(json_decode(file_get_contents("/Fixture/ag-v$version-body-form-multipart-structured-arrays.json"), true, 512, JSON_THROW_ON_ERROR)); + + $psr7Response = new Response(200, [ + 'Content-Type' => 'multipart/form-data; boundary=testBoundary', + ], "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n"); + + $response = Psr7Bridge::convertRequest($psr7Response); + self::assertSame([ + 'isBase64Encoded' => false, + 'statusCode' => 200, + 'headers' => [ + 'Content-Type' => 'multipart/form-data; boundary=testBoundary', + ], + 'body' => [ + 'content' => '

Test content

', + 'some_id' => '3034', + 'references' => [ + [ + 'other_id' => '4390954279', + 'url' => '', + ], + [ + 'other_id' => '4313323164', + 'url' => '', + ], + [ + 'other_id' => '', + 'url' => 'https://someurl.com/node/745911', + ], + ], + 'tags' => [ + 'public health', + 'public finance', + ], + '_method' => 'PATCH', + ], + ], $response->toApiGatewayFormat()); + } + protected function fromFixture(string $file): void { $event = new HttpRequestEvent(json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR)); From 8e53a9057db3536ffcf8efbf2c515106b951a665 Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 19:25:01 +0700 Subject: [PATCH 05/10] Revert incomplete test --- tests/Event/Http/Psr7BridgeTest.php | 41 ----------------------------- 1 file changed, 41 deletions(-) diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index e4c954f3a..a6d0f921c 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -32,47 +32,6 @@ public function test I can create a response from a PSR7 response() ], $response->toApiGatewayFormat()); } - public function test I can convert a request from a PSR7 response form data type() - { - $event = new HttpRequestEvent(json_decode(file_get_contents("/Fixture/ag-v$version-body-form-multipart-structured-arrays.json"), true, 512, JSON_THROW_ON_ERROR)); - - $psr7Response = new Response(200, [ - 'Content-Type' => 'multipart/form-data; boundary=testBoundary', - ], "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n"); - - $response = Psr7Bridge::convertRequest($psr7Response); - self::assertSame([ - 'isBase64Encoded' => false, - 'statusCode' => 200, - 'headers' => [ - 'Content-Type' => 'multipart/form-data; boundary=testBoundary', - ], - 'body' => [ - 'content' => '

Test content

', - 'some_id' => '3034', - 'references' => [ - [ - 'other_id' => '4390954279', - 'url' => '', - ], - [ - 'other_id' => '4313323164', - 'url' => '', - ], - [ - 'other_id' => '', - 'url' => 'https://someurl.com/node/745911', - ], - ], - 'tags' => [ - 'public health', - 'public finance', - ], - '_method' => 'PATCH', - ], - ], $response->toApiGatewayFormat()); - } - protected function fromFixture(string $file): void { $event = new HttpRequestEvent(json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR)); From 74478c24d9270d0681a885e36f067875be669247 Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 19:31:46 +0700 Subject: [PATCH 06/10] Added a test to Psr7BridgeTest --- tests/Event/Http/Psr7BridgeTest.php | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index a6d0f921c..19248d2a0 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -32,6 +32,41 @@ public function test I can create a response from a PSR7 response() ], $response->toApiGatewayFormat()); } + public function test I can convert a request from an event with body multipart data type() + { + $expectedBody = [ + 'content' => '

Test content

', + 'some_id' => '3034', + 'references' => [ + [ + 'other_id' => '4390954279', + 'url' => '', + ], + [ + 'other_id' => '4313323164', + 'url' => '', + ], + [ + 'other_id' => '', + 'url' => 'https://someurl.com/node/745911', + ], + ], + 'tags' => [ + 'public health', + 'public finance', + ], + '_method' => 'PATCH', + ]; + + $eventv1 = new HttpRequestEvent(json_decode(file_get_contents(__DIR__ . '/Fixture/ag-v1-body-form-multipart-structured-arrays.json'), true, 512, JSON_THROW_ON_ERROR)); + $requestv1 = Psr7Bridge::convertRequest($eventv1, Context::fake()); + $this->assertEquals($expectedBody, $requestv1->getParsedBody()); + + $eventv2 = new HttpRequestEvent(json_decode(file_get_contents(__DIR__ . '/Fixture/ag-v2-body-form-multipart-structured-arrays.json'), true, 512, JSON_THROW_ON_ERROR)); + $requestv2 = Psr7Bridge::convertRequest($eventv2, Context::fake()); + $this->assertEquals($expectedBody, $requestv2->getParsedBody()); + } + protected function fromFixture(string $file): void { $event = new HttpRequestEvent(json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR)); From 167608c5d3d3270c9a86ab941b197f64db0ccdcf Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 19:43:07 +0700 Subject: [PATCH 07/10] Inlined test data --- tests/Event/Http/Psr7BridgeTest.php | 100 +++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index 19248d2a0..c7aa8b45f 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -34,6 +34,102 @@ public function test I can create a response from a PSR7 response() public function test I can convert a request from an event with body multipart data type() { + $datav1 = [ + 'version' => '1.0', + 'resource' => '/path', + 'path' => '/path', + 'httpMethod' => 'POST', + 'headers' => [ + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip, deflate', + 'Cache-Control' => 'no-cache', + 'Content-Type' => 'multipart/form-data; boundary=testBoundary', + 'Host' => 'example.org', + 'User-Agent' => 'PostmanRuntime/7.20.1', + 'X-Amzn-Trace-Id' => 'Root=1-ffffffff-ffffffffffffffffffffffff', + 'X-Forwarded-For' => '1.1.1.1', + 'X-Forwarded-Port' => '443', + 'X-Forwarded-Proto' => 'https', + ], + 'queryStringParameters' => null, + 'pathParameters' => null, + 'stageVariables' => null, + 'requestContext' => [ + 'resourceId' => 'xxxxxx', + 'resourcePath' => '/path', + 'httpMethod' => 'POST', + 'extendedRequestId' => 'XXXXXX-xxxxxxxx=', + 'requestTime' => '24/Nov/2019:18:55:08 +0000', + 'path' => '/path', + 'accountId' => '123400000000', + 'protocol' => 'HTTP/1.1', + 'stage' => 'dev', + 'domainPrefix' => 'dev', + 'requestTimeEpoch' => 1574621708700, + 'requestId' => 'ffffffff-ffff-4fff-ffff-ffffffffffff', + 'identity' => [ + 'cognitoIdentityPoolId' => null, + 'accountId' => null, + 'cognitoIdentityId' => null, + 'caller' => null, + 'sourceIp' => '1.1.1.1', + 'principalOrgId' => null, + 'accessKey' => null, + 'cognitoAuthenticationType' => null, + 'cognitoAuthenticationProvider' => null, + 'userArn' => null, + 'userAgent' => 'PostmanRuntime/7.20.1', + 'user' => null, + ], + 'domainName' => 'example.org', + 'apiId' => 'xxxxxxxxxx', + ], + 'body' => "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n", + 'isBase64Encoded' => false, + ]; + + $datav2 = [ + 'version' => '2.0', + 'routeKey' => 'ANY /path', + 'rawPath' => '/path', + 'rawQueryString' => '', + 'headers' => [ + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip, deflate', + 'Cache-Control' => 'no-cache', + 'Content-Type' => 'multipart/form-data; boundary=testBoundary', + 'Host' => 'example.org', + 'User-Agent' => 'PostmanRuntime/7.20.1', + 'X-Amzn-Trace-Id' => 'Root=1-ffffffff-ffffffffffffffffffffffff', + 'X-Forwarded-For' => '1.1.1.1', + 'X-Forwarded-Port' => '443', + 'X-Forwarded-Proto' => 'https', + ], + 'queryStringParameters' => null, + 'stageVariables' => null, + 'requestContext' => [ + 'accountId' => '123400000000', + 'apiId' => 'xxxxxxxxxx', + 'domainName' => 'example.org', + 'domainPrefix' => '0000000000', + 'http' => [ + 'method' => 'POST', + 'path' => '/path', + 'protocol' => 'HTTP/1.1', + 'sourceIp' => '1.1.1.1', + 'userAgent' => 'PostmanRuntime/7.20.1', + ], + 'requestId' => 'JTHoQgr2oAMEPMg=', + 'routeId' => '47matwk', + 'routeKey' => 'ANY /path', + 'stage' => '$default', + 'time' => '24/Nov/2019:18:55:08 +0000', + 'timeEpoch' => 1574621708700, + ], + 'body' => "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n", + 'isBase64Encoded' => false, + ]; + $expectedBody = [ 'content' => '

Test content

', 'some_id' => '3034', @@ -58,11 +154,11 @@ public function test I can convert a request from an event with body  '_method' => 'PATCH', ]; - $eventv1 = new HttpRequestEvent(json_decode(file_get_contents(__DIR__ . '/Fixture/ag-v1-body-form-multipart-structured-arrays.json'), true, 512, JSON_THROW_ON_ERROR)); + $eventv1 = new HttpRequestEvent($datav1); $requestv1 = Psr7Bridge::convertRequest($eventv1, Context::fake()); $this->assertEquals($expectedBody, $requestv1->getParsedBody()); - $eventv2 = new HttpRequestEvent(json_decode(file_get_contents(__DIR__ . '/Fixture/ag-v2-body-form-multipart-structured-arrays.json'), true, 512, JSON_THROW_ON_ERROR)); + $eventv2 = new HttpRequestEvent($datav2); $requestv2 = Psr7Bridge::convertRequest($eventv2, Context::fake()); $this->assertEquals($expectedBody, $requestv2->getParsedBody()); } From 88518a71412cd01f4bc9eb21a881f40c16d05879 Mon Sep 17 00:00:00 2001 From: Vedmant Date: Tue, 21 Oct 2025 20:11:33 +0700 Subject: [PATCH 08/10] Test Psr7Bridge::mergeRecursivePreserveNumeric --- tests/Event/Http/Psr7BridgeMergeTest.php | 440 +++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 tests/Event/Http/Psr7BridgeMergeTest.php diff --git a/tests/Event/Http/Psr7BridgeMergeTest.php b/tests/Event/Http/Psr7BridgeMergeTest.php new file mode 100644 index 000000000..8dbf301c0 --- /dev/null +++ b/tests/Event/Http/Psr7BridgeMergeTest.php @@ -0,0 +1,440 @@ +getMethod('mergeRecursivePreserveNumeric'); + $method->setAccessible(true); + return $method; + } + + public function testMergeRecursivePreserveNumericBasicExamples() + { + $method = $this->getMergeMethod(); + + // Test case 1: References with numeric keys + $a1 = [ + 'references' => [ + 0 => ['some_id' => '4390954279', 'url' => ''], + 1 => ['some_id' => '4313323164'], + ], + ]; + + $b1 = [ + 'references' => [ + 1 => ['url' => ''], + ], + ]; + + $expected1 = [ + 'references' => [ + 0 => ['some_id' => '4390954279', 'url' => ''], + 1 => ['some_id' => '4313323164', 'url' => ''], + ], + ]; + + $result1 = $method->invoke(null, $a1, $b1); + $this->assertEquals($expected1, $result1); + + // Test case 2: Delete categories with scalar arrays + $a2 = [ + 'delete' => [ + 'categories' => ['123'], + ], + ]; + + $b2 = [ + 'delete' => [ + 'categories' => ['456'], + ], + ]; + + $expected2 = [ + 'delete' => [ + 'categories' => ['123', '456'], + ], + ]; + + $result2 = $method->invoke(null, $a2, $b2); + $this->assertEquals($expected2, $result2); + } + + public function testMergeWithEmptyArrays() + { + $method = $this->getMergeMethod(); + + // Test merging with empty array + $a = ['key' => ['value1']]; + $b = ['key' => []]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['key' => ['value1']], $result); + + // Test merging empty array with non-empty + $a = ['key' => []]; + $b = ['key' => ['value1']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['key' => ['value1']], $result); + } + + public function testMergeWithScalarValues() + { + $method = $this->getMergeMethod(); + + // Test scalar values override arrays + $a = ['key' => ['nested' => 'value']]; + $b = ['key' => 'scalar']; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['key' => 'scalar'], $result); + + // Test arrays override scalars + $a = ['key' => 'scalar']; + $b = ['key' => ['nested' => 'value']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['key' => ['nested' => 'value']], $result); + } + + public function testMergeWithMixedArrayTypes() + { + $method = $this->getMergeMethod(); + + // Test associative array with list + $a = ['data' => ['key1' => 'value1']]; + $b = ['data' => ['value2', 'value3']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['data' => ['key1' => 'value1', 0 => 'value2', 1 => 'value3']], $result); + + // Test list with associative array + $a = ['data' => ['value1', 'value2']]; + $b = ['data' => ['key1' => 'value3']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['data' => [0 => 'value1', 1 => 'value2', 'key1' => 'value3']], $result); + } + + public function testMergeWithNestedObjects() + { + $method = $this->getMergeMethod(); + + // Test nested object merging + $a = [ + 'users' => [ + 0 => ['id' => 1, 'name' => 'John'], + 1 => ['id' => 2, 'name' => 'Jane'], + ], + ]; + + $b = [ + 'users' => [ + 0 => ['email' => 'john@example.com'], + 1 => ['email' => 'jane@example.com', 'age' => 25], + ], + ]; + + $expected = [ + 'users' => [ + 0 => ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'], + 1 => ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com', 'age' => 25], + ], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + } + + public function testMergeWithDifferentArraySizes() + { + $method = $this->getMergeMethod(); + + // Test when array A is larger + $a = [ + 'items' => [ + 0 => ['id' => 1], + 1 => ['id' => 2], + 2 => ['id' => 3], + ], + ]; + + $b = [ + 'items' => [ + 0 => ['name' => 'Item 1'], + 1 => ['name' => 'Item 2'], + ], + ]; + + $expected = [ + 'items' => [ + 0 => ['id' => 1, 'name' => 'Item 1'], + 1 => ['id' => 2, 'name' => 'Item 2'], + 2 => ['id' => 3], + ], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + + // Test when array B is larger + $a = [ + 'items' => [ + 0 => ['id' => 1], + ], + ]; + + $b = [ + 'items' => [ + 0 => ['name' => 'Item 1'], + 1 => ['id' => 2, 'name' => 'Item 2'], + 2 => ['id' => 3, 'name' => 'Item 3'], + ], + ]; + + $expected = [ + 'items' => [ + 0 => ['id' => 1, 'name' => 'Item 1'], + 1 => ['id' => 2, 'name' => 'Item 2'], + 2 => ['id' => 3, 'name' => 'Item 3'], + ], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + } + + public function testMergeWithScalarArrays() + { + $method = $this->getMergeMethod(); + + // Test scalar arrays (lists) are appended + $a = ['tags' => ['php', 'testing']]; + $b = ['tags' => ['unit', 'integration']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['tags' => ['php', 'testing', 'unit', 'integration']], $result); + + // Test with empty scalar arrays + $a = ['tags' => []]; + $b = ['tags' => ['new', 'tags']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['tags' => ['new', 'tags']], $result); + } + + public function testMergeWithComplexNestedStructure() + { + $method = $this->getMergeMethod(); + + $a = [ + 'config' => [ + 'database' => [ + 'host' => 'localhost', + 'port' => 3306, + ], + 'cache' => [ + 'driver' => 'redis', + 'servers' => ['server1', 'server2'], + ], + ], + 'features' => ['auth', 'logging'], + ]; + + $b = [ + 'config' => [ + 'database' => [ + 'name' => 'myapp', + 'port' => 5432, // This should override + ], + 'cache' => [ + 'servers' => ['server3'], // This should be appended to existing servers + ], + ], + 'features' => ['monitoring'], // This should be appended + ]; + + $expected = [ + 'config' => [ + 'database' => [ + 'host' => 'localhost', + 'port' => 5432, // Overridden + 'name' => 'myapp', + ], + 'cache' => [ + 'driver' => 'redis', + 'servers' => ['server1', 'server2', 'server3'], // Appended + ], + ], + 'features' => ['auth', 'logging', 'monitoring'], // Appended + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + } + + public function testMergeWithNullValues() + { + $method = $this->getMergeMethod(); + + // Test null values + $a = ['key' => 'value']; + $b = ['key' => null]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['key' => null], $result); + + // Test null with array + $a = ['key' => null]; + $b = ['key' => ['nested' => 'value']]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['key' => ['nested' => 'value']], $result); + } + + public function testMergeWithBooleanValues() + { + $method = $this->getMergeMethod(); + + $a = ['enabled' => true]; + $b = ['enabled' => false]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['enabled' => false], $result); + + $a = ['settings' => ['debug' => true]]; + $b = ['settings' => ['debug' => false]]; + $result = $method->invoke(null, $a, $b); + $this->assertEquals(['settings' => ['debug' => false]], $result); + } + + public function testMergeWithNumericKeys() + { + $method = $this->getMergeMethod(); + + // Test with non-sequential numeric keys + $a = [ + 'items' => [ + 0 => 'first', + 2 => 'third', + ], + ]; + + $b = [ + 'items' => [ + 1 => 'second', + 3 => 'fourth', + ], + ]; + + $expected = [ + 'items' => [ + 0 => 'first', + 1 => 'second', + 2 => 'third', + 3 => 'fourth', + ], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + } + + public function testMergeWithStringKeys() + { + $method = $this->getMergeMethod(); + + $a = [ + 'user' => [ + 'name' => 'John', + 'age' => 30, + ], + ]; + + $b = [ + 'user' => [ + 'email' => 'john@example.com', + 'age' => 31, // This should override + ], + ]; + + $expected = [ + 'user' => [ + 'name' => 'John', + 'age' => 31, // Overridden + 'email' => 'john@example.com', + ], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + } + + public function testMergeWithEmptyFirstArray() + { + $method = $this->getMergeMethod(); + + $a = []; + $b = [ + 'key1' => 'value1', + 'key2' => ['nested' => 'value2'], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($b, $result); + } + + public function testMergeWithEmptySecondArray() + { + $method = $this->getMergeMethod(); + + $a = [ + 'key1' => 'value1', + 'key2' => ['nested' => 'value2'], + ]; + $b = []; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($a, $result); + } + + public function testMergeWithDeepNesting() + { + $method = $this->getMergeMethod(); + + $a = [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'value' => 'original', + 'array' => ['a', 'b'], + ], + ], + ], + ]; + + $b = [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'value' => 'updated', + 'array' => ['c', 'd'], + 'new' => 'added', + ], + ], + ], + ]; + + $expected = [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'value' => 'updated', + 'array' => ['a', 'b', 'c', 'd'], + 'new' => 'added', + ], + ], + ], + ]; + + $result = $method->invoke(null, $a, $b); + $this->assertEquals($expected, $result); + } +} From 9bf785ec9685b0fbfae389b62aeade39ab4e183e Mon Sep 17 00:00:00 2001 From: Vedmant Date: Wed, 22 Oct 2025 11:15:40 +0700 Subject: [PATCH 09/10] Added more comprehensive test, removed mergeRecursivePreserveNumeric test --- tests/Event/Http/Psr7BridgeMergeTest.php | 440 ----------------------- tests/Event/Http/Psr7BridgeTest.php | 125 ++++++- 2 files changed, 106 insertions(+), 459 deletions(-) delete mode 100644 tests/Event/Http/Psr7BridgeMergeTest.php diff --git a/tests/Event/Http/Psr7BridgeMergeTest.php b/tests/Event/Http/Psr7BridgeMergeTest.php deleted file mode 100644 index 8dbf301c0..000000000 --- a/tests/Event/Http/Psr7BridgeMergeTest.php +++ /dev/null @@ -1,440 +0,0 @@ -getMethod('mergeRecursivePreserveNumeric'); - $method->setAccessible(true); - return $method; - } - - public function testMergeRecursivePreserveNumericBasicExamples() - { - $method = $this->getMergeMethod(); - - // Test case 1: References with numeric keys - $a1 = [ - 'references' => [ - 0 => ['some_id' => '4390954279', 'url' => ''], - 1 => ['some_id' => '4313323164'], - ], - ]; - - $b1 = [ - 'references' => [ - 1 => ['url' => ''], - ], - ]; - - $expected1 = [ - 'references' => [ - 0 => ['some_id' => '4390954279', 'url' => ''], - 1 => ['some_id' => '4313323164', 'url' => ''], - ], - ]; - - $result1 = $method->invoke(null, $a1, $b1); - $this->assertEquals($expected1, $result1); - - // Test case 2: Delete categories with scalar arrays - $a2 = [ - 'delete' => [ - 'categories' => ['123'], - ], - ]; - - $b2 = [ - 'delete' => [ - 'categories' => ['456'], - ], - ]; - - $expected2 = [ - 'delete' => [ - 'categories' => ['123', '456'], - ], - ]; - - $result2 = $method->invoke(null, $a2, $b2); - $this->assertEquals($expected2, $result2); - } - - public function testMergeWithEmptyArrays() - { - $method = $this->getMergeMethod(); - - // Test merging with empty array - $a = ['key' => ['value1']]; - $b = ['key' => []]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['key' => ['value1']], $result); - - // Test merging empty array with non-empty - $a = ['key' => []]; - $b = ['key' => ['value1']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['key' => ['value1']], $result); - } - - public function testMergeWithScalarValues() - { - $method = $this->getMergeMethod(); - - // Test scalar values override arrays - $a = ['key' => ['nested' => 'value']]; - $b = ['key' => 'scalar']; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['key' => 'scalar'], $result); - - // Test arrays override scalars - $a = ['key' => 'scalar']; - $b = ['key' => ['nested' => 'value']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['key' => ['nested' => 'value']], $result); - } - - public function testMergeWithMixedArrayTypes() - { - $method = $this->getMergeMethod(); - - // Test associative array with list - $a = ['data' => ['key1' => 'value1']]; - $b = ['data' => ['value2', 'value3']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['data' => ['key1' => 'value1', 0 => 'value2', 1 => 'value3']], $result); - - // Test list with associative array - $a = ['data' => ['value1', 'value2']]; - $b = ['data' => ['key1' => 'value3']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['data' => [0 => 'value1', 1 => 'value2', 'key1' => 'value3']], $result); - } - - public function testMergeWithNestedObjects() - { - $method = $this->getMergeMethod(); - - // Test nested object merging - $a = [ - 'users' => [ - 0 => ['id' => 1, 'name' => 'John'], - 1 => ['id' => 2, 'name' => 'Jane'], - ], - ]; - - $b = [ - 'users' => [ - 0 => ['email' => 'john@example.com'], - 1 => ['email' => 'jane@example.com', 'age' => 25], - ], - ]; - - $expected = [ - 'users' => [ - 0 => ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'], - 1 => ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com', 'age' => 25], - ], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - } - - public function testMergeWithDifferentArraySizes() - { - $method = $this->getMergeMethod(); - - // Test when array A is larger - $a = [ - 'items' => [ - 0 => ['id' => 1], - 1 => ['id' => 2], - 2 => ['id' => 3], - ], - ]; - - $b = [ - 'items' => [ - 0 => ['name' => 'Item 1'], - 1 => ['name' => 'Item 2'], - ], - ]; - - $expected = [ - 'items' => [ - 0 => ['id' => 1, 'name' => 'Item 1'], - 1 => ['id' => 2, 'name' => 'Item 2'], - 2 => ['id' => 3], - ], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - - // Test when array B is larger - $a = [ - 'items' => [ - 0 => ['id' => 1], - ], - ]; - - $b = [ - 'items' => [ - 0 => ['name' => 'Item 1'], - 1 => ['id' => 2, 'name' => 'Item 2'], - 2 => ['id' => 3, 'name' => 'Item 3'], - ], - ]; - - $expected = [ - 'items' => [ - 0 => ['id' => 1, 'name' => 'Item 1'], - 1 => ['id' => 2, 'name' => 'Item 2'], - 2 => ['id' => 3, 'name' => 'Item 3'], - ], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - } - - public function testMergeWithScalarArrays() - { - $method = $this->getMergeMethod(); - - // Test scalar arrays (lists) are appended - $a = ['tags' => ['php', 'testing']]; - $b = ['tags' => ['unit', 'integration']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['tags' => ['php', 'testing', 'unit', 'integration']], $result); - - // Test with empty scalar arrays - $a = ['tags' => []]; - $b = ['tags' => ['new', 'tags']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['tags' => ['new', 'tags']], $result); - } - - public function testMergeWithComplexNestedStructure() - { - $method = $this->getMergeMethod(); - - $a = [ - 'config' => [ - 'database' => [ - 'host' => 'localhost', - 'port' => 3306, - ], - 'cache' => [ - 'driver' => 'redis', - 'servers' => ['server1', 'server2'], - ], - ], - 'features' => ['auth', 'logging'], - ]; - - $b = [ - 'config' => [ - 'database' => [ - 'name' => 'myapp', - 'port' => 5432, // This should override - ], - 'cache' => [ - 'servers' => ['server3'], // This should be appended to existing servers - ], - ], - 'features' => ['monitoring'], // This should be appended - ]; - - $expected = [ - 'config' => [ - 'database' => [ - 'host' => 'localhost', - 'port' => 5432, // Overridden - 'name' => 'myapp', - ], - 'cache' => [ - 'driver' => 'redis', - 'servers' => ['server1', 'server2', 'server3'], // Appended - ], - ], - 'features' => ['auth', 'logging', 'monitoring'], // Appended - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - } - - public function testMergeWithNullValues() - { - $method = $this->getMergeMethod(); - - // Test null values - $a = ['key' => 'value']; - $b = ['key' => null]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['key' => null], $result); - - // Test null with array - $a = ['key' => null]; - $b = ['key' => ['nested' => 'value']]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['key' => ['nested' => 'value']], $result); - } - - public function testMergeWithBooleanValues() - { - $method = $this->getMergeMethod(); - - $a = ['enabled' => true]; - $b = ['enabled' => false]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['enabled' => false], $result); - - $a = ['settings' => ['debug' => true]]; - $b = ['settings' => ['debug' => false]]; - $result = $method->invoke(null, $a, $b); - $this->assertEquals(['settings' => ['debug' => false]], $result); - } - - public function testMergeWithNumericKeys() - { - $method = $this->getMergeMethod(); - - // Test with non-sequential numeric keys - $a = [ - 'items' => [ - 0 => 'first', - 2 => 'third', - ], - ]; - - $b = [ - 'items' => [ - 1 => 'second', - 3 => 'fourth', - ], - ]; - - $expected = [ - 'items' => [ - 0 => 'first', - 1 => 'second', - 2 => 'third', - 3 => 'fourth', - ], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - } - - public function testMergeWithStringKeys() - { - $method = $this->getMergeMethod(); - - $a = [ - 'user' => [ - 'name' => 'John', - 'age' => 30, - ], - ]; - - $b = [ - 'user' => [ - 'email' => 'john@example.com', - 'age' => 31, // This should override - ], - ]; - - $expected = [ - 'user' => [ - 'name' => 'John', - 'age' => 31, // Overridden - 'email' => 'john@example.com', - ], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - } - - public function testMergeWithEmptyFirstArray() - { - $method = $this->getMergeMethod(); - - $a = []; - $b = [ - 'key1' => 'value1', - 'key2' => ['nested' => 'value2'], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($b, $result); - } - - public function testMergeWithEmptySecondArray() - { - $method = $this->getMergeMethod(); - - $a = [ - 'key1' => 'value1', - 'key2' => ['nested' => 'value2'], - ]; - $b = []; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($a, $result); - } - - public function testMergeWithDeepNesting() - { - $method = $this->getMergeMethod(); - - $a = [ - 'level1' => [ - 'level2' => [ - 'level3' => [ - 'value' => 'original', - 'array' => ['a', 'b'], - ], - ], - ], - ]; - - $b = [ - 'level1' => [ - 'level2' => [ - 'level3' => [ - 'value' => 'updated', - 'array' => ['c', 'd'], - 'new' => 'added', - ], - ], - ], - ]; - - $expected = [ - 'level1' => [ - 'level2' => [ - 'level3' => [ - 'value' => 'updated', - 'array' => ['a', 'b', 'c', 'd'], - 'new' => 'added', - ], - ], - ], - ]; - - $result = $method->invoke(null, $a, $b); - $this->assertEquals($expected, $result); - } -} diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index c7aa8b45f..3c1d0176b 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -32,8 +32,10 @@ public function test I can create a response from a PSR7 response() ], $response->toApiGatewayFormat()); } - public function test I can convert a request from an event with body multipart data type() + public function test I can convert a request from an event with complex multipart form data structures() { + $body = "--complexBoundary\r\nContent-Disposition: form-data; name=\"simple_string\"\r\n\r\nHello World\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_string\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_string\"\r\n\r\n12345\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"boolean_string\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[0]\"\r\n\r\nfirst_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[1]\"\r\n\r\nsecond_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[2]\"\r\n\r\nthird_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[name]\"\r\n\r\nJohn Doe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[email]\"\r\n\r\njohn@example.com\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][first_name]\"\r\n\r\nJohn\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][last_name]\"\r\n\r\nDoe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][theme]\"\r\n\r\ndark\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][notifications]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][name]\"\r\n\r\nAcme Corp\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][employees]\"\r\n\r\n150\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][id]\"\r\n\r\n1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][name]\"\r\n\r\nItem One\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][0]\"\r\n\r\ntag1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][1]\"\r\n\r\ntag2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][id]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][name]\"\r\n\r\nItem Two\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][0]\"\r\n\r\ntag3\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][1]\"\r\n\r\ntag4\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[0]\"\r\n\r\nfirst\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[2]\"\r\n\r\nthird\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[5]\"\r\n\r\nsixth\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[first_key]\"\r\n\r\nfirst_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[second_key]\"\r\n\r\nsecond_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[0]\"\r\n\r\nzero_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[1]\"\r\n\r\none_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[10]\"\r\n\r\nten_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[empty_string]\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[zero_string]\"\r\n\r\n0\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[false_string]\"\r\n\r\nfalse\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[null_string]\"\r\n\r\nnull\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][name]\"\r\n\r\nDeep Item 1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][value]\"\r\n\r\n100\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][name]\"\r\n\r\nDeep Item 2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][value]\"\r\n\r\n200\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][count]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][enabled]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nfirst_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nsecond_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nthird_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with spaces]\"\r\n\r\nvalue with spaces\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with-dashes]\"\r\n\r\nvalue-with-dashes\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with_underscores]\"\r\n\r\nvalue_with_underscores\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with.dots]\"\r\n\r\nvalue.with.dots\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"unicode[测试]\"\r\n\r\n测试值\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"unicode[emoji]\"\r\n\r\n🚀🌟💻\r\n--complexBoundary--\r\n"; + $datav1 = [ 'version' => '1.0', 'resource' => '/path', @@ -43,7 +45,7 @@ public function test I can convert a request from an event with body  'Accept' => '*/*', 'Accept-Encoding' => 'gzip, deflate', 'Cache-Control' => 'no-cache', - 'Content-Type' => 'multipart/form-data; boundary=testBoundary', + 'Content-Type' => 'multipart/form-data; boundary=complexBoundary', 'Host' => 'example.org', 'User-Agent' => 'PostmanRuntime/7.20.1', 'X-Amzn-Trace-Id' => 'Root=1-ffffffff-ffffffffffffffffffffffff', @@ -84,7 +86,7 @@ public function test I can convert a request from an event with body  'domainName' => 'example.org', 'apiId' => 'xxxxxxxxxx', ], - 'body' => "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n", + 'body' => $body, 'isBase64Encoded' => false, ]; @@ -97,7 +99,7 @@ public function test I can convert a request from an event with body  'Accept' => '*/*', 'Accept-Encoding' => 'gzip, deflate', 'Cache-Control' => 'no-cache', - 'Content-Type' => 'multipart/form-data; boundary=testBoundary', + 'Content-Type' => 'multipart/form-data; boundary=complexBoundary', 'Host' => 'example.org', 'User-Agent' => 'PostmanRuntime/7.20.1', 'X-Amzn-Trace-Id' => 'Root=1-ffffffff-ffffffffffffffffffffffff', @@ -126,32 +128,117 @@ public function test I can convert a request from an event with body  'time' => '24/Nov/2019:18:55:08 +0000', 'timeEpoch' => 1574621708700, ], - 'body' => "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n

Test content

\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n", + 'body' => $body, 'isBase64Encoded' => false, ]; $expectedBody = [ - 'content' => '

Test content

', - 'some_id' => '3034', - 'references' => [ - [ - 'other_id' => '4390954279', - 'url' => '', + 'simple_string' => 'Hello World', + 'empty_string' => '', + 'numeric_string' => '12345', + 'boolean_string' => 'true', + 'indexed_array' => [ + 'first_item', + 'second_item', + 'third_item', + ], + 'associative_array' => [ + 'name' => 'John Doe', + 'age' => '30', + 'email' => 'john@example.com', + ], + 'nested_objects' => [ + 'user' => [ + 'profile' => [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'age' => '30', + ], + 'settings' => [ + 'theme' => 'dark', + 'notifications' => 'true', + ], ], + 'company' => [ + 'name' => 'Acme Corp', + 'employees' => '150', + ], + ], + 'mixed_arrays' => [ [ - 'other_id' => '4313323164', - 'url' => '', + 'id' => '1', + 'name' => 'Item One', + 'tags' => [ + 'tag1', + 'tag2', + ], ], [ - 'other_id' => '', - 'url' => 'https://someurl.com/node/745911', + 'id' => '2', + 'name' => 'Item Two', + 'tags' => [ + 'tag3', + 'tag4', + ], + ], + ], + 'sparse_array' => [ + 0 => 'first', + 2 => 'third', + 5 => 'sixth', + ], + 'string_keys' => [ + 'first_key' => 'first_value', + 'second_key' => 'second_value', + ], + 'numeric_keys' => [ + 0 => 'zero_value', + 1 => 'one_value', + 10 => 'ten_value', + ], + 'empty_values' => [ + 'empty_string' => '', + 'zero_string' => '0', + 'false_string' => 'false', + 'null_string' => 'null', + ], + 'complex_nesting' => [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'items' => [ + [ + 'name' => 'Deep Item 1', + 'value' => '100', + ], + [ + 'name' => 'Deep Item 2', + 'value' => '200', + ], + ], + 'metadata' => [ + 'count' => '2', + 'enabled' => 'true', + ], + ], + ], ], ], - 'tags' => [ - 'public health', - 'public finance', + 'duplicate_keys' => [ + 'first_duplicate', + 'second_duplicate', + 'third_duplicate', + ], + 'special_chars' => [ + 'with spaces' => 'value with spaces', + 'with-dashes' => 'value-with-dashes', + 'with_underscores' => 'value_with_underscores', + 'with.dots' => 'value.with.dots', + ], + 'unicode' => [ + '??????' => '测试值', + 'emoji' => '🚀🌟💻', ], - '_method' => 'PATCH', ]; $eventv1 = new HttpRequestEvent($datav1); From 9a32f616b98cde1e04a5b6f72def15a0045be13d Mon Sep 17 00:00:00 2001 From: Vedmant Date: Fri, 24 Oct 2025 11:48:49 +0700 Subject: [PATCH 10/10] PHP 8.0 support --- src/Event/Http/Psr7Bridge.php | 15 ++++++++++++++- tests/Event/Http/Psr7BridgeTest.php | 6 +----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php index 4935b693a..94ecc9784 100644 --- a/src/Event/Http/Psr7Bridge.php +++ b/src/Event/Http/Psr7Bridge.php @@ -11,9 +11,22 @@ use Riverline\MultiPartParser\Part; use RuntimeException; -use function array_is_list; use function str_starts_with; +// Polyfill for array_is_list (PHP 8.1+) to support PHP 8.0 +if (! function_exists('array_is_list')) { + function array_is_list(array $array): bool + { + $i = 0; + foreach ($array as $key => $value) { + if ($key !== $i++) { + return false; + } + } + return true; + } +} + /** * Bridges PSR-7 requests and responses with API Gateway or ALB event/response formats. */ diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index 3c1d0176b..df8e37db3 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -34,7 +34,7 @@ public function test I can create a response from a PSR7 response() public function test I can convert a request from an event with complex multipart form data structures() { - $body = "--complexBoundary\r\nContent-Disposition: form-data; name=\"simple_string\"\r\n\r\nHello World\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_string\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_string\"\r\n\r\n12345\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"boolean_string\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[0]\"\r\n\r\nfirst_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[1]\"\r\n\r\nsecond_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[2]\"\r\n\r\nthird_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[name]\"\r\n\r\nJohn Doe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[email]\"\r\n\r\njohn@example.com\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][first_name]\"\r\n\r\nJohn\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][last_name]\"\r\n\r\nDoe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][theme]\"\r\n\r\ndark\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][notifications]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][name]\"\r\n\r\nAcme Corp\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][employees]\"\r\n\r\n150\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][id]\"\r\n\r\n1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][name]\"\r\n\r\nItem One\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][0]\"\r\n\r\ntag1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][1]\"\r\n\r\ntag2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][id]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][name]\"\r\n\r\nItem Two\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][0]\"\r\n\r\ntag3\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][1]\"\r\n\r\ntag4\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[0]\"\r\n\r\nfirst\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[2]\"\r\n\r\nthird\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[5]\"\r\n\r\nsixth\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[first_key]\"\r\n\r\nfirst_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[second_key]\"\r\n\r\nsecond_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[0]\"\r\n\r\nzero_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[1]\"\r\n\r\none_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[10]\"\r\n\r\nten_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[empty_string]\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[zero_string]\"\r\n\r\n0\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[false_string]\"\r\n\r\nfalse\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[null_string]\"\r\n\r\nnull\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][name]\"\r\n\r\nDeep Item 1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][value]\"\r\n\r\n100\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][name]\"\r\n\r\nDeep Item 2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][value]\"\r\n\r\n200\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][count]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][enabled]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nfirst_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nsecond_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nthird_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with spaces]\"\r\n\r\nvalue with spaces\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with-dashes]\"\r\n\r\nvalue-with-dashes\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with_underscores]\"\r\n\r\nvalue_with_underscores\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with.dots]\"\r\n\r\nvalue.with.dots\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"unicode[测试]\"\r\n\r\n测试值\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"unicode[emoji]\"\r\n\r\n🚀🌟💻\r\n--complexBoundary--\r\n"; + $body = "--complexBoundary\r\nContent-Disposition: form-data; name=\"simple_string\"\r\n\r\nHello World\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_string\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_string\"\r\n\r\n12345\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"boolean_string\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[0]\"\r\n\r\nfirst_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[1]\"\r\n\r\nsecond_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[2]\"\r\n\r\nthird_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[name]\"\r\n\r\nJohn Doe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[email]\"\r\n\r\njohn@example.com\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][first_name]\"\r\n\r\nJohn\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][last_name]\"\r\n\r\nDoe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][theme]\"\r\n\r\ndark\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][notifications]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][name]\"\r\n\r\nAcme Corp\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][employees]\"\r\n\r\n150\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][id]\"\r\n\r\n1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][name]\"\r\n\r\nItem One\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][0]\"\r\n\r\ntag1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][1]\"\r\n\r\ntag2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][id]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][name]\"\r\n\r\nItem Two\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][0]\"\r\n\r\ntag3\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][1]\"\r\n\r\ntag4\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[0]\"\r\n\r\nfirst\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[2]\"\r\n\r\nthird\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[5]\"\r\n\r\nsixth\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[first_key]\"\r\n\r\nfirst_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[second_key]\"\r\n\r\nsecond_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[0]\"\r\n\r\nzero_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[1]\"\r\n\r\none_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[10]\"\r\n\r\nten_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[empty_string]\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[zero_string]\"\r\n\r\n0\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[false_string]\"\r\n\r\nfalse\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[null_string]\"\r\n\r\nnull\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][name]\"\r\n\r\nDeep Item 1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][value]\"\r\n\r\n100\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][name]\"\r\n\r\nDeep Item 2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][value]\"\r\n\r\n200\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][count]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][enabled]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nfirst_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nsecond_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nthird_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with spaces]\"\r\n\r\nvalue with spaces\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with-dashes]\"\r\n\r\nvalue-with-dashes\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with_underscores]\"\r\n\r\nvalue_with_underscores\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with.dots]\"\r\n\r\nvalue.with.dots\r\n--complexBoundary--\r\n"; $datav1 = [ 'version' => '1.0', @@ -235,10 +235,6 @@ public function test I can convert a request from an event with comple 'with_underscores' => 'value_with_underscores', 'with.dots' => 'value.with.dots', ], - 'unicode' => [ - '??????' => '测试值', - 'emoji' => '🚀🌟💻', - ], ]; $eventv1 = new HttpRequestEvent($datav1);