Skip to content

Commit e4c3d97

Browse files
CLI-1729: Restore /translation API handling and _links removal logic in CLI (#1962)
1 parent 5b44af7 commit e4c3d97

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

src/Command/Api/ApiBaseCommand.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
130130
$exitCode = 1;
131131
}
132132

133+
if (substr($this->path, 0, 12) === '/translation') {
134+
$this->mungeResponse($response);
135+
}
133136
if ($exitCode || !$this->getParamFromInput($input, 'task-wait')) {
134137
$contents = json_encode($response, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
135138
$this->output->writeln($contents);
@@ -140,6 +143,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
140143
return $success ? Command::SUCCESS : Command::FAILURE;
141144
}
142145

146+
private function mungeResponse(mixed $response): void
147+
{
148+
if (is_object($response) && property_exists($response, '_links')) {
149+
unset($response->_links);
150+
}
151+
foreach ($response as $value) {
152+
if (property_exists($value, '_links')) {
153+
unset($value->_links);
154+
}
155+
}
156+
}
157+
143158
public function setMethod(string $method): void
144159
{
145160
$this->method = $method;

tests/phpunit/src/Commands/Api/ApiCommandTest.php

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)