@@ -761,4 +761,206 @@ public function testPrereleaseCommandsAreHidden(): void
761761 }
762762 }
763763 }
764+
765+ /**
766+ * Tests that mungeResponse is called for paths starting with exactly '/translation' (12 characters).
767+ * This test ensures the correct substring length check.
768+ */
769+ public function testTranslationPathMungesResponse (): void
770+ {
771+ $ this ->clientProphecy ->addOption ('headers ' , ['Accept ' => 'application/hal+json, version=2 ' ])
772+ ->shouldBeCalled ();
773+
774+ // Create a response array with items that have _links that should be removed.
775+ $ item1 = (object )[
776+ 'id ' => 1 ,
777+ 'name ' => 'item1 ' ,
778+ '_links ' => (object )['self ' => (object )['href ' => '/translation/1 ' ]],
779+ ];
780+ $ item2 = (object )[
781+ 'id ' => 2 ,
782+ 'name ' => 'item2 ' ,
783+ '_links ' => (object )['self ' => (object )['href ' => '/translation/2 ' ]],
784+ ];
785+ $ mockResponse = [$ item1 , $ item2 ];
786+
787+ $ this ->clientProphecy ->request ('get ' , '/translation ' )
788+ ->willReturn ($ mockResponse )
789+ ->shouldBeCalled ();
790+
791+ $ this ->command = $ this ->getApiCommandByName ('api:accounts:ssh-keys-list ' );
792+ // Override the path to test translation endpoint.
793+ $ this ->command ->setPath ('/translation ' );
794+ $ this ->command ->setMethod ('get ' );
795+
796+ $ this ->executeCommand ();
797+
798+ $ output = $ this ->getDisplay ();
799+ $ decoded = json_decode ($ output , true );
800+
801+ // Verify _links was removed (munged) from all items.
802+ $ this ->assertIsArray ($ decoded );
803+ $ this ->assertCount (2 , $ decoded );
804+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded [0 ]);
805+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded [1 ]);
806+ $ this ->assertEquals (1 , $ decoded [0 ]['id ' ]);
807+ $ this ->assertEquals (2 , $ decoded [1 ]['id ' ]);
808+ }
809+
810+ /**
811+ * Tests that mungeResponse IS called for a 13-character path starting with '/translation'.
812+ * This kills the IncrementInteger mutant that checks for 13 characters instead of 12.
813+ *
814+ * Path '/translationX' (13 chars):
815+ * - Original: substr('/translationX', 0, 12) = '/translation' → matches → munges
816+ * - Mutant: substr('/translationX', 0, 13) = '/translationX' !== '/translation' → doesn't match → doesn't munge
817+ *
818+ * By verifying _links are removed, we ensure the original 12-char check is used.
819+ * If the mutant exists, _links would remain and this test would fail.
820+ */
821+ public function testTranslationsPathDoesNotMungeResponse (): void
822+ {
823+ $ this ->clientProphecy ->addOption ('headers ' , ['Accept ' => 'application/hal+json, version=2 ' ])
824+ ->shouldBeCalled ();
825+
826+ // Create a response array with items that have _links.
827+ // Original code will munge (remove _links), mutant will not.
828+ $ item1 = (object )[
829+ 'id ' => 1 ,
830+ 'name ' => 'item1 ' ,
831+ '_links ' => (object )['self ' => (object )['href ' => '/translationX/1 ' ]],
832+ ];
833+ $ item2 = (object )[
834+ 'id ' => 2 ,
835+ 'name ' => 'item2 ' ,
836+ '_links ' => (object )['self ' => (object )['href ' => '/translationX/2 ' ]],
837+ ];
838+ $ mockResponse = [$ item1 , $ item2 ];
839+
840+ $ this ->clientProphecy ->request ('get ' , '/translationX ' )
841+ ->willReturn ($ mockResponse )
842+ ->shouldBeCalled ();
843+
844+ $ this ->command = $ this ->getApiCommandByName ('api:accounts:ssh-keys-list ' );
845+ // Path is 13 characters, starts with '/translation' (12 chars)
846+ $ this ->command ->setPath ('/translationX ' );
847+ $ this ->command ->setMethod ('get ' );
848+
849+ $ this ->executeCommand ();
850+
851+ $ output = $ this ->getDisplay ();
852+ $ decoded = json_decode ($ output , true );
853+
854+ // Verify _links was removed (munged by original 12-char check).
855+ // If mutant (13-char check) exists, _links would remain and this assertion would fail.
856+ $ this ->assertIsArray ($ decoded );
857+ $ this ->assertCount (2 , $ decoded );
858+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded [0 ]);
859+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded [1 ]);
860+ }
861+
862+ /**
863+ * Tests that mungeResponse is NOT called for paths ending with '/translation' but not starting with it.
864+ * This kills the DecrementInteger mutant that checks the end of the string.
865+ */
866+ public function testPathEndingWithTranslationDoesNotMungeResponse (): void
867+ {
868+ $ this ->clientProphecy ->addOption ('headers ' , ['Accept ' => 'application/hal+json, version=2 ' ])
869+ ->shouldBeCalled ();
870+
871+ // Create a response array with items that have _links that should NOT be removed.
872+ $ item1 = (object )[
873+ 'id ' => 1 ,
874+ 'name ' => 'item1 ' ,
875+ '_links ' => (object )['self ' => (object )['href ' => '/something/translation/1 ' ]],
876+ ];
877+ $ item2 = (object )[
878+ 'id ' => 2 ,
879+ 'name ' => 'item2 ' ,
880+ '_links ' => (object )['self ' => (object )['href ' => '/something/translation/2 ' ]],
881+ ];
882+ $ mockResponse = [$ item1 , $ item2 ];
883+
884+ $ this ->clientProphecy ->request ('get ' , '/something/translation ' )
885+ ->willReturn ($ mockResponse )
886+ ->shouldBeCalled ();
887+
888+ $ this ->command = $ this ->getApiCommandByName ('api:accounts:ssh-keys-list ' );
889+ // Override the path to test a path that ends with /translation but doesn't start with it.
890+ $ this ->command ->setPath ('/something/translation ' );
891+ $ this ->command ->setMethod ('get ' );
892+
893+ $ this ->executeCommand ();
894+
895+ $ output = $ this ->getDisplay ();
896+ $ decoded = json_decode ($ output , true );
897+
898+ // Verify _links was NOT removed (not munged)
899+ $ this ->assertIsArray ($ decoded );
900+ $ this ->assertCount (2 , $ decoded );
901+ $ this ->assertArrayHasKey ('_links ' , $ decoded [0 ]);
902+ $ this ->assertArrayHasKey ('_links ' , $ decoded [1 ]);
903+ }
904+
905+ /**
906+ * Tests that mungeResponse removes _links from the top-level response object.
907+ * This kills the LogicalAndNegation mutant that negates the condition.
908+ *
909+ * Original: if (is_object($response) && property_exists($response, '_links')) → unsets _links
910+ * Mutant: if (!(is_object($response) && property_exists($response, '_links'))) → doesn't unset _links
911+ *
912+ * By verifying _links are removed from the top-level object, we ensure the original condition is used.
913+ * If the mutant exists, _links would remain and this test would fail.
914+ */
915+ public function testTranslationPathMungesTopLevelLinks (): void
916+ {
917+ $ this ->clientProphecy ->addOption ('headers ' , ['Accept ' => 'application/hal+json, version=2 ' ])
918+ ->shouldBeCalled ();
919+
920+ // Create a response object (not array) with _links at the top level.
921+ // The foreach loop will iterate over object properties, so we only include object properties
922+ // to avoid calling property_exists() on non-object values.
923+ $ mockResponse = (object )[
924+ 'data ' => (object )[
925+ 'message ' => 'test ' ,
926+ '_links ' => (object )['self ' => (object )['href ' => '/translation/data ' ]],
927+ ],
928+ 'meta ' => (object )[
929+ 'count ' => 10 ,
930+ '_links ' => (object )['self ' => (object )['href ' => '/translation/meta ' ]],
931+ ],
932+ '_links ' => (object )[
933+ 'next ' => (object )['href ' => '/translation?page=2 ' ],
934+ 'self ' => (object )['href ' => '/translation ' ],
935+ ],
936+ ];
937+
938+ $ this ->clientProphecy ->request ('get ' , '/translation ' )
939+ ->willReturn ($ mockResponse )
940+ ->shouldBeCalled ();
941+
942+ $ this ->command = $ this ->getApiCommandByName ('api:accounts:ssh-keys-list ' );
943+ // Override the path to test translation endpoint.
944+ $ this ->command ->setPath ('/translation ' );
945+ $ this ->command ->setMethod ('get ' );
946+
947+ $ this ->executeCommand ();
948+
949+ $ output = $ this ->getDisplay ();
950+ $ decoded = json_decode ($ output , true );
951+
952+ // Verify _links was removed (munged) from the top-level object.
953+ // Original code: is_object($response) && property_exists($response, '_links') → true → unsets
954+ // Mutant code: !(is_object($response) && property_exists($response, '_links')) → false → doesn't unset
955+ // So if mutant exists, _links would remain and this assertion would fail.
956+ $ this ->assertIsArray ($ decoded );
957+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded );
958+ // Verify nested objects' _links are also removed by the foreach loop.
959+ $ this ->assertIsArray ($ decoded ['data ' ]);
960+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded ['data ' ]);
961+ $ this ->assertEquals ('test ' , $ decoded ['data ' ]['message ' ]);
962+ $ this ->assertIsArray ($ decoded ['meta ' ]);
963+ $ this ->assertArrayNotHasKey ('_links ' , $ decoded ['meta ' ]);
964+ $ this ->assertEquals (10 , $ decoded ['meta ' ]['count ' ]);
965+ }
764966}
0 commit comments