1
0
mirror of https://github.com/php/pie.git synced 2026-03-24 07:22:17 +01:00
Files
archived-pie/test/integration/Command/InstallExtensionsForProjectCommandTest.php
James Titcumb 50644e72ca 517: fix inability to provide sudo prompt when using "pie install" on a PHP project
Previously, the `pie install` would invoke another instance of `pie`. However,
since this would always be run in a non-interactive shell, `sudo` prompts will
never work. I've changed this instead to use `InvokeSubCommand` which copies
the input params into the sub command, so it is all run in the same PIE process
and therefore retains the interactivity (or lack thereof) of the shell it was
invoked from.
2026-02-27 14:14:43 +00:00

228 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace Php\PieIntegrationTest\Command;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Package\Link;
use Composer\Package\RootPackage;
use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\RepositoryManager;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\MultiConstraint;
use Php\Pie\Command\InstallExtensionsForProjectCommand;
use Php\Pie\ComposerIntegration\MinimalHelperSet;
use Php\Pie\ComposerIntegration\PieJsonEditor;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\Container;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject;
use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired;
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
use Php\Pie\Platform\InstalledPiePackages;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use function getcwd;
#[CoversClass(InstallExtensionsForProjectCommand::class)]
final class InstallExtensionsForProjectCommandTest extends TestCase
{
private CommandTester $commandTester;
private ComposerFactoryForProject&MockObject $composerFactoryForProject;
private FindMatchingPackages&MockObject $findMatchingPackages;
private InstalledPiePackages&MockObject $installedPiePackages;
private InstallSelectedPackage&MockObject $installSelectedPackage;
private QuestionHelper&MockObject $questionHelper;
private InstallPiePackageFromPath&MockObject $installPiePackage;
public function setUp(): void
{
parent::setUp();
$container = $this->createMock(ContainerInterface::class);
$container->method('get')->willReturnCallback(
/** @param class-string $service */
function (string $service): mixed {
switch ($service) {
case QuieterConsoleIO::class:
return new QuieterConsoleIO(
new ArrayInput([]),
new BufferedOutput(),
new MinimalHelperSet(['question' => new QuestionHelper()]),
);
default:
return $this->createMock($service);
}
},
);
$this->composerFactoryForProject = $this->createMock(ComposerFactoryForProject::class);
$this->findMatchingPackages = $this->createMock(FindMatchingPackages::class);
$this->installedPiePackages = $this->createMock(InstalledPiePackages::class);
$this->installSelectedPackage = $this->createMock(InstallSelectedPackage::class);
$this->installPiePackage = $this->createMock(InstallPiePackageFromPath::class);
$this->questionHelper = $this->createMock(QuestionHelper::class);
Container::testFactory();
$cmd = new InstallExtensionsForProjectCommand(
$this->composerFactoryForProject,
new DetermineExtensionsRequired(),
$this->installedPiePackages,
$this->findMatchingPackages,
$this->installSelectedPackage,
$this->installPiePackage,
$container,
Container::testBuffer(),
);
$cmd->setHelperSet(new HelperSet([
'question' => $this->questionHelper,
]));
$this->commandTester = new CommandTester($cmd);
}
public function testInstallingExtensionsForPhpProject(): void
{
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
$rootPackage->setRequires([
'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'),
'ext-spl' => new Link('my/project', 'ext-spl', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'),
'ext-foobar' => new Link('my/project', 'ext-foobar', new MultiConstraint([
new Constraint('>=', '1.2.0.0-dev'),
new Constraint('<', '2.0.0.0-dev'),
]), Link::TYPE_REQUIRE, '^1.2'),
]);
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
$installedRepository = new InstalledArrayRepository([$rootPackage]);
$repositoryManager = $this->createMock(RepositoryManager::class);
$repositoryManager->method('getLocalRepository')->willReturn($installedRepository);
$composer = $this->createMock(Composer::class);
$composer->method('getPackage')->willReturn($rootPackage);
$composer->method('getRepositoryManager')->willReturn($repositoryManager);
$this->composerFactoryForProject->method('composer')->willReturn($composer);
$this->findMatchingPackages->method('for')->willReturn([
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
]);
$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');
$this->installSelectedPackage->expects(self::once())
->method('withSubCommand')
->with(
ExtensionName::normaliseFromString('foobar'),
new RequestedPackageAndVersion(
'vendor1/foobar',
'^1.2',
),
)
->willReturn(0);
$this->commandTester->execute(
['--allow-non-interactive-project-install' => true],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);
$outputString = $this->commandTester->getDisplay();
$this->commandTester->assertCommandIsSuccessful($outputString);
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
self::assertStringContainsString('requires: ext-spl:* ✅ Already installed', $outputString);
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
}
public function testInstallingExtensionsForPhpProjectWithMultipleMatches(): void
{
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
$rootPackage->setRequires([
'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'),
'ext-foobar' => new Link('my/project', 'ext-foobar', new MultiConstraint([
new Constraint('>=', '1.2.0.0-dev'),
new Constraint('<', '2.0.0.0-dev'),
]), Link::TYPE_REQUIRE, '^1.2'),
]);
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
$installedRepository = new InstalledArrayRepository([$rootPackage]);
$repositoryManager = $this->createMock(RepositoryManager::class);
$repositoryManager->method('getLocalRepository')->willReturn($installedRepository);
$composer = $this->createMock(Composer::class);
$composer->method('getPackage')->willReturn($rootPackage);
$composer->method('getRepositoryManager')->willReturn($repositoryManager);
$this->composerFactoryForProject->method('composer')->willReturn($composer);
$this->findMatchingPackages->method('for')->willReturn([
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
]);
$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');
$this->installSelectedPackage->expects(self::never())
->method('withSubCommand');
$this->commandTester->execute(
['--allow-non-interactive-project-install' => true],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);
$outputString = $this->commandTester->getDisplay();
self::assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
self::assertStringContainsString('Multiple packages were found for ext-foobar', $outputString);
}
public function testInstallingExtensionsForPieProject(): void
{
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
$rootPackage->setType(ExtensionType::PhpModule->value);
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
$this->installPiePackage
->expects(self::once())
->method('__invoke')
->with(
self::isInstanceOf(InstallExtensionsForProjectCommand::class),
getcwd(),
$rootPackage,
self::isInstanceOf(PieJsonEditor::class),
self::isInstanceOf(InputInterface::class),
self::isInstanceOf(IOInterface::class),
)
->willReturn(Command::SUCCESS);
$this->commandTester->execute(
[],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);
$this->commandTester->assertCommandIsSuccessful();
}
}