From d0d64a98cf1086adf8354798eaca4e53f2dcacd6 Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Sat, 3 May 2025 13:26:14 +0300 Subject: [PATCH 1/5] magento/magento2#39729: Cannot return null for non-nullable field "SelectedCustomizableOption.label" --- .../QuoteGraphQl/Model/Resolver/CustomizableOptions.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php index 87ed4ba7ef5d6..c0ef249237fa1 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php @@ -1,7 +1,7 @@ Date: Sat, 5 Jul 2025 09:35:18 +0300 Subject: [PATCH 2/5] magento/magento2#39729: Cannot return null for non-nullable field - Add unit tests for CustomizableOptions resolver --- .../Resolver/CustomizableOptionsTest.php | 446 ++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php new file mode 100644 index 0000000000000..1e79c320b4fa4 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php @@ -0,0 +1,446 @@ +customizableOptionMock = $this->createMock(CustomizableOption::class); + $this->fieldMock = $this->createMock(Field::class); + $this->resolveInfoMock = $this->createMock(ResolveInfo::class); + $this->quoteItemMock = $this->createMock(QuoteItem::class); + $this->quoteItemOptionMock = $this->createMock(QuoteItemOption::class); + + $this->resolver = new CustomizableOptions($this->customizableOptionMock); + } + + /** + * Test resolve method throws exception when model is not provided + */ + public function testResolveThrowsExceptionWhenModelNotProvided(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('"model" value should be specified'); + + $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, []); + } + + /** + * Test resolve method returns empty array when no option_ids found + */ + public function testResolveReturnsEmptyArrayWhenNoOptionIds(): void + { + $value = ['model' => $this->quoteItemMock]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn(null); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([], $result); + } + + /** + * Test resolve method returns empty array when option_ids value is null + */ + public function testResolveReturnsEmptyArrayWhenOptionIdsValueIsNull(): void + { + $value = ['model' => $this->quoteItemMock]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(1)) + ->method('getValue') + ->willReturn(null); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([], $result); + } + + /** + * Test resolve method returns customizable options data when valid options exist + */ + public function testResolveReturnsCustomizableOptionsData(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = '1,2,3'; + $expectedOptionData1 = [ + 'id' => 1, + 'label' => 'Option 1', + 'type' => 'field', + 'values' => [] + ]; + $expectedOptionData2 = [ + 'id' => 2, + 'label' => 'Option 2', + 'type' => 'dropdown', + 'values' => [] + ]; + $expectedOptionData3 = [ + 'id' => 3, + 'label' => 'Option 3', + 'type' => 'area', + 'values' => [] + ]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->exactly(3)) + ->method('getData') + ->willReturnCallback(function ($cartItem, $optionId) use ($expectedOptionData1, $expectedOptionData2, $expectedOptionData3) { + switch ($optionId) { + case 1: + return $expectedOptionData1; + case 2: + return $expectedOptionData2; + case 3: + return $expectedOptionData3; + default: + return []; + } + }); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([$expectedOptionData1, $expectedOptionData2, $expectedOptionData3], $result); + } + + /** + * Test resolve method skips empty customizable options and returns only valid ones + */ + public function testResolveSkipsEmptyCustomizableOptions(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = '1,2,3'; + $expectedOptionData1 = [ + 'id' => 1, + 'label' => 'Option 1', + 'type' => 'field', + 'values' => [] + ]; + $expectedOptionData3 = [ + 'id' => 3, + 'label' => 'Option 3', + 'type' => 'area', + 'values' => [] + ]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->exactly(3)) + ->method('getData') + ->willReturnCallback(function ($cartItem, $optionId) use ($expectedOptionData1, $expectedOptionData3) { + switch ($optionId) { + case 1: + return $expectedOptionData1; + case 2: + return []; + case 3: + return $expectedOptionData3; + default: + return []; + } + }); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([$expectedOptionData1, $expectedOptionData3], $result); + $this->assertCount(2, $result); + } + + /** + * Test resolve method handles all empty customizable options + */ + public function testResolveHandlesAllEmptyCustomizableOptions(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = '1,2,3'; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->exactly(3)) + ->method('getData') + ->willReturn([]); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([], $result); + $this->assertCount(0, $result); + } + + /** + * Test resolve method handles mixed null and empty array returns + */ + public function testResolveHandlesMixedNullAndEmptyArrayReturns(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = '1,2,3,4'; + $expectedOptionData1 = [ + 'id' => 1, + 'label' => 'Valid Option', + 'type' => 'field', + 'values' => [] + ]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->exactly(4)) + ->method('getData') + ->willReturnCallback(function ($cartItem, $optionId) use ($expectedOptionData1) { + switch ($optionId) { + case 1: + return $expectedOptionData1; + case 2: + case 3: + case 4: + return []; + default: + return []; + } + }); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([$expectedOptionData1], $result); + $this->assertCount(1, $result); + } + + /** + * Test resolve method handles single option ID + */ + public function testResolveHandlesSingleOptionId(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = '1'; + $expectedOptionData = [ + 'id' => 1, + 'label' => 'Single Option', + 'type' => 'field', + 'values' => [] + ]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->once()) + ->method('getData') + ->with($this->quoteItemMock, 1) + ->willReturn($expectedOptionData); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([$expectedOptionData], $result); + $this->assertCount(1, $result); + } + + /** + * Test resolve method handles empty option IDs string + */ + public function testResolveHandlesEmptyOptionIdsString(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = ''; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->once()) + ->method('getData') + ->with($this->quoteItemMock, 0) + ->willReturn([]); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([], $result); + } + + /** + * Test resolve method handles whitespace-only option IDs + */ + public function testResolveHandlesWhitespaceOnlyOptionIds(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = ' , , '; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->exactly(3)) + ->method('getData') + ->willReturn([]); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([], $result); + } + + /** + * Test resolve method handles deleted custom options by skipping them and returning only valid options + */ + public function testResolveHandlesDeletedCustomOption(): void + { + $value = ['model' => $this->quoteItemMock]; + $optionIds = '1,999,2'; + $validOption1 = [ + 'id' => 1, + 'label' => 'Valid Option 1', + 'type' => 'field', + 'values' => [ + [ + 'label' => 'Valid Value', + 'value' => 'test' + ] + ] + ]; + $validOption2 = [ + 'id' => 2, + 'label' => 'Valid Option 2', + 'type' => 'dropdown', + 'values' => [ + [ + 'label' => 'Another Valid Value', + 'value' => 'test2' + ] + ] + ]; + + $this->quoteItemMock->expects($this->once()) + ->method('getOptionByCode') + ->with('option_ids') + ->willReturn($this->quoteItemOptionMock); + + $this->quoteItemOptionMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturn($optionIds); + + $this->customizableOptionMock->expects($this->exactly(3)) + ->method('getData') + ->willReturnCallback(function ($cartItem, $optionId) use ($validOption1, $validOption2) { + switch ($optionId) { + case 1: + return $validOption1; + case 999: + return []; + case 2: + return $validOption2; + default: + return []; + } + }); + + $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); + + $this->assertEquals([$validOption1, $validOption2], $result); + $this->assertCount(2, $result); + + foreach ($result as $option) { + $this->assertIsArray($option); + $this->assertArrayHasKey('label', $option); + $this->assertNotNull($option['label']); + } + } +} From 701d6bfd9f4962400b73b95d6ec4a5a573512034 Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Sat, 5 Jul 2025 12:32:00 +0300 Subject: [PATCH 3/5] magento/magento2#39729: Cannot return null for non-nullable field - Refactor tests to improve callback formatting. --- .../Resolver/CustomizableOptionsTest.php | 100 ++++++++++-------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php index 1e79c320b4fa4..5751aa0d56056 100644 --- a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php @@ -152,18 +152,24 @@ public function testResolveReturnsCustomizableOptionsData(): void $this->customizableOptionMock->expects($this->exactly(3)) ->method('getData') - ->willReturnCallback(function ($cartItem, $optionId) use ($expectedOptionData1, $expectedOptionData2, $expectedOptionData3) { - switch ($optionId) { - case 1: - return $expectedOptionData1; - case 2: - return $expectedOptionData2; - case 3: - return $expectedOptionData3; - default: - return []; + ->willReturnCallback( + function ($unused, $optionId) use ( + $expectedOptionData1, + $expectedOptionData2, + $expectedOptionData3 + ) { + switch ($optionId) { + case 1: + return $expectedOptionData1; + case 2: + return $expectedOptionData2; + case 3: + return $expectedOptionData3; + default: + return []; + } } - }); + ); $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); @@ -201,18 +207,20 @@ public function testResolveSkipsEmptyCustomizableOptions(): void $this->customizableOptionMock->expects($this->exactly(3)) ->method('getData') - ->willReturnCallback(function ($cartItem, $optionId) use ($expectedOptionData1, $expectedOptionData3) { - switch ($optionId) { - case 1: - return $expectedOptionData1; - case 2: - return []; - case 3: - return $expectedOptionData3; - default: - return []; + ->willReturnCallback( + function ($unused, $optionId) use ($expectedOptionData1, $expectedOptionData3) { + switch ($optionId) { + case 1: + return $expectedOptionData1; + case 2: + return []; + case 3: + return $expectedOptionData3; + default: + return []; + } } - }); + ); $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); @@ -272,18 +280,20 @@ public function testResolveHandlesMixedNullAndEmptyArrayReturns(): void $this->customizableOptionMock->expects($this->exactly(4)) ->method('getData') - ->willReturnCallback(function ($cartItem, $optionId) use ($expectedOptionData1) { - switch ($optionId) { - case 1: - return $expectedOptionData1; - case 2: - case 3: - case 4: - return []; - default: - return []; + ->willReturnCallback( + function ($unused, $optionId) use ($expectedOptionData1) { + switch ($optionId) { + case 1: + return $expectedOptionData1; + case 2: + case 3: + case 4: + return []; + default: + return []; + } } - }); + ); $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); @@ -419,18 +429,20 @@ public function testResolveHandlesDeletedCustomOption(): void $this->customizableOptionMock->expects($this->exactly(3)) ->method('getData') - ->willReturnCallback(function ($cartItem, $optionId) use ($validOption1, $validOption2) { - switch ($optionId) { - case 1: - return $validOption1; - case 999: - return []; - case 2: - return $validOption2; - default: - return []; + ->willReturnCallback( + function ($unused, $optionId) use ($validOption1, $validOption2) { + switch ($optionId) { + case 1: + return $validOption1; + case 999: + return []; + case 2: + return $validOption2; + default: + return []; + } } - }); + ); $result = $this->resolver->resolve($this->fieldMock, null, $this->resolveInfoMock, $value); From cdd13d6b5479c1c33019b5a38bc16e7336425eec Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Sat, 5 Jul 2025 14:28:21 +0300 Subject: [PATCH 4/5] magento/magento2#39729: Cannot return null for non-nullable field - Refactor callback signatures in unit tests. --- .../Resolver/CustomizableOptionsTest.php | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php index 5751aa0d56056..cb6347a5adbb0 100644 --- a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php @@ -19,6 +19,7 @@ /** * Unit test for CustomizableOptions resolver + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ class CustomizableOptionsTest extends TestCase { @@ -153,7 +154,10 @@ public function testResolveReturnsCustomizableOptionsData(): void $this->customizableOptionMock->expects($this->exactly(3)) ->method('getData') ->willReturnCallback( - function ($unused, $optionId) use ( + function ( + QuoteItem $item, + int $optionId + ) use ( $expectedOptionData1, $expectedOptionData2, $expectedOptionData3 @@ -208,7 +212,10 @@ public function testResolveSkipsEmptyCustomizableOptions(): void $this->customizableOptionMock->expects($this->exactly(3)) ->method('getData') ->willReturnCallback( - function ($unused, $optionId) use ($expectedOptionData1, $expectedOptionData3) { + function ( + QuoteItem $item, + int $optionId + ) use ($expectedOptionData1, $expectedOptionData3) { switch ($optionId) { case 1: return $expectedOptionData1; @@ -281,7 +288,10 @@ public function testResolveHandlesMixedNullAndEmptyArrayReturns(): void $this->customizableOptionMock->expects($this->exactly(4)) ->method('getData') ->willReturnCallback( - function ($unused, $optionId) use ($expectedOptionData1) { + function ( + QuoteItem $item, + int $optionId + ) use ($expectedOptionData1) { switch ($optionId) { case 1: return $expectedOptionData1; @@ -430,7 +440,10 @@ public function testResolveHandlesDeletedCustomOption(): void $this->customizableOptionMock->expects($this->exactly(3)) ->method('getData') ->willReturnCallback( - function ($unused, $optionId) use ($validOption1, $validOption2) { + function ( + QuoteItem $item, + int $optionId + ) use ($validOption1, $validOption2) { switch ($optionId) { case 1: return $validOption1; From 9cda7a13aca429a506c6469a0a62c370140b4996 Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Sat, 5 Jul 2025 15:17:19 +0300 Subject: [PATCH 5/5] magento/magento2#39729: Cannot return null for non-nullable field - Refactor callback signatures in unit tests. --- .../Unit/Model/Resolver/CustomizableOptionsTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php index cb6347a5adbb0..22ea3a12fbc1b 100644 --- a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CustomizableOptionsTest.php @@ -215,7 +215,10 @@ public function testResolveSkipsEmptyCustomizableOptions(): void function ( QuoteItem $item, int $optionId - ) use ($expectedOptionData1, $expectedOptionData3) { + ) use ( + $expectedOptionData1, + $expectedOptionData3 + ) { switch ($optionId) { case 1: return $expectedOptionData1; @@ -443,7 +446,10 @@ public function testResolveHandlesDeletedCustomOption(): void function ( QuoteItem $item, int $optionId - ) use ($validOption1, $validOption2) { + ) use ( + $validOption1, + $validOption2 + ) { switch ($optionId) { case 1: return $validOption1;