UPSTREAM BASELINE: nextcloud/spreed v22.0.12 (без изменений)
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Источник: https://github.com/nextcloud/spreed/archive/refs/tags/v22.0.12.tar.gz С этого коммита ветка официального Nextcloud Talk отрезана (решение владельца 2026-07-06). Все дальнейшие изменения — только наши; версии релизов: 22.0.12-f7.N.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use Behat\Step\Given;
|
||||
use Behat\Step\Then;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
require __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
// The following attributes are expected to be available in the class that uses
|
||||
// this trait:
|
||||
// - currentServer
|
||||
// - localServerUrl
|
||||
// - remoteServerUrl
|
||||
trait CommandLineTrait {
|
||||
/** @var int return code of last command */
|
||||
private int $lastCode = 0;
|
||||
/** @var string stdout of last command */
|
||||
private string $lastStdOut = '';
|
||||
/** @var string stderr of last command */
|
||||
private string $lastStdErr = '';
|
||||
protected string $ocPath = '../../../..';
|
||||
|
||||
/**
|
||||
* Invokes an OCC command
|
||||
*
|
||||
* @param string[] $args OCC command, the part behind "occ". For example: "files:transfer-ownership"
|
||||
* @param string[] $env environment variables
|
||||
* @return int exit code
|
||||
*/
|
||||
public function runOcc(array $args = [], array $env = []): int {
|
||||
// Set UTF-8 locale to ensure that escapeshellarg will not strip
|
||||
// multibyte characters.
|
||||
setlocale(LC_CTYPE, 'C.UTF-8');
|
||||
|
||||
$clearOpcodeCache = in_array($args[0], [
|
||||
'app:disable',
|
||||
'app:enable',
|
||||
'config:system:delete',
|
||||
'config:system:set',
|
||||
'maintenance:mode',
|
||||
], true);
|
||||
|
||||
$args = array_map(static function (string|int $arg) {
|
||||
return escapeshellarg((string)$arg);
|
||||
}, $args);
|
||||
$args[] = '--no-ansi';
|
||||
$argString = implode(' ', $args);
|
||||
|
||||
if ($this->currentServer === 'REMOTE') {
|
||||
$env['NEXTCLOUD_CONFIG_DIR'] = getenv('NEXTCLOUD_REMOTE_CONFIG_DIR');
|
||||
$serverRootDir = getenv('NEXTCLOUD_REMOTE_ROOT_DIR');
|
||||
} else {
|
||||
$env['NEXTCLOUD_CONFIG_DIR'] = getenv('NEXTCLOUD_HOST_CONFIG_DIR');
|
||||
$serverRootDir = getenv('NEXTCLOUD_HOST_ROOT_DIR');
|
||||
}
|
||||
|
||||
$descriptor = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
$process = proc_open('php ' . $serverRootDir . '/console.php ' . $argString, $descriptor, $pipes, $this->ocPath, $env);
|
||||
$this->lastStdOut = stream_get_contents($pipes[1]);
|
||||
$this->lastStdErr = stream_get_contents($pipes[2]);
|
||||
$this->lastCode = proc_close($process);
|
||||
|
||||
if ($clearOpcodeCache) {
|
||||
// Clean opcode cache
|
||||
$client = new GuzzleHttp\Client();
|
||||
|
||||
if ($this->currentServer === 'REMOTE') {
|
||||
$client->request('GET', $this->remoteServerUrl . 'apps/testing/clean_opcode_cache.php');
|
||||
} else {
|
||||
$client->request('GET', $this->localServerUrl . 'apps/testing/clean_opcode_cache.php');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lastCode;
|
||||
}
|
||||
|
||||
#[Given('/^invoking occ with "([^"]*)"$/')]
|
||||
public function invokingTheCommand(string $cmd): void {
|
||||
// FIXME this way is deprecated
|
||||
if (preg_match('/room-name:(?P<token>\w+)/', $cmd, $matches)) {
|
||||
if (array_key_exists($matches['token'], self::$identifierToToken)) {
|
||||
$cmd = preg_replace('/room-name:(\w+)/', self::$identifierToToken[$matches['token']], $cmd);
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/ROOM\((?P<name>\w+)\)/', $cmd, $matches)) {
|
||||
if (array_key_exists($matches['name'], self::$identifierToToken)) {
|
||||
$cmd = preg_replace('/ROOM\((\w+)\)/', self::$identifierToToken[$matches['name']], $cmd);
|
||||
}
|
||||
}
|
||||
if (preg_match('/BOT\((?P<name>\w+)\)/', $cmd, $matches)) {
|
||||
if (array_key_exists($matches['name'], self::$botNameToId)) {
|
||||
$cmd = preg_replace('/BOT\((\w+)\)/', (string)self::$botNameToId[$matches['name']], $cmd);
|
||||
}
|
||||
}
|
||||
|
||||
$args = explode(' ', $cmd);
|
||||
$this->runOcc($args);
|
||||
}
|
||||
|
||||
public function getLastStdOut(): string {
|
||||
return $this->lastStdOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find exception texts in stderr
|
||||
*/
|
||||
public function findExceptions(): array {
|
||||
$exceptions = [];
|
||||
$captureNext = false;
|
||||
// the exception text usually appears after an "[Exception"] row
|
||||
foreach (explode("\n", $this->lastStdErr) as $line) {
|
||||
if (preg_match('/\[Exception\]/', $line)) {
|
||||
$captureNext = true;
|
||||
continue;
|
||||
}
|
||||
if ($captureNext) {
|
||||
$exceptions[] = trim($line);
|
||||
$captureNext = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $exceptions;
|
||||
}
|
||||
|
||||
#[Then('/^the command was successful$/')]
|
||||
public function theCommandWasSuccessful(): void {
|
||||
$exceptions = $this->findExceptions();
|
||||
if ($this->lastCode !== 0) {
|
||||
echo $this->lastStdErr;
|
||||
|
||||
$msg = 'The command was not successful, exit code was ' . $this->lastCode . '.';
|
||||
if (!empty($exceptions)) {
|
||||
$msg .= "\n" . ' Exceptions: ' . implode(', ', $exceptions);
|
||||
} else {
|
||||
$msg .= "\n" . ' ' . $this->lastStdOut;
|
||||
$msg .= "\n" . ' ' . $this->lastStdErr;
|
||||
}
|
||||
throw new \Exception($msg);
|
||||
} elseif (!empty($exceptions)) {
|
||||
$msg = 'The command was successful but triggered exceptions: ' . implode(', ', $exceptions);
|
||||
throw new \Exception($msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[Then('/^the command failed with exit code ([0-9]+)$/')]
|
||||
public function theCommandFailedWithExitCode(int $exitCode): void {
|
||||
Assert::assertEquals($exitCode, $this->lastCode, 'The commands exit code did not match: ' . $this->lastStdOut . "\n\n" . $this->lastStdErr);
|
||||
}
|
||||
|
||||
#[Then('/^the command failed with exception text "([^"]*)"$/')]
|
||||
public function theCommandFailedWithException(string $exceptionText): void {
|
||||
$exceptions = $this->findExceptions();
|
||||
if (empty($exceptions)) {
|
||||
throw new \Exception('The command did not throw any exceptions');
|
||||
}
|
||||
|
||||
if (!in_array($exceptionText, $exceptions)) {
|
||||
throw new \Exception('The command did not throw any exception with the text "' . $exceptionText . '"');
|
||||
}
|
||||
}
|
||||
|
||||
#[Then('/^the command output contains the text:$/')]
|
||||
#[Then('/^the command output contains the text "([^"]*)"$/')]
|
||||
#[Then("/^the command output contains the text '([^']*)'\$/")]
|
||||
public function theCommandOutputContainsTheText(string $text): void {
|
||||
if ($this->lastStdOut === '' && $this->lastStdErr !== '') {
|
||||
Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stdout');
|
||||
Assert::assertTrue(false, 'The command did not output the expected text on stdout but stderr');
|
||||
}
|
||||
|
||||
Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
|
||||
}
|
||||
|
||||
#[Then('/^the command output is empty$/')]
|
||||
public function theCommandOutputIsEmpty(): void {
|
||||
Assert::assertEmpty($this->lastStdOut, 'The command did output unexpected text on stdout');
|
||||
}
|
||||
|
||||
#[Then("/^the command output contains the list entry '([^']*)' with value '([^']*)'\$/")]
|
||||
public function theCommandOutputContainsTheListEntry(string $key, string $value): void {
|
||||
if (preg_match('/^"ROOM\(([^"]+)\)"$/', $key, $matches)) {
|
||||
$key = '"' . self::$identifierToToken[$matches[1]] . '"';
|
||||
}
|
||||
$text = '- ' . $key . ': ' . $value;
|
||||
|
||||
if ($this->lastStdOut === '' && $this->lastStdErr !== '') {
|
||||
Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stdout');
|
||||
Assert::assertTrue(false, 'The command did not output the expected text on stdout but stderr');
|
||||
}
|
||||
|
||||
Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
|
||||
}
|
||||
|
||||
#[Then('/^the command error output contains the text "([^"]*)"$/')]
|
||||
public function theCommandErrorOutputContainsTheText(string $text): void {
|
||||
if ($this->lastStdErr === '' && $this->lastStdOut !== '') {
|
||||
Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
|
||||
Assert::assertTrue(false, 'The command did not output the expected text on stdout but stderr');
|
||||
}
|
||||
|
||||
Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stderr');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
use Behat\Hook\AfterScenario;
|
||||
use Behat\Step\Given;
|
||||
use Behat\Step\Then;
|
||||
use Behat\Step\When;
|
||||
use GuzzleHttp\Client;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
// The following attributes and methods are expected to be available in the
|
||||
// class that uses this trait:
|
||||
// - baseUrl
|
||||
// - assertStatusCode()
|
||||
// - sendRequest()
|
||||
// - sendRequestFullUrl()
|
||||
// - setAppConfig()
|
||||
trait RecordingTrait {
|
||||
/** @var ?resource */
|
||||
private $recordingServerProcess = '';
|
||||
/** @var ?resource */
|
||||
private $signalingServerProcess = '';
|
||||
|
||||
private string $recordingServerAddress = 'localhost';
|
||||
private int $recordingServerPort = 0;
|
||||
private string $signalingServerAddress = 'localhost';
|
||||
private int $signalingServerPort = 0;
|
||||
|
||||
private function getRecordingServerAddress(): string {
|
||||
if (!str_contains($this->recordingServerAddress, ':')) {
|
||||
$port = $this->getOpenPort($this->recordingServerAddress);
|
||||
$this->recordingServerAddress = $this->recordingServerAddress . ':' . $port;
|
||||
}
|
||||
return $this->recordingServerAddress;
|
||||
}
|
||||
|
||||
private function getSignalingServerAddress(): string {
|
||||
if (!str_contains($this->signalingServerAddress, ':')) {
|
||||
$port = $this->getOpenPort($this->signalingServerAddress);
|
||||
$this->signalingServerAddress = $this->signalingServerAddress . ':' . $port;
|
||||
}
|
||||
return $this->signalingServerAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an open port
|
||||
*/
|
||||
private function getOpenPort(string $address): int {
|
||||
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
|
||||
|
||||
if (!socket_bind($sock, $address, 0)) {
|
||||
throw new \Exception('Failute to bind socket to host ' . $address);
|
||||
}
|
||||
|
||||
socket_getsockname($sock, $ipAddress, $port);
|
||||
socket_close($sock);
|
||||
|
||||
if ($port > 0) {
|
||||
return $port;
|
||||
}
|
||||
|
||||
throw new \Exception('Impossible to find an open port');
|
||||
}
|
||||
|
||||
#[Given('/^(recording|signaling) server is started$/')]
|
||||
public function recordingServerIsStarted(): void {
|
||||
if ($this->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "the … secret" is hardcoded in the fake recording server.
|
||||
$this->setAppConfig('spreed', new TableNode([['recording_servers', json_encode([
|
||||
'servers' => [
|
||||
[
|
||||
'server' => 'http://' . $this->getRecordingServerAddress()
|
||||
]
|
||||
],
|
||||
'secret' => 'the recording secret'
|
||||
])]]));
|
||||
$this->setAppConfig('spreed', new TableNode([['signaling_servers', json_encode([
|
||||
'servers' => [
|
||||
[
|
||||
'server' => 'http://' . $this->getSignalingServerAddress()
|
||||
]
|
||||
],
|
||||
'secret' => 'the signaling secret'
|
||||
])]]));
|
||||
|
||||
$path = 'mocks/FakeRecordingServer.php';
|
||||
$this->recordingServerProcess = $this->startMockServer(
|
||||
$this->getRecordingServerAddress() . ' ' . $path
|
||||
);
|
||||
|
||||
$path = 'mocks/FakeSignalingServer.php';
|
||||
$this->signalingServerProcess = $this->startMockServer(
|
||||
$this->getSignalingServerAddress() . ' ' . $path
|
||||
);
|
||||
|
||||
$this->waitForMockServer();
|
||||
|
||||
register_shutdown_function(function () {
|
||||
$this->recordingServerIsStopped();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource
|
||||
*/
|
||||
private function startMockServer(string $path) {
|
||||
$cmd = 'php -S ' . $path;
|
||||
$stdout = tempnam(sys_get_temp_dir(), 'mockserv-stdout-');
|
||||
|
||||
// We need to prefix exec to get the correct process http://php.net/manual/ru/function.proc-get-status.php#93382
|
||||
$fullCmd = sprintf('exec %s > %s 2>&1',
|
||||
$cmd,
|
||||
$stdout
|
||||
);
|
||||
|
||||
$pipes = [];
|
||||
$env = null;
|
||||
$cwd = null;
|
||||
|
||||
$stdin = fopen('php://stdin', 'rb');
|
||||
$stdoutf = tempnam(sys_get_temp_dir(), 'MockWebServer.stdout');
|
||||
$stderrf = tempnam(sys_get_temp_dir(), 'MockWebServer.stderr');
|
||||
|
||||
$descriptorSpec = [
|
||||
0 => $stdin,
|
||||
1 => [ 'file', $stdoutf, 'a' ],
|
||||
2 => [ 'file', $stderrf, 'a' ],
|
||||
];
|
||||
|
||||
$process = proc_open($fullCmd, $descriptorSpec, $pipes, $cwd, $env, [
|
||||
'suppress_errors' => false,
|
||||
'bypass_shell' => true,
|
||||
]);
|
||||
|
||||
if (is_resource($process)) {
|
||||
return $process;
|
||||
}
|
||||
|
||||
throw new \Exception('Error starting server');
|
||||
}
|
||||
|
||||
private function waitForMockServer(): void {
|
||||
[$host, $port] = explode(':', $this->getSignalingServerAddress());
|
||||
$mockServerIsUp = false;
|
||||
for ($i = 0; $i <= 20; $i++) {
|
||||
$open = @fsockopen($host, (int)$port);
|
||||
if (is_resource($open)) {
|
||||
fclose($open);
|
||||
$mockServerIsUp = true;
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
if (!$mockServerIsUp) {
|
||||
throw new \Exception('Failure to start mock server.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the Web Server currently running?
|
||||
*/
|
||||
public function isRunning(): bool {
|
||||
if (!is_resource($this->recordingServerProcess)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$processStatus = proc_get_status($this->recordingServerProcess);
|
||||
|
||||
if (!$processStatus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $processStatus['running'];
|
||||
}
|
||||
|
||||
#[AfterScenario]
|
||||
#[Given('/^(recording|signaling) server is stopped$/')]
|
||||
public function recordingServerIsStopped(): void {
|
||||
if (gettype($this->recordingServerProcess) === 'resource') {
|
||||
$this->stop($this->recordingServerProcess);
|
||||
$this->recordingServerProcess = null;
|
||||
}
|
||||
if (gettype($this->signalingServerProcess) === 'resource') {
|
||||
$this->stop($this->signalingServerProcess);
|
||||
$this->signalingServerProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function stop($process): void {
|
||||
proc_terminate($process);
|
||||
|
||||
$attempts = 0;
|
||||
while ($this->isRunning()) {
|
||||
if (++$attempts > 1000) {
|
||||
throw new \Exception('Failed to stop server.');
|
||||
}
|
||||
|
||||
usleep(10000);
|
||||
}
|
||||
}
|
||||
|
||||
#[When('/^recording server sent started request for "(audio|video)" recording in room "([^"]*)" as "([^"]*)" with (\d+)(?: \((v1)\))?$/')]
|
||||
public function recordingServerSentStartedRequestForRecordingInRoomAsWith(string $recordingType, string $identifier, string $user, int $statusCode, string $apiVersion = 'v1'): void {
|
||||
$recordingTypes = [
|
||||
'video' => 1,
|
||||
'audio' => 2,
|
||||
];
|
||||
|
||||
$data = [
|
||||
'type' => 'started',
|
||||
'started' => [
|
||||
'token' => FeatureContext::getTokenForIdentifier($identifier),
|
||||
'status' => $recordingTypes[$recordingType],
|
||||
'actor' => [
|
||||
'type' => 'users',
|
||||
'id' => $user,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->sendBackendRequestFromRecordingServer($data, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
#[When('/^recording server sent stopped request for recording in room "([^"]*)" with (\d+)(?: \((v1)\))?$/')]
|
||||
public function recordingServerSentStoppedRequestForRecordingInRoomWith(string $identifier, int $statusCode, string $apiVersion = 'v1'): void {
|
||||
$this->recordingServerSentStoppedRequestForRecordingInRoomAsWith($identifier, null, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
#[When('/^recording server sent stopped request for recording in room "([^"]*)" as "([^"]*)" with (\d+)(?: \((v1)\))?$/')]
|
||||
public function recordingServerSentStoppedRequestForRecordingInRoomAsWith(string $identifier, ?string $user, int $statusCode, string $apiVersion = 'v1'): void {
|
||||
$data = [
|
||||
'type' => 'stopped',
|
||||
'stopped' => [
|
||||
'token' => FeatureContext::getTokenForIdentifier($identifier),
|
||||
],
|
||||
];
|
||||
|
||||
if ($user !== null) {
|
||||
$data['stopped']['actor'] = [
|
||||
'type' => 'users',
|
||||
'id' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
$this->sendBackendRequestFromRecordingServer($data, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
#[When('/^recording server sent failed request for recording in room "([^"]*)" with (\d+)(?: \((v1)\))?$/')]
|
||||
public function recordingServerSentFailedRequestForRecordingInRoomWith(string $identifier, int $statusCode, string $apiVersion = 'v1'): void {
|
||||
$data = [
|
||||
'type' => 'failed',
|
||||
'failed' => [
|
||||
'token' => FeatureContext::getTokenForIdentifier($identifier),
|
||||
],
|
||||
];
|
||||
|
||||
$this->sendBackendRequestFromRecordingServer($data, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
private function sendBackendRequestFromRecordingServer(array $data, int $statusCode, string $apiVersion = 'v1'): void {
|
||||
$body = json_encode($data);
|
||||
|
||||
$random = md5((string)rand());
|
||||
$checksum = hash_hmac('sha256', $random . $body, 'the recording secret');
|
||||
|
||||
$headers = [
|
||||
'Backend-Url' => $this->baseUrl . 'ocs/v2.php/apps/spreed/api/' . $apiVersion . '/recording/backend',
|
||||
'Talk-Recording-Random' => $random,
|
||||
'Talk-Recording-Checksum' => $checksum,
|
||||
];
|
||||
|
||||
$this->sendRequestFullUrl('POST', 'http://' . $this->getRecordingServerAddress() . '/fake/send-backend-request', $body, $headers);
|
||||
$this->assertStatusCode($this->response, $statusCode);
|
||||
}
|
||||
|
||||
#[Then('/^(recording|signaling) server received the following requests$/')]
|
||||
public function fakeServerReceivedTheFollowingRequests(string $server, ?TableNode $formData = null): void {
|
||||
if ($server === 'recording') {
|
||||
$requests = $this->getRecordingServerReceivedRequests();
|
||||
} else {
|
||||
$requests = $this->getSignalingServerReceivedRequests();
|
||||
}
|
||||
|
||||
if ($formData === null) {
|
||||
Assert::assertEmpty($requests);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($requests === null) {
|
||||
Assert::fail('No received requests');
|
||||
return;
|
||||
}
|
||||
|
||||
$count = count($formData->getHash());
|
||||
Assert::assertCount($count, $requests, 'Request count does not match' . "\n" . json_encode($requests));
|
||||
|
||||
$requests = array_map(static function (array $actual) {
|
||||
$actualDataJson = json_decode($actual['data'], true);
|
||||
$write = false;
|
||||
|
||||
// Fix sorting of postgres
|
||||
if (isset($actualDataJson['update']['userids'])) {
|
||||
sort($actualDataJson['update']['userids']);
|
||||
$write = true;
|
||||
}
|
||||
if (isset($actualDataJson['participants']['users'])) {
|
||||
usort($actualDataJson['participants']['users'], static fn (array $u1, array $u2) => $u1['userId'] <=> $u2['userId']);
|
||||
$write = true;
|
||||
}
|
||||
|
||||
if ($write) {
|
||||
$actual['data'] = json_encode($actualDataJson);
|
||||
}
|
||||
|
||||
return $actual;
|
||||
}, $requests);
|
||||
|
||||
$expected = array_map(static function (array $request, array $actual) {
|
||||
$identifier = $request['token'];
|
||||
$request['token'] = FeatureContext::getTokenForIdentifier($identifier);
|
||||
|
||||
$matched = preg_match('/ROOM\(([^)]+)\)/', $request['data'], $matches);
|
||||
if ($matched) {
|
||||
$request['data'] = str_replace(
|
||||
'ROOM(' . $matches[1] . ')',
|
||||
FeatureContext::getTokenForIdentifier($matches[1]),
|
||||
$request['data']
|
||||
);
|
||||
}
|
||||
|
||||
$matched = preg_match('/PHONE\((\+\d+)\)/', $request['data'], $matches);
|
||||
if ($matched) {
|
||||
$request['data'] = str_replace(
|
||||
'PHONE(' . $matches[1] . ')',
|
||||
FeatureContext::getActorIdForPhoneNumber($matches[1]),
|
||||
$request['data']
|
||||
);
|
||||
}
|
||||
|
||||
$matched = preg_match('/PHONEATTENDEE\((\+\d+)\)/', $request['data'], $matches);
|
||||
if ($matched) {
|
||||
$request['data'] = str_replace(
|
||||
'PHONEATTENDEE(' . $matches[1] . ')',
|
||||
(string)FeatureContext::getAttendeeIdForPhoneNumber($identifier, $matches[1]),
|
||||
$request['data']
|
||||
);
|
||||
}
|
||||
|
||||
$matched = preg_match('/SESSION\(([^)]+)\)/', $request['data'], $matches);
|
||||
if ($matched) {
|
||||
$request['data'] = str_replace(
|
||||
'SESSION(' . $matches[1] . ')',
|
||||
str_replace('/', '\/', FeatureContext::getSessionIdForUser($matches[1])),
|
||||
$request['data']
|
||||
);
|
||||
}
|
||||
|
||||
$matched = preg_match('/"lastPing":LAST_PING\(\)/', $request['data'], $matches);
|
||||
if ($matched) {
|
||||
$matched = preg_match('/"lastPing":(\d+)/', $actual['data'], $matches);
|
||||
if ($matched) {
|
||||
$request['data'] = str_replace(
|
||||
'"lastPing":LAST_PING()',
|
||||
$matches[0],
|
||||
$request['data']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$matched = preg_match('/"active-since":\{"date":"ACTIVE_SINCE\(\)","timezone_type":3,"timezone":"UTC"}/', $request['data'], $matches);
|
||||
if ($matched) {
|
||||
$matched = preg_match('/"active-since":\{"date":"([\d\- :.]+)","timezone_type":3,"timezone":"UTC"}/', $actual['data'], $matches);
|
||||
if ($matched) {
|
||||
$request['data'] = str_replace(
|
||||
'ACTIVE_SINCE()',
|
||||
$matches[1],
|
||||
$request['data']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $request;
|
||||
}, $formData->getHash(), $requests);
|
||||
|
||||
Assert::assertEquals($expected, $requests);
|
||||
}
|
||||
|
||||
#[Then('/^signaling server will respond with$/')]
|
||||
public function nextSignalingServerResponseIs(TableNode $formData): void {
|
||||
$data = $formData->getRowsHash();
|
||||
$nextResponseFile = sys_get_temp_dir() . '/fake-nextcloud-talk-signaling-response';
|
||||
file_put_contents($nextResponseFile, $data['response']);
|
||||
}
|
||||
|
||||
#[Then('/^reset (recording|signaling) server requests$/')]
|
||||
public function resetSignalingServerRequests(string $server): void {
|
||||
if ($server === 'recording') {
|
||||
$this->getRecordingServerReceivedRequests();
|
||||
} else {
|
||||
$this->getSignalingServerReceivedRequests();
|
||||
}
|
||||
}
|
||||
|
||||
private function getRecordingServerReceivedRequests(): ?array {
|
||||
$url = 'http://' . $this->getRecordingServerAddress() . '/fake/requests';
|
||||
$client = new Client();
|
||||
$response = $client->get($url);
|
||||
|
||||
return json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
|
||||
private function getSignalingServerReceivedRequests(): ?array {
|
||||
$url = 'http://' . $this->getSignalingServerAddress() . '/fake/requests';
|
||||
$client = new Client();
|
||||
$response = $client->get($url);
|
||||
|
||||
return json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use Behat\Behat\Context\Context;
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
use Behat\Step\Given;
|
||||
use Behat\Step\Then;
|
||||
use Behat\Step\When;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class SharingContext implements Context {
|
||||
private string $baseUrl;
|
||||
private string $currentServer;
|
||||
private ?ResponseInterface $response = null;
|
||||
private ?string $responseBody = null;
|
||||
private string $currentUser = '';
|
||||
private array $adminUser;
|
||||
private string $regularUserPassword;
|
||||
private ?\SimpleXMLElement $lastCreatedShareData = null;
|
||||
|
||||
public function __construct(string $baseUrl, array $admin, string $regularUserPassword) {
|
||||
$this->baseUrl = $baseUrl;
|
||||
$this->adminUser = $admin;
|
||||
$this->regularUserPassword = $regularUserPassword;
|
||||
|
||||
// in case of ci deployment we take the server url from the environment
|
||||
$testServerUrl = getenv('TEST_SERVER_URL');
|
||||
if ($testServerUrl !== false) {
|
||||
$this->baseUrl = $testServerUrl;
|
||||
}
|
||||
}
|
||||
|
||||
public function setCurrentServer(string $currentServer, string $baseUrl): void {
|
||||
$this->currentServer = $currentServer;
|
||||
$this->baseUrl = $baseUrl;
|
||||
}
|
||||
|
||||
#[Given('user :user creates folder :destination')]
|
||||
public function userCreatesFolder(string $user, string $destination): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = "/$user/$destination/";
|
||||
|
||||
$this->sendingToDav('MKCOL', $url);
|
||||
|
||||
$this->theHTTPStatusCodeShouldBe(201);
|
||||
}
|
||||
|
||||
#[Given('user :user moves file :source to :destination')]
|
||||
public function userMovesFileTo(string $user, string $source, string $destination): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = "/$user/$source";
|
||||
|
||||
$headers = [];
|
||||
$headers['Destination'] = $this->baseUrl . "remote.php/dav/files/$user/" . $destination;
|
||||
|
||||
$this->sendingToDav('MOVE', $url, $headers);
|
||||
}
|
||||
|
||||
#[Given('user :user moves file :source to :destination with :statusCode')]
|
||||
public function userMovesFileToWith(string $user, string $source, string $destination, int $statusCode): void {
|
||||
$this->userMovesFileTo($user, $source, $destination);
|
||||
$this->theHTTPStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[Given('user :user deletes file :file')]
|
||||
public function userDeletesFile(string $user, string $file): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = "/$user/$file";
|
||||
|
||||
$this->sendingToDav('DELETE', $url);
|
||||
|
||||
$this->theHTTPStatusCodeShouldBe(204);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with user :sharee')]
|
||||
public function userSharesWithUser(string $user, string $path, string $sharee, ?TableNode $body = null): void {
|
||||
$this->userSharesWith($user, $path, 0 /*IShare::TYPE_USER*/, $sharee, $body);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with user :sharee with OCS :statusCode')]
|
||||
public function userSharesWithUserWithOcs(string $user, string $path, string $sharee, int $statusCode): void {
|
||||
$this->userSharesWithUser($user, $path, $sharee);
|
||||
$this->theOCSStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with group :sharee')]
|
||||
public function userSharesWithGroup(string $user, string $path, string $sharee, ?TableNode $body = null): void {
|
||||
$this->userSharesWith($user, $path, 1 /*IShare::TYPE_GROUP*/, $sharee, $body);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with group :sharee with OCS :statusCode')]
|
||||
public function userSharesWithGroupWithOcs(string $user, string $path, string $sharee, int $statusCode): void {
|
||||
$this->userSharesWithGroup($user, $path, $sharee);
|
||||
$this->theOCSStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with team :sharee')]
|
||||
public function userSharesWithTeam(string $user, string $path, string $sharee, ?TableNode $body = null): void {
|
||||
$this->userSharesWith($user, $path, 7 /*IShare::TYPE_CIRCLE*/, $sharee, $body);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with team :sharee with OCS :statusCode')]
|
||||
public function userSharesWithTeamWithOcs(string $user, string $path, string $sharee, int $statusCode): void {
|
||||
$this->userSharesWithTeam($user, $path, FeatureContext::getTeamIdForLabel($this->currentServer, $sharee));
|
||||
$this->theOCSStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with room :room')]
|
||||
public function userSharesWithRoom(string $user, string $path, string $room, ?TableNode $body = null): void {
|
||||
$this->userSharesWith($user, $path, 10 /*IShare::TYPE_ROOM*/, FeatureContext::getTokenForIdentifier($room), $body);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with :amount rooms')]
|
||||
public function userSharesWithManyRooms(string $user, string $path, int $amount, ?TableNode $body = null): void {
|
||||
for ($i = 1; $i <= $amount; $i++) {
|
||||
$identifier = 'room' . $i;
|
||||
$this->userSharesWith($user, $path, 10 /*IShare::TYPE_ROOM*/, FeatureContext::getTokenForIdentifier($identifier), $body);
|
||||
}
|
||||
}
|
||||
|
||||
#[When('user :user shares :path with room :room with OCS :statusCode')]
|
||||
public function userSharesWithRoomWithOcs(string $user, string $path, string $room, int $statusCode): void {
|
||||
$this->userSharesWithRoom($user, $path, $room);
|
||||
$this->theOCSStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path by link')]
|
||||
public function userSharesByLink(string $user, string $path, ?TableNode $body = null): void {
|
||||
$this->userSharesWith($user, $path, 3 /*IShare::TYPE_LINK*/, '', $body);
|
||||
}
|
||||
|
||||
#[When('user :user shares :path by link with OCS :statusCode')]
|
||||
public function userSharesByLinkWithOcs(string $user, string $path, int $statusCode, ?TableNode $body = null): void {
|
||||
$this->userSharesByLink($user, $path, $body);
|
||||
$this->theOCSStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[When('user :user updates last share with')]
|
||||
public function userUpdatesLastShareWith(string $user, TableNode $body): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares/' . $this->getLastShareId();
|
||||
|
||||
$this->sendingTo('PUT', $url, $body);
|
||||
}
|
||||
|
||||
#[When('user :user deletes last share')]
|
||||
public function userDeletesLastShare(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares/' . $this->getLastShareId();
|
||||
|
||||
$this->sendingTo('DELETE', $url);
|
||||
}
|
||||
|
||||
#[When('user :user deletes last share with OCS :statusCode')]
|
||||
public function userDeletesLastShareWithOcs(string $user, int $statusCode): void {
|
||||
$this->userDeletesLastShare($user);
|
||||
$this->theOCSStatusCodeShouldBe($statusCode);
|
||||
}
|
||||
|
||||
#[When('user :user restores last share')]
|
||||
public function userRestoresLastShareWithOcs(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/deletedshares/ocRoomShare:' . $this->getLastShareId();
|
||||
|
||||
$this->sendingTo('POST', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets last share')]
|
||||
public function userGetsLastShare(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares/' . $this->getLastShareId();
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user accepts last share')]
|
||||
public function userAcceptsLastShare(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares/pending/' . $this->getLastShareId();
|
||||
|
||||
$this->sendingTo('POST', $url);
|
||||
|
||||
$this->theHTTPStatusCodeShouldBe(200);
|
||||
$this->theOCSStatusCodeShouldBe(100);
|
||||
}
|
||||
|
||||
#[When('user :user gets all shares')]
|
||||
public function userGetsAllShares(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares';
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets all shares and reshares')]
|
||||
public function userGetsAllSharesAndReshares(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares?reshares=true';
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets all shares for :path')]
|
||||
public function userGetsAllSharesFor(string $user, string $path): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares?path=' . $path;
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets all shares and reshares for :path')]
|
||||
public function userGetsAllSharesAndResharesFor(string $user, string $path): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares?reshares=true&path=' . $path;
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets all shares for :path and its subfiles')]
|
||||
public function userGetsAllSharesForAndItsSubfiles(string $user, string $path): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares?subfiles=true&path=' . $path;
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets all received shares')]
|
||||
public function userGetsAllReceivedShares(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares?shared_with_me=true';
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets all received shares for :path')]
|
||||
public function userGetsAllReceivedSharesFor(string $user, string $path): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares?shared_with_me=true&path=' . $path;
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('user :user gets deleted shares')]
|
||||
public function userGetsDeletedShares(string $user): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/deletedshares';
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
}
|
||||
|
||||
#[When('/^user "([^"]*)" gets sharees for$/')]
|
||||
public function userGetsShareesFor(string $user, TableNode $body): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/sharees';
|
||||
|
||||
$parameters = [];
|
||||
$parameters[] = 'shareType=10'; // IShare::TYPE_ROOM,
|
||||
$parameters[] = 'itemType=file';
|
||||
foreach ($body->getRowsHash() as $key => $value) {
|
||||
$parameters[] = $key . '=' . $value;
|
||||
}
|
||||
|
||||
$url .= '?' . implode('&', $parameters);
|
||||
|
||||
$this->sendingTo('GET', $url);
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals(200, $this->response->getStatusCode());
|
||||
}
|
||||
|
||||
#[When('user :user gets the DAV properties for :path')]
|
||||
public function userGetsTheDavPropertiesFor(string $user, string $path): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = "/$user/$path";
|
||||
|
||||
$this->sendingToDav('PROPFIND', $url);
|
||||
|
||||
$this->theHTTPStatusCodeShouldBe(207);
|
||||
}
|
||||
|
||||
#[When('user :user gets the share-type DAV property for :path')]
|
||||
public function userGetsTheShareTypeDavPropertyFor(string $user, string $path): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = "/$user/$path";
|
||||
|
||||
$headers = null;
|
||||
|
||||
$body = '<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">'
|
||||
. ' <d:prop>'
|
||||
. ' <oc:share-types/>'
|
||||
. ' </d:prop>'
|
||||
. '</d:propfind>';
|
||||
|
||||
$this->sendingToDav('PROPFIND', $url, $headers, $body);
|
||||
|
||||
$this->theHTTPStatusCodeShouldBe(207);
|
||||
}
|
||||
|
||||
#[When('user :user gets recent files')]
|
||||
public function userGetsRecentFiles(string $user): void {
|
||||
// Recents endpoint is not an OCS endpoint, so a request token must be
|
||||
// provided.
|
||||
[$requestToken, $cookieJar] = $this->loggingInUsingWebAs($user);
|
||||
|
||||
$url = '/index.php/apps/files/api/v1/recent';
|
||||
|
||||
$this->sendingToWithRequestToken('GET', $url, $requestToken, $cookieJar);
|
||||
}
|
||||
|
||||
#[When('transfering ownership from :user1 to :user2')]
|
||||
public function transferingOwnershipFromTo(string $user1, string $user2): void {
|
||||
$args = ['files:transfer-ownership', $user1, $user2];
|
||||
|
||||
$args = array_map(function ($arg) {
|
||||
return escapeshellarg($arg);
|
||||
}, $args);
|
||||
$args[] = '--no-ansi';
|
||||
$args = implode(' ', $args);
|
||||
|
||||
$descriptor = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
$process = proc_open('php console.php ' . $args, $descriptor, $pipes, '../../../../');
|
||||
$lastStdOut = stream_get_contents($pipes[1]);
|
||||
$lastStdErr = stream_get_contents($pipes[2]);
|
||||
$lastCode = proc_close($process);
|
||||
|
||||
// Clean opcode cache
|
||||
$client = new GuzzleHttp\Client();
|
||||
$client->request('GET', $this->baseUrl . '/apps/testing/clean_opcode_cache.php');
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals(0, $lastCode);
|
||||
}
|
||||
|
||||
#[Then('the OCS status code should be :statusCode')]
|
||||
public function theOCSStatusCodeShouldBe(int $statusCode): void {
|
||||
$meta = $this->getXmlResponse()->meta[0];
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals($statusCode, (int)$meta->statuscode, 'Response message: ' . (string)$meta->message);
|
||||
}
|
||||
|
||||
#[Then('the HTTP status code should be :statusCode')]
|
||||
public function theHTTPStatusCodeShouldBe(int $statusCode): void {
|
||||
\PHPUnit\Framework\Assert::assertEquals($statusCode, $this->response->getStatusCode());
|
||||
}
|
||||
|
||||
#[Then('the list of returned shares has :count shares')]
|
||||
public function theListOfReturnedSharesHasShares(int $count): void {
|
||||
$this->theHTTPStatusCodeShouldBe(200);
|
||||
$this->theOCSStatusCodeShouldBe(100);
|
||||
|
||||
$returnedShares = $this->getXmlResponse()->data[0];
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals($count, count($returnedShares->element));
|
||||
}
|
||||
|
||||
#[Then('share is returned with')]
|
||||
public function shareIsReturnedWith(TableNode $body): void {
|
||||
$this->shareXIsReturnedWith(0, $body);
|
||||
}
|
||||
|
||||
#[Then('share :number is returned with')]
|
||||
public function shareXIsReturnedWith(int $number, TableNode $body): void {
|
||||
$this->theHTTPStatusCodeShouldBe(200);
|
||||
$this->theOCSStatusCodeShouldBe(100);
|
||||
|
||||
if (!($body instanceof TableNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$returnedShare = $this->getXmlResponse()->data[0];
|
||||
if ($returnedShare->element) {
|
||||
$returnedShare = (array)$returnedShare;
|
||||
$returnedShare = $returnedShare['element'];
|
||||
if (is_array($returnedShare)) {
|
||||
usort($returnedShare, static function ($share1, $share2) {
|
||||
return (int)$share1->id - (int)$share2->id;
|
||||
});
|
||||
}
|
||||
|
||||
$returnedShare = $returnedShare[$number];
|
||||
}
|
||||
|
||||
$defaultExpectedFields = [
|
||||
'id' => 'A_NUMBER',
|
||||
'share_type' => '10', // IShare::TYPE_ROOM,
|
||||
'permissions' => '19',
|
||||
'stime' => 'A_NUMBER',
|
||||
'parent' => '',
|
||||
'expiration' => '',
|
||||
'token' => '',
|
||||
'storage' => 'A_NUMBER',
|
||||
'item_source' => 'A_NUMBER',
|
||||
'file_source' => 'A_NUMBER',
|
||||
'file_parent' => 'A_NUMBER',
|
||||
'mail_send' => '0',
|
||||
];
|
||||
|
||||
$fields = $body->getRowsHash();
|
||||
if (isset($fields['share_type']) && ($fields['share_type'] === '10' || $fields['share_type'] === '11')) {
|
||||
$defaultExpectedFields['share_with_link'] = 'URL';
|
||||
}
|
||||
|
||||
if (isset($fields['share_type']) && ($fields['share_type'] === '0' || $fields['share_type'] === '1')) {
|
||||
/**
|
||||
* This field was changed so often in the server,
|
||||
* that we simply don't care any more. We want to test Talk,
|
||||
* not normal user shares.
|
||||
* Refs:
|
||||
* - https://github.com/nextcloud/server/pull/49898
|
||||
* - https://github.com/nextcloud/server/pull/48381
|
||||
* - https://github.com/nextcloud/spreed/pull/13632
|
||||
*/
|
||||
unset($defaultExpectedFields['mail_send']);
|
||||
}
|
||||
|
||||
$expectedFields = array_merge($defaultExpectedFields, $fields);
|
||||
|
||||
if (!array_key_exists('uid_file_owner', $expectedFields)
|
||||
&& array_key_exists('uid_owner', $expectedFields)) {
|
||||
$expectedFields['uid_file_owner'] = $expectedFields['uid_owner'];
|
||||
}
|
||||
if (!array_key_exists('displayname_file_owner', $expectedFields)
|
||||
&& array_key_exists('displayname_owner', $expectedFields)) {
|
||||
$expectedFields['displayname_file_owner'] = $expectedFields['displayname_owner'];
|
||||
}
|
||||
|
||||
if (array_key_exists('share_type', $expectedFields)
|
||||
&& $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */
|
||||
&& array_key_exists('share_with', $expectedFields)) {
|
||||
if ($expectedFields['share_with'] === 'private_conversation') {
|
||||
$expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/';
|
||||
$expectedFields['share_with_link'] = '';
|
||||
} else {
|
||||
$expectedFields['share_with'] = FeatureContext::getTokenForIdentifier($expectedFields['share_with']);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('share_with_link', $expectedFields)
|
||||
&& $expectedFields['share_with_link'] === 'URL') {
|
||||
if (array_key_exists('share_with', $expectedFields)) {
|
||||
$expectedFields['share_with_link'] = $this->baseUrl . 'index.php/call/' . $expectedFields['share_with'];
|
||||
} else {
|
||||
$expectedFields['share_with_link'] = 'REGEXP ' . '/\/call\//';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($expectedFields as $field => $value) {
|
||||
$share = json_decode(json_encode($returnedShare), true);
|
||||
if (isset($share[$field]) && empty($share[$field]) && is_array($share[$field])) {
|
||||
// Fix XML parsing fails
|
||||
$share[$field] = '';
|
||||
}
|
||||
|
||||
if (in_array($field, ['path', 'storage_id'], true)
|
||||
&& preg_match('/Transferred from [^ ]* on (\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2})/', $share[$field], $matches)) {
|
||||
// We have to replace strings like
|
||||
// "Transferred from participant1-displayname on 2025-06-30 12-31-32"
|
||||
// with something neutral that works in tests
|
||||
$share[$field] = str_replace($matches[1], '{{DATE AND TIME}}', $share[$field]);
|
||||
}
|
||||
|
||||
$this->assertFieldIsInReturnedShare($field, $value, $share);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Each sharee is specified as "| room name | room test identifier |"; the
|
||||
* name is checked against the returned "label" value, and the room test
|
||||
* identifier is used to get the room token, which is checked against the
|
||||
* returned "shareWith" value. The returned "shareType" value is expected to
|
||||
* always be "IShare::TYPE_ROOM", so there is no need to specify it.
|
||||
*/
|
||||
#[Then('/^"([^"]*)" sharees returned (are|is empty)$/')]
|
||||
public function shareesReturnedAreIsEmpty(string $shareeType, string $isEmpty, ?TableNode $shareesList = null): void {
|
||||
if ($isEmpty !== 'is empty') {
|
||||
$sharees = [];
|
||||
foreach ($shareesList->getRows() as $row) {
|
||||
$expectedSharee = [$row[0]];
|
||||
$expectedSharee[] = 10; // IShare::TYPE_ROOM
|
||||
$expectedSharee[] = FeatureContext::getTokenForIdentifier($row[1]);
|
||||
$sharees[] = $expectedSharee;
|
||||
}
|
||||
$respondedArray = $this->getArrayOfShareesResponded($this->getXmlResponse(), $shareeType);
|
||||
usort($sharees, function ($a, $b) {
|
||||
return $a[2] <=> $b[2]; // Sort by token
|
||||
});
|
||||
usort($respondedArray, function ($a, $b) {
|
||||
return $a[2] <=> $b[2]; // Sort by token
|
||||
});
|
||||
\PHPUnit\Framework\Assert::assertEquals($sharees, $respondedArray);
|
||||
} else {
|
||||
$respondedArray = $this->getArrayOfShareesResponded($this->getXmlResponse(), $shareeType);
|
||||
\PHPUnit\Framework\Assert::assertEmpty($respondedArray);
|
||||
}
|
||||
}
|
||||
|
||||
#[Then('the list of returned files for :user is')]
|
||||
public function theListOfReturnedFilesForIs(string $user, ?TableNode $table = null): void {
|
||||
$xmlResponse = $this->getXmlResponse();
|
||||
$xmlResponse->registerXPathNamespace('d', 'DAV:');
|
||||
|
||||
$hrefs = [];
|
||||
foreach ($xmlResponse->xpath('//d:response/d:href') as $href) {
|
||||
$hrefs[] = (string)$href;
|
||||
}
|
||||
|
||||
$expectedHrefs = [];
|
||||
if ($table !== null) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
$expectedHrefs[] = '/remote.php/dav/files/' . $user . (string)$row[0];
|
||||
}
|
||||
}
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals($expectedHrefs, $hrefs);
|
||||
}
|
||||
|
||||
#[Then('the response contains a share-types DAV property with')]
|
||||
public function theResponseContainsAShareTypesDavPropertyWith(?TableNode $table = null): void {
|
||||
$xmlResponse = $this->getXmlResponse();
|
||||
$xmlResponse->registerXPathNamespace('oc', 'http://owncloud.org/ns');
|
||||
|
||||
$shareTypes = [];
|
||||
foreach ($xmlResponse->xpath('//oc:share-types/oc:share-type') as $shareType) {
|
||||
$shareTypes[] = (int)$shareType;
|
||||
}
|
||||
|
||||
$expectedShareTypes = [];
|
||||
if ($table !== null) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
$expectedShareTypes[] = (int)$row[0];
|
||||
}
|
||||
}
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals($expectedShareTypes, $shareTypes);
|
||||
}
|
||||
|
||||
#[Then('the response contains a share-types file property for :path with')]
|
||||
public function theResponseContainsAShareTypesFilesPropertyForWith(string $path, ?TableNode $table = null): void {
|
||||
if ($this->responseBody === null) {
|
||||
$this->responseBody = $this->response->getBody()->getContents();
|
||||
}
|
||||
$response = json_decode($this->responseBody);
|
||||
|
||||
$fileForPath = array_filter($response->files, function ($file) use ($path) {
|
||||
$filePath = $file->path . (substr($file->path, -1) === '/'? '': '/');
|
||||
return ($filePath . $file->name) === $path;
|
||||
});
|
||||
|
||||
if (empty($fileForPath)) {
|
||||
\PHPUnit\Framework\Assert::fail("$path not found in the response");
|
||||
}
|
||||
|
||||
$fileForPath = array_shift($fileForPath);
|
||||
|
||||
$shareTypes = [];
|
||||
if (property_exists($fileForPath, 'shareTypes')) {
|
||||
foreach ($fileForPath->shareTypes as $shareType) {
|
||||
$shareTypes[] = (int)$shareType;
|
||||
}
|
||||
}
|
||||
|
||||
$expectedShareTypes = [];
|
||||
if ($table !== null) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
$expectedShareTypes[] = (int)$row[0];
|
||||
}
|
||||
}
|
||||
|
||||
\PHPUnit\Framework\Assert::assertEquals($expectedShareTypes, $shareTypes);
|
||||
}
|
||||
|
||||
private function userSharesWith(string $user, string $path, int $shareType, string $shareWith, ?TableNode $body = null): void {
|
||||
$this->currentUser = $user;
|
||||
|
||||
$url = '/apps/files_sharing/api/v1/shares';
|
||||
|
||||
$parameters = [];
|
||||
$parameters[] = 'path=' . $path;
|
||||
$parameters[] = 'shareType=' . $shareType;
|
||||
$parameters[] = 'shareWith=' . $shareWith;
|
||||
|
||||
$talkMetaData = [];
|
||||
|
||||
if ($body instanceof TableNode) {
|
||||
foreach ($body->getRowsHash() as $key => $value) {
|
||||
if ($key === 'expireDate' && $value !== 'invalid date') {
|
||||
$value = date('Y-m-d', strtotime($value));
|
||||
}
|
||||
if ($key === 'talkMetaData.replyTo' || $key === 'talkMetaData.threadId') {
|
||||
$value = FeatureContext::getMessageIdForText($value);
|
||||
}
|
||||
if (str_starts_with($key, 'talkMetaData.')) {
|
||||
$talkMetaData[substr($key, 13)] = $value;
|
||||
} else {
|
||||
$parameters[] = $key . '=' . $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($talkMetaData)) {
|
||||
$parameters[] = 'talkMetaData=' . json_encode($talkMetaData);
|
||||
}
|
||||
|
||||
$url .= '?' . implode('&', $parameters);
|
||||
|
||||
$this->sendingTo('POST', $url);
|
||||
|
||||
$this->lastCreatedShareData = $this->getXmlResponse();
|
||||
}
|
||||
|
||||
private function sendingTo(string $verb, string $url, ?TableNode $body = null): void {
|
||||
$fullUrl = $this->baseUrl . 'ocs/v1.php' . $url;
|
||||
$client = new Client();
|
||||
$options = [];
|
||||
if ($this->currentUser === 'admin') {
|
||||
$options['auth'] = $this->adminUser;
|
||||
} else {
|
||||
$options['auth'] = [$this->currentUser, $this->regularUserPassword];
|
||||
}
|
||||
$options['headers'] = [
|
||||
'OCS_APIREQUEST' => 'true'
|
||||
];
|
||||
if ($body instanceof TableNode) {
|
||||
$fd = $body->getRowsHash();
|
||||
if (array_key_exists('expireDate', $fd)) {
|
||||
$fd['expireDate'] = date('Y-m-d', strtotime($fd['expireDate']));
|
||||
}
|
||||
$options['form_params'] = $fd;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->response = $client->request($verb, $fullUrl, $options);
|
||||
$this->responseBody = null;
|
||||
} catch (GuzzleHttp\Exception\ClientException $ex) {
|
||||
$this->response = $ex->getResponse();
|
||||
$this->responseBody = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendingToDav(string $verb, string $url, ?array $headers = null, ?string $body = null): void {
|
||||
$fullUrl = $this->baseUrl . 'remote.php/dav/files' . $url;
|
||||
$client = new Client();
|
||||
$options = [];
|
||||
if ($this->currentUser === 'admin') {
|
||||
$options['auth'] = $this->adminUser;
|
||||
} else {
|
||||
$options['auth'] = [$this->currentUser, $this->regularUserPassword];
|
||||
}
|
||||
$options['headers'] = [
|
||||
'OCS_APIREQUEST' => 'true'
|
||||
];
|
||||
if ($headers !== null) {
|
||||
$options['headers'] = array_merge($options['headers'], $headers);
|
||||
}
|
||||
if ($body !== null) {
|
||||
$options['body'] = $body;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->response = $client->request($verb, $fullUrl, $options);
|
||||
$this->responseBody = null;
|
||||
} catch (GuzzleHttp\Exception\ClientException $ex) {
|
||||
$this->response = $ex->getResponse();
|
||||
$this->responseBody = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendingToWithRequestToken(string $verb, string $url, string $requestToken, CookieJar $cookieJar): void {
|
||||
$fullUrl = $this->baseUrl . $url;
|
||||
|
||||
$client = new Client();
|
||||
try {
|
||||
$this->response = $client->request(
|
||||
$verb,
|
||||
$fullUrl,
|
||||
[
|
||||
'cookies' => $cookieJar,
|
||||
'headers' => [
|
||||
'requesttoken' => $requestToken
|
||||
]
|
||||
]
|
||||
);
|
||||
$this->responseBody = null;
|
||||
} catch (GuzzleHttp\Exception\ClientException $e) {
|
||||
$this->response = $e->getResponse();
|
||||
$this->responseBody = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function extractRequestTokenFromResponse(ResponseInterface $response): string {
|
||||
return substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89);
|
||||
}
|
||||
|
||||
private function loggingInUsingWebAs(string $user): array {
|
||||
$loginUrl = $this->baseUrl . '/login';
|
||||
|
||||
$cookieJar = new CookieJar();
|
||||
|
||||
// Request a new session and extract CSRF token
|
||||
$client = new Client();
|
||||
$response = $client->get(
|
||||
$loginUrl,
|
||||
[
|
||||
'cookies' => $cookieJar,
|
||||
]
|
||||
);
|
||||
$requestToken = $this->extractRequestTokenFromResponse($response);
|
||||
|
||||
// Login and extract new token
|
||||
$password = ($user === 'admin') ? $this->adminUser[1] : $this->regularUserPassword;
|
||||
$client = new Client();
|
||||
$response = $client->post(
|
||||
$loginUrl,
|
||||
[
|
||||
'form_params' => [
|
||||
'user' => $user,
|
||||
'password' => $password,
|
||||
'requesttoken' => $requestToken,
|
||||
],
|
||||
'cookies' => $cookieJar,
|
||||
'headers' => [
|
||||
'Origin' => $this->baseUrl,
|
||||
],
|
||||
]
|
||||
);
|
||||
$requestToken = $this->extractRequestTokenFromResponse($response);
|
||||
|
||||
return [$requestToken, $cookieJar];
|
||||
}
|
||||
|
||||
public function getLastShareToken(): string {
|
||||
return (string)$this->lastCreatedShareData->data[0]->token;
|
||||
}
|
||||
|
||||
private function getLastShareId(): string {
|
||||
return (string)$this->lastCreatedShareData->data[0]->id;
|
||||
}
|
||||
|
||||
private function getXmlResponse(): \SimpleXMLElement|false {
|
||||
if ($this->responseBody === null) {
|
||||
$this->responseBody = $this->response->getBody()->getContents();
|
||||
}
|
||||
return simplexml_load_string($this->responseBody);
|
||||
}
|
||||
|
||||
private function assertFieldIsInReturnedShare(string $field, string $contentExpected, array $returnedShare): void {
|
||||
if ($contentExpected === 'IGNORE') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!array_key_exists($field, $returnedShare)) {
|
||||
\PHPUnit\Framework\Assert::fail("$field was not found in response");
|
||||
}
|
||||
|
||||
if ($field === 'expiration' && !empty($contentExpected)) {
|
||||
$contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00';
|
||||
}
|
||||
|
||||
if ($contentExpected === 'A_NUMBER') {
|
||||
\PHPUnit\Framework\Assert::assertTrue(is_numeric((string)$returnedShare[$field]), "Field '$field' is not a number: " . $returnedShare[$field]);
|
||||
} elseif ($contentExpected === 'A_TOKEN') {
|
||||
// A token is composed by 15 characters from
|
||||
// ISecureRandom::CHAR_HUMAN_READABLE.
|
||||
\PHPUnit\Framework\Assert::assertMatchesRegularExpression('/^[abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789]{15}$/', (string)$returnedShare[$field], "Field '$field' is not a token");
|
||||
} elseif (strpos($contentExpected, 'REGEXP ') === 0) {
|
||||
\PHPUnit\Framework\Assert::assertMatchesRegularExpression(substr($contentExpected, strlen('REGEXP ')), (string)$returnedShare[$field], "Field '$field' does not match");
|
||||
} else {
|
||||
\PHPUnit\Framework\Assert::assertEquals($contentExpected, (string)$returnedShare[$field], "Field '$field' does not match");
|
||||
}
|
||||
}
|
||||
|
||||
private function getArrayOfShareesResponded(\SimpleXMLElement $response, string $shareeType): array {
|
||||
$elements = $response->data;
|
||||
$elements = json_decode(json_encode($elements), true);
|
||||
if (strpos($shareeType, 'exact ') === 0) {
|
||||
$elements = $elements['exact'];
|
||||
$shareeType = substr($shareeType, 6);
|
||||
}
|
||||
|
||||
// "simplexml_load_string" creates a SimpleXMLElement object for each
|
||||
// XML element with child elements. In turn, each child is indexed by
|
||||
// its tag in the SimpleXMLElement object. However, when there are
|
||||
// several child XML elements with the same tag, an array with all the
|
||||
// children with the same tag is indexed instead. Therefore, when the
|
||||
// XML contains
|
||||
// <rooms>
|
||||
// <element>
|
||||
// <label>...</label>
|
||||
// <value>...</value>
|
||||
// </element>
|
||||
// </rooms>
|
||||
// the "$elements[$shareeType]" variable contains an "element" key which
|
||||
// in turn contains "label" and "value" keys, but when the XML contains
|
||||
// <rooms>
|
||||
// <element>
|
||||
// <label>...</label>
|
||||
// <value>...</value>
|
||||
// </element>
|
||||
// <element>
|
||||
// <label>...</label>
|
||||
// <value>...</value>
|
||||
// </element>
|
||||
// </rooms>
|
||||
// the "$elements[$shareeType]" variable contains an "element" key which
|
||||
// in turn contains "0" and "1" keys, and in turn each one contains
|
||||
// "label" and "value" keys.
|
||||
$elements = $elements[$shareeType];
|
||||
if (array_key_exists('element', $elements) && is_int(array_keys($elements['element'])[0])) {
|
||||
$elements = $elements['element'];
|
||||
}
|
||||
|
||||
$sharees = [];
|
||||
foreach ($elements as $element) {
|
||||
$sharees[] = [$element['label'], $element['value']['shareType'], $element['value']['shareWith']];
|
||||
}
|
||||
return $sharees;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user