From 74625b8c0abb32a00de3811f7af8d176f69d3d24 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:35:37 -0300 Subject: [PATCH 1/4] feat: run command These steps make possible run occ and bash commands as steps. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- README.md | 13 +++++++ features/test.feature | 18 +++++++++ src/NextcloudApiContext.php | 73 +++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/README.md b/README.md index f1128d3..4a95456 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ vendor/bin/behat -dl Given as user :user Given user :user exists Given sending :verb to :url +Given set the response to: Given the response should be a JSON array with the following mandatory values Given /^set the display name of user "([^"]*)" to "([^"]*)"$/ Given /^set the email of user "([^"]*)" to "([^"]*)"$/ @@ -60,6 +61,9 @@ 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 /^run the command "(?P(?:[^"]|\\")*)" with result code (\d+)$/ +Given /^run the bash command "(?P(?:[^"]|\\")*)" with result code (\d+)$/ ``` ## Tips @@ -83,6 +87,15 @@ Given sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"base64":""} | ``` +### Step: all steps that run commands + +Before the command be executd, will replace the follow placeholders: + +| 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/test.feature b/features/test.feature index f187f1b..1b75e51 100644 --- a/features/test.feature +++ b/features/test.feature @@ -227,3 +227,21 @@ Feature: Test this extension Then the response should contain the initial state "appid-json-array" json that match with: | key | value | | (jq).[0] | orange | + + Scenario: Test list app directory with success + When run the bash command "ls " with result code 0 + + 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 + + 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 diff --git a/src/NextcloudApiContext.php b/src/NextcloudApiContext.php index 59ad4f0..95cb17e 100644 --- a/src/NextcloudApiContext.php +++ b/src/NextcloudApiContext.php @@ -448,6 +448,79 @@ protected function parseText(string $text): string { return $text; } + #[Given('/^run the command "(?P(?:[^"]|\\")*)"$/')] + public static function runCommand(string $command): array { + $console = self::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 (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' => self::findParentDirContainingFile('appinfo'), + 'nextcloudRootDir' => self::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); + return [ + 'command' => $command, + 'output' => $output, + 'resultCode' => $resultCode, + ]; + } + + #[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)); + } + #[AfterScenario()] public function tearDown(): void { foreach ($this->createdUsers as $user) { From 5980376621ec6e8306f8b6d98b634cbeeb95c10b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:38:49 -0300 Subject: [PATCH 2/4] feat: add more steps to works with commands Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- README.md | 8 +++++--- features/test.feature | 17 +++++++++++++++++ src/NextcloudApiContext.php | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4a95456..638d33f 100644 --- a/README.md +++ b/README.md @@ -51,19 +51,21 @@ vendor/bin/behat -dl Given as user :user Given user :user exists Given sending :verb to :url -Given set the response to: Given the response should be a JSON array with the following mandatory values Given /^set the display name of user "([^"]*)" to "([^"]*)"$/ Given /^set the email of user "([^"]*)" to "([^"]*)"$/ Given sending :verb to ocs :url Given the response should have a status code :code -Given fetch field :path from previous JSON response +Given fetch field :path from prevous 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 @@ -89,7 +91,7 @@ Given sending "post" to ocs "/apps/libresign/api/v1/request-signature" ### Step: all steps that run commands -Before the command be executd, will replace the follow placeholders: +Before the command is executed, the following placeholders will be replaced: | Placeholder | Value | | -------------------- | ---------------------- | diff --git a/features/test.feature b/features/test.feature index 1b75e51..2944281 100644 --- a/features/test.feature +++ b/features/test.feature @@ -230,18 +230,35 @@ Feature: Test this extension Scenario: Test list app directory with success When run the bash command "ls " with result code 0 + Then the output of the last command should contain the following text: + """ + appinfo + """ 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 + Then the output of the last command should contain the following text: + """ + version: + """ 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 "" to be used by occ command + And run the command "user:add --password-from-env test" with result code 1 + Then the output of the last command should contain the following text: + """ + --password-from-env given, but NC_PASS/OC_PASS is empty! + """ diff --git a/src/NextcloudApiContext.php b/src/NextcloudApiContext.php index 95cb17e..171ab1f 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; @@ -63,11 +66,13 @@ public static function beforeSuite(BeforeSuiteScope $scope):void { "runuser -u %s -- %s\n\n", get_current_user(), $whoami, get_current_user(), $command)); } + self::runCommand('config:system:set debug --value true --type boolean'); } #[BeforeScenario()] - public function setUp(): void { - $this->createdUsers = []; + public function beforeScenario(): void { + self::$createdUsers = []; + self::$environments = []; } #[Given('as user :user')] @@ -110,7 +115,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); } @@ -461,6 +466,9 @@ public static function runCommand(string $command): array { 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; } @@ -502,6 +510,7 @@ private static function runBashCommand(string $command): array { } exec($command, $output, $resultCode); + self::$commandOutput = implode("\n", $output); return [ 'command' => $command, 'output' => $output, @@ -509,6 +518,16 @@ private static function runBashCommand(string $command): array { ]; } + #[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); @@ -521,9 +540,15 @@ public static function runBashCommandWithResultCode(string $command, int $result 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); } } @@ -534,7 +559,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; } From 2fc5a50feb0a8b4f8fa2a82f3043f3584191c0ea Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:25:14 -0300 Subject: [PATCH 3/4] fix: change steps to consider execution outside Nextcloud environment 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 console.php. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- features/bootstrap/FeatureContext.php | 9 +++++++++ features/bootstrap/console.php | 17 +++++++++++++++++ features/test.feature | 12 ++++-------- src/NextcloudApiContext.php | 7 +++---- 4 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 features/bootstrap/console.php 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: """ - appinfo + FeatureContext.php """ Scenario: Test list Nextcloud directory with success @@ -247,18 +247,14 @@ Feature: Test this extension Scenario: Run occ command with success When run the command "status" with result code 0 - Then the output of the last command should contain the following text: - """ - version: - """ 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 "" to be used by occ command - And run the command "user:add --password-from-env test" with result code 1 + 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: """ - --password-from-env given, but NC_PASS/OC_PASS is empty! + I found the environment variable OC_PASS with value 123456 """ diff --git a/src/NextcloudApiContext.php b/src/NextcloudApiContext.php index 171ab1f..a7ec9b4 100644 --- a/src/NextcloudApiContext.php +++ b/src/NextcloudApiContext.php @@ -66,7 +66,6 @@ public static function beforeSuite(BeforeSuiteScope $scope):void { "runuser -u %s -- %s\n\n", get_current_user(), $whoami, get_current_user(), $command)); } - self::runCommand('config:system:set debug --value true --type boolean'); } #[BeforeScenario()] @@ -455,7 +454,7 @@ protected function parseText(string $text): string { #[Given('/^run the command "(?P(?:[^"]|\\")*)"$/')] public static function runCommand(string $command): array { - $console = self::findParentDirContainingFile('console.php'); + $console = static::findParentDirContainingFile('console.php'); $console .= '/console.php'; $fileOwnerUid = fileowner($console); if (!is_int($fileOwnerUid)) { @@ -497,8 +496,8 @@ private static function runBashCommand(string $command): array { $patterns = []; $replacements = []; $fields = [ - 'appRootDir' => self::findParentDirContainingFile('appinfo'), - 'nextcloudRootDir' => self::findParentDirContainingFile('console.php'), + 'appRootDir' => static::findParentDirContainingFile('appinfo'), + 'nextcloudRootDir' => static::findParentDirContainingFile('console.php'), ]; foreach ($fields as $key => $value) { $patterns[] = '/<' . $key . '>/'; From d870a6033c813dc17e97ac022d02a9816fac8965 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:51:54 -0300 Subject: [PATCH 4/4] fix: changes after rebase Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 638d33f..750530e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Given /^set the display name of user "([^"]*)" to "([^"]*)"$/ Given /^set the email of user "([^"]*)" to "([^"]*)"$/ Given sending :verb to ocs :url Given the response should have a status code :code -Given fetch field :path from prevous JSON response +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