diff --git a/README.md b/README.md index f1128d3..750530e 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,12 @@ Given fetch field :path from previous JSON response Given the response should contain the initial state :name with the following values: Given the response should contain the initial state :name json that match with: Given the following :appId app config is set +Given /^run the command "(?P(?:[^"]|\\")*)"$/ +Given the output of the last command should contain the following text: +Given the output of the last command should be empty +Given /^run the command "(?P(?:[^"]|\\")*)" with result code (\d+)$/ +Given /^run the bash command "(?P(?:[^"]|\\")*)" with result code (\d+)$/ +Given create an environment :name with value :value to be used by occ command ``` ## Tips @@ -83,6 +89,15 @@ Given sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"base64":""} | ``` +### Step: all steps that run commands + +Before the command is executed, the following placeholders will be replaced: + +| Placeholder | Value | +| -------------------- | ---------------------- | +| `` | your app root dir | +| `` | The Nextcloud root dir | + ### Step: `fetch field :path from previous JSON response` If the json response is an array, you can fetch specific values using this step. The fetched values is stored to be used by other steps. diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 9c93f9a..9bfbb90 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -52,6 +52,15 @@ private function getLastRequest(): RequestInfo { return $lastRequest; } + /** + * When whe run the test suit of this repository at GitHub Actions, is + * necessary to consider that we haven't Nextcloud installed and mock + * the real path of files. + */ + public static function findParentDirContainingFile(string $filename): string { + return __DIR__; + } + /** * @inheritDoc */ diff --git a/features/bootstrap/console.php b/features/bootstrap/console.php new file mode 100644 index 0000000..60e588d --- /dev/null +++ b/features/bootstrap/console.php @@ -0,0 +1,17 @@ +" with result code 0 + Then the output of the last command should contain the following text: + """ + FeatureContext.php + """ + + Scenario: Test list Nextcloud directory with success + When run the bash command "ls " with result code 0 + + Scenario: Test run bash command with success + When run the bash command "true" with result code 0 + Then the output of the last command should be empty + + Scenario: Test run bash command with error + When run the bash command "false" with result code 1 + + Scenario: Run occ command with success + When run the command "status" with result code 0 + + Scenario: Run occ command with success + When run the command "invalid-command" with result code 1 + + Scenario: Create an environment with value to be used by occ command + When create an environment "OC_PASS" with value "123456" to be used by occ command + And run the command "fake-command" with result code 0 + Then the output of the last command should contain the following text: + """ + I found the environment variable OC_PASS with value 123456 + """ diff --git a/src/NextcloudApiContext.php b/src/NextcloudApiContext.php index 59ad4f0..a7ec9b4 100644 --- a/src/NextcloudApiContext.php +++ b/src/NextcloudApiContext.php @@ -30,10 +30,13 @@ class NextcloudApiContext implements Context { protected RunServerListener $server; protected string $currentUser = ''; protected array $fields = []; + protected static array $environments = []; + protected static string $commandOutput = ''; + /** * @var string[] */ - protected array $createdUsers = []; + protected static array $createdUsers = []; protected ResponseInterface $response; /** @var CookieJar[] */ protected $cookieJars; @@ -66,8 +69,9 @@ public static function beforeSuite(BeforeSuiteScope $scope):void { } #[BeforeScenario()] - public function setUp(): void { - $this->createdUsers = []; + public function beforeScenario(): void { + self::$createdUsers = []; + self::$environments = []; } #[Given('as user :user')] @@ -110,7 +114,7 @@ protected function createUser(string $user): void { $this->sendOCSRequest('GET', '/cloud/users' . '/' . $user); $this->assertStatusCode($this->response, 200, 'Failed to do first login'); - $this->createdUsers[] = $user; + self::$createdUsers[] = $user; $this->setCurrentUser($currentUser); } @@ -448,9 +452,102 @@ protected function parseText(string $text): string { return $text; } + #[Given('/^run the command "(?P(?:[^"]|\\")*)"$/')] + public static function runCommand(string $command): array { + $console = static::findParentDirContainingFile('console.php'); + $console .= '/console.php'; + $fileOwnerUid = fileowner($console); + if (!is_int($fileOwnerUid)) { + throw new \Exception('The console file owner of ' . $console . ' is not an integer UID.'); + } + $owner = posix_getpwuid($fileOwnerUid); + if ($owner === false) { + throw new \Exception('Could not retrieve owner information for UID ' . $fileOwnerUid); + } + $fullCommand = 'php ' . $console . ' ' . $command; + if (!empty(self::$environments)) { + $fullCommand = http_build_query(self::$environments, '', ' ') . ' ' . $fullCommand; + } + if (posix_getuid() !== $owner['uid']) { + $fullCommand = 'runuser -u ' . $owner['name'] . ' -- ' . $fullCommand; + } + $fullCommand .= ' 2>&1'; + return self::runBashCommand($fullCommand); + } + + public static function findParentDirContainingFile(string $filename): string { + $dir = getcwd(); + if (is_bool($dir)) { + throw new \Exception('Could not get current working directory (getcwd() returned false)'); + } + + while ($dir !== dirname($dir)) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $filename)) { + return $dir; + } + $dir = dirname($dir); + } + + throw new \Exception('The file ' . $filename . ' was not found in the parent directories of ' . $dir); + } + + private static function runBashCommand(string $command): array { + $command = str_replace('\"', '"', $command); + $patterns = []; + $replacements = []; + $fields = [ + 'appRootDir' => static::findParentDirContainingFile('appinfo'), + 'nextcloudRootDir' => static::findParentDirContainingFile('console.php'), + ]; + foreach ($fields as $key => $value) { + $patterns[] = '/<' . $key . '>/'; + $replacements[] = $value; + } + $command = preg_replace($patterns, $replacements, $command); + if (!is_string($command)) { + throw new \Exception('The command is not a string after preg_replace: ' . print_r($command, true)); + } + + exec($command, $output, $resultCode); + self::$commandOutput = implode("\n", $output); + return [ + 'command' => $command, + 'output' => $output, + 'resultCode' => $resultCode, + ]; + } + + #[Given('the output of the last command should contain the following text:')] + public static function theOutputOfTheLastCommandContains(PyStringNode $text): void { + Assert::assertStringContainsString((string) $text, self::$commandOutput, 'The output of the last command does not contain: ' . $text); + } + + #[Given('the output of the last command should be empty')] + public static function theOutputOfTheLastCommandShouldBeEmpty(): void { + Assert::assertEmpty(self::$commandOutput, 'The output of the last command should be empty, but got: ' . self::$commandOutput); + } + + #[Given('/^run the command "(?P(?:[^"]|\\")*)" with result code (\d+)$/')] + public static function runCommandWithResultCode(string $command, int $resultCode = 0): void { + $return = self::runCommand($command); + Assert::assertEquals($resultCode, $return['resultCode'], print_r($return, true)); + } + + #[Given('/^run the bash command "(?P(?:[^"]|\\")*)" with result code (\d+)$/')] + public static function runBashCommandWithResultCode(string $command, int $resultCode = 0): void { + $return = self::runBashCommand($command); + Assert::assertEquals($resultCode, $return['resultCode'], print_r($return, true)); + } + + #[Given('create an environment :name with value :value to be used by occ command')] + public static function createAnEnvironmentWithValueToBeUsedByOccCommand(string $name, string $value):void { + self::$environments[$name] = $value; + } + #[AfterScenario()] public function tearDown(): void { - foreach ($this->createdUsers as $user) { + self::$environments = []; + foreach (self::$createdUsers as $user) { $this->deleteUser($user); } } @@ -461,7 +558,7 @@ protected function deleteUser(string $user): ResponseInterface { $this->sendOCSRequest('DELETE', '/cloud/users/' . $user); $this->setCurrentUser($currentUser); - unset($this->createdUsers[array_search($user, $this->createdUsers, true)]); + unset(self::$createdUsers[array_search($user, self::$createdUsers, true)]); return $this->response; }