mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.9.x' into feat-insights-module
# Conflicts: # composer.lock # src/Appwrite/Platform/Workers/Migrations.php
This commit is contained in:
@@ -320,6 +320,26 @@ return [
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'codex-plugin',
|
||||
'name' => 'CodexPlugin',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/codex-plugin.git',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'codex-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-codex-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/codex-plugin.git',
|
||||
'gitRepoName' => 'codex-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/codex-plugin/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@
|
||||
"ext-sockets": "*",
|
||||
"appwrite/php-runtimes": "0.20.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.2.*",
|
||||
"utopia-php/abuse": "1.3.*",
|
||||
"utopia-php/agents": "1.2.*",
|
||||
"utopia-php/analytics": "0.15.*",
|
||||
"utopia-php/audit": "2.2.*",
|
||||
|
||||
Generated
+38
-38
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4ef65b015dba97e91f6571b061787653",
|
||||
"content-hash": "d58736fec3028d1f9aedd055e6d82684",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -69,25 +69,25 @@
|
||||
},
|
||||
{
|
||||
"name": "appwrite/appwrite",
|
||||
"version": "19.1.0",
|
||||
"version": "23.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-for-php.git",
|
||||
"reference": "8738e812062f899c85b2598eef43d6a247f08a56"
|
||||
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56",
|
||||
"reference": "8738e812062f899c85b2598eef43d6a247f08a56",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa",
|
||||
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"php": ">=7.1.0"
|
||||
"php": ">=8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"mockery/mockery": "1.6.12",
|
||||
"phpunit/phpunit": "^10"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -100,14 +100,14 @@
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
|
||||
"description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
|
||||
"support": {
|
||||
"email": "team@appwrite.io",
|
||||
"issues": "https://github.com/appwrite/sdk-for-php/issues",
|
||||
"source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0",
|
||||
"source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0",
|
||||
"url": "https://appwrite.io/support"
|
||||
},
|
||||
"time": "2025-12-18T08:07:43+00:00"
|
||||
"time": "2026-05-08T13:44:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-clamav",
|
||||
@@ -3359,24 +3359,24 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/abuse",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/abuse.git",
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c"
|
||||
"reference": "5d7efbe5c6b0cf7d06003114fd86e24ba785582f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/5d7efbe5c6b0cf7d06003114fd86e24ba785582f",
|
||||
"reference": "5d7efbe5c6b0cf7d06003114fd86e24ba785582f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"appwrite/appwrite": "19.*",
|
||||
"appwrite/appwrite": "23.*",
|
||||
"ext-curl": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-redis": "*",
|
||||
"php": ">=8.0",
|
||||
"php": ">=8.2",
|
||||
"utopia-php/database": "5.*"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -3405,9 +3405,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/abuse/issues",
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.3"
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.3.0"
|
||||
},
|
||||
"time": "2026-04-29T11:19:08+00:00"
|
||||
"time": "2026-05-11T08:07:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
@@ -3614,16 +3614,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cache",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/cache.git",
|
||||
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c"
|
||||
"reference": "d36f9050c39c02e09a7763389c9e71258e74af1f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c",
|
||||
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c",
|
||||
"url": "https://api.github.com/repos/utopia-php/cache/zipball/d36f9050c39c02e09a7763389c9e71258e74af1f",
|
||||
"reference": "d36f9050c39c02e09a7763389c9e71258e74af1f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3660,9 +3660,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/cache/issues",
|
||||
"source": "https://github.com/utopia-php/cache/tree/1.0.1"
|
||||
"source": "https://github.com/utopia-php/cache/tree/1.0.2"
|
||||
},
|
||||
"time": "2026-03-12T03:39:09+00:00"
|
||||
"time": "2026-05-08T11:40:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cli",
|
||||
@@ -4541,24 +4541,24 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.10.2",
|
||||
"version": "1.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "211d01b90ccab9729029151c6c61f543bd755c2e"
|
||||
"reference": "0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/211d01b90ccab9729029151c6c61f543bd755c2e",
|
||||
"reference": "211d01b90ccab9729029151c6c61f543bd755c2e",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1",
|
||||
"reference": "0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"appwrite/appwrite": "19.*",
|
||||
"appwrite/appwrite": "23.*",
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*",
|
||||
"halaxa/json-machine": "^1.2",
|
||||
"php": ">=8.1",
|
||||
"php": ">=8.2",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/dsn": "0.2.*",
|
||||
"utopia-php/storage": "2.*"
|
||||
@@ -4590,9 +4590,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.10.2"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.11.0"
|
||||
},
|
||||
"time": "2026-05-08T06:25:47+00:00"
|
||||
"time": "2026-05-11T08:13:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -5476,16 +5476,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.27.5",
|
||||
"version": "1.28.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "9faa38b48d422f3da764a719712905c83b3922cb"
|
||||
"reference": "009118ccda8ccece2b9fc043c158cb1dd3efaa88"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9faa38b48d422f3da764a719712905c83b3922cb",
|
||||
"reference": "9faa38b48d422f3da764a719712905c83b3922cb",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/009118ccda8ccece2b9fc043c158cb1dd3efaa88",
|
||||
"reference": "009118ccda8ccece2b9fc043c158cb1dd3efaa88",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5521,9 +5521,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.27.5"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.28.1"
|
||||
},
|
||||
"time": "2026-05-05T12:09:40+00:00"
|
||||
"time": "2026-05-08T13:24:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
|
||||
@@ -55,7 +55,7 @@ class Google extends OAuth2
|
||||
'state' => \json_encode($this->state),
|
||||
'response_type' => 'code',
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent'
|
||||
'prompt' => $this->getPrompt()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ class Google extends OAuth2
|
||||
'https://oauth2.googleapis.com/token?' . \http_build_query([
|
||||
'code' => $code,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
'client_secret' => $this->getClientSecret(),
|
||||
'redirect_uri' => $this->callback,
|
||||
'scope' => null,
|
||||
'grant_type' => 'authorization_code'
|
||||
@@ -95,7 +95,7 @@ class Google extends OAuth2
|
||||
'https://oauth2.googleapis.com/token?' . \http_build_query([
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
'client_secret' => $this->getClientSecret(),
|
||||
'grant_type' => 'refresh_token'
|
||||
])
|
||||
), true);
|
||||
@@ -177,4 +177,54 @@ class Google extends OAuth2
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the Client Secret from the JSON stored in appSecret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getClientSecret(): string
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
|
||||
return $secret['clientSecret'] ?? $this->appSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the prompt values from the JSON stored in appSecret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getPrompt(): string
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
$prompt = $secret['prompt'] ?? [];
|
||||
|
||||
if (empty($prompt)) {
|
||||
$prompt = ['consent'];
|
||||
}
|
||||
|
||||
return \implode(' ', $prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON stored in appSecret.
|
||||
* Falls back to treating the raw string as the client secret for backwards compatibility.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getAppSecret(): array
|
||||
{
|
||||
try {
|
||||
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Throwable $th) {
|
||||
return ['clientSecret' => $this->appSecret];
|
||||
}
|
||||
|
||||
if (!\is_array($secret)) {
|
||||
return ['clientSecret' => $this->appSecret];
|
||||
}
|
||||
|
||||
return $secret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ class Create extends Action
|
||||
{
|
||||
// TODO: use database worker for for creating the v2 schema if not present
|
||||
// it is considered that the v2 metadata schema is already created during server start in the http.php
|
||||
return self::constructDatabaseDSNFromProjectDatabase($this->getDatabaseType(), $project->getAttribute('region'), $project->getAttribute('database'));
|
||||
return $this->constructDatabaseDSNFromProjectDatabase($this->getDatabaseType(), $project->getAttribute('region'), $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
public static function constructDatabaseDSNFromProjectDatabase(string $databasetype, $region, ?string $dsn = null): string
|
||||
private function constructDatabaseDSNFromProjectDatabase(string $databasetype, $region, ?string $dsn = null): string
|
||||
{
|
||||
$databases = [];
|
||||
$databaseKeys = [];
|
||||
|
||||
@@ -3,8 +3,22 @@
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google;
|
||||
|
||||
use Appwrite\Auth\OAuth2\Google;
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Base
|
||||
{
|
||||
@@ -52,4 +66,118 @@ class Update extends Base
|
||||
{
|
||||
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
|
||||
}
|
||||
|
||||
public static function getParameters(): array
|
||||
{
|
||||
return \array_merge(parent::getParameters(), [
|
||||
[
|
||||
'$id' => 'prompt',
|
||||
'name' => 'Prompt',
|
||||
'example' => '["consent"]',
|
||||
'hint' => '',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$providerId = static::getProviderId();
|
||||
$providerLabel = static::getProviderLabel();
|
||||
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/oauth2/' . $providerId)
|
||||
->desc('Update project OAuth2 ' . $providerLabel)
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'oauth2.write')
|
||||
->label('event', 'oauth2.[providerId].update')
|
||||
->label('audits.event', 'project.oauth2.[providerId].update')
|
||||
->label('audits.resource', 'project.oauth2/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'oauth2',
|
||||
name: static::getProviderSDKMethod(),
|
||||
description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: static::getResponseModel(),
|
||||
)
|
||||
],
|
||||
))
|
||||
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
|
||||
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
|
||||
->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function buildReadResponse(Document $project): Document
|
||||
{
|
||||
$providerId = static::getProviderId();
|
||||
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
|
||||
$decoded = $this->decodeStoredSecret($project);
|
||||
|
||||
return new Document([
|
||||
'$id' => $providerId,
|
||||
'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false,
|
||||
static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '',
|
||||
static::getClientSecretParamName() => '',
|
||||
'prompt' => $decoded['prompt'] ?? ['consent'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom callback used instead of the parent's `action()` because Google
|
||||
* takes an additional optional `prompt` parameter. The method is named
|
||||
* differently to avoid an LSP-incompatible override of Base::action().
|
||||
*/
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
?array $prompt,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
QueueEvent $queueForEvents
|
||||
): void {
|
||||
$providerId = static::getProviderId();
|
||||
$queueForEvents->setParam('providerId', $providerId);
|
||||
|
||||
if ($prompt !== null) {
|
||||
if (empty($prompt)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Prompt array cannot be empty.');
|
||||
}
|
||||
|
||||
if (\in_array('none', $prompt) && \count($prompt) > 1) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When "none" is used as a prompt value, it must be the only element in the array.');
|
||||
}
|
||||
}
|
||||
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = $this->decodeStoredSecret($project);
|
||||
|
||||
// Backwards compatibility: secrets stored before the prompt feature
|
||||
// were saved as plain strings. Treat the raw value as clientSecret.
|
||||
if (!empty($storedRaw) && empty($existing)) {
|
||||
$existing = ['clientSecret' => $storedRaw];
|
||||
}
|
||||
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'prompt' => $prompt ?? ($existing['prompt'] ?? ['consent']),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
$response->dynamic($this->buildReadResponse($project), static::getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\SDK\Language\Android;
|
||||
use Appwrite\SDK\Language\Apple;
|
||||
use Appwrite\SDK\Language\ClaudePlugin;
|
||||
use Appwrite\SDK\Language\CLI;
|
||||
use Appwrite\SDK\Language\CodexPlugin;
|
||||
use Appwrite\SDK\Language\CursorPlugin;
|
||||
use Appwrite\SDK\Language\Dart;
|
||||
use Appwrite\SDK\Language\Deno;
|
||||
@@ -455,6 +456,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
case 'claude-plugin':
|
||||
$config = new ClaudePlugin();
|
||||
break;
|
||||
case 'codex-plugin':
|
||||
$config = new CodexPlugin();
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Language "' . $language['key'] . '" not supported');
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Create as DatabaseCreate;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Usage\Context;
|
||||
use Utopia\Compression\Compression;
|
||||
@@ -285,8 +284,6 @@ class Migrations extends Action
|
||||
$options = $migration->getAttribute('options', []);
|
||||
$credentials = $migration->getAttribute('credentials');
|
||||
|
||||
$destinationProject = $this->project;
|
||||
|
||||
return match ($destination) {
|
||||
DestinationAppwrite::getName() => new DestinationAppwrite(
|
||||
$this->project->getId(),
|
||||
@@ -296,11 +293,7 @@ class Migrations extends Action
|
||||
$this->getDatabasesDB,
|
||||
Config::getParam('collections', [])['databases']['collections'],
|
||||
OnDuplicate::tryFrom($options['onDuplicate'] ?? '') ?? OnDuplicate::Fail,
|
||||
fn (ResourceDatabase $resource): string => DatabaseCreate::constructDatabaseDSNFromProjectDatabase(
|
||||
$resource->getType() ?: 'legacy',
|
||||
$destinationProject->getAttribute('region'),
|
||||
$destinationProject->getAttribute('database'),
|
||||
),
|
||||
$this->resolveDestinationDatabaseDsn(...),
|
||||
),
|
||||
DestinationCSV::getName() => new DestinationCSV(
|
||||
$this->deviceForFiles,
|
||||
@@ -324,6 +317,19 @@ class Migrations extends Action
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy / tablesdb databases route to the destination project's DSN (same as a fresh
|
||||
* Databases create), while documentsdb / vectorsdb keep the source DSN — the dedicated-DB
|
||||
* backfill that would re-point them is not run during migrations.
|
||||
*/
|
||||
private function resolveDestinationDatabaseDsn(ResourceDatabase $resource): string
|
||||
{
|
||||
return match ($resource->getType()) {
|
||||
DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB => (string) $resource->getDatabase(),
|
||||
default => (string) $this->project->getAttribute('database', ''),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
* @throws Structure
|
||||
|
||||
@@ -25,6 +25,20 @@ class OAuth2Google extends OAuth2Base
|
||||
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->addRule('prompt', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Google OAuth2 prompt values.',
|
||||
'default' => ['consent'],
|
||||
'example' => ['consent'],
|
||||
'array' => true,
|
||||
'enum' => ['none', 'consent', 'select_account'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
|
||||
@@ -50,6 +50,8 @@ class Comment
|
||||
|
||||
protected string $statePrefix = '[appwrite]: #';
|
||||
|
||||
protected ?string $tip = null;
|
||||
|
||||
/**
|
||||
* @var mixed[] $builds
|
||||
*/
|
||||
@@ -81,7 +83,14 @@ class Comment
|
||||
|
||||
public function generateComment(): string
|
||||
{
|
||||
$json = \json_encode($this->builds);
|
||||
if ($this->tip === null) {
|
||||
$this->tip = $this->tips[\array_rand($this->tips)];
|
||||
}
|
||||
|
||||
$json = \json_encode([
|
||||
'builds' => $this->builds,
|
||||
'tip' => $this->tip,
|
||||
]);
|
||||
|
||||
$text = $this->statePrefix . \base64_encode($json) . "\n\n";
|
||||
|
||||
@@ -226,8 +235,7 @@ class Comment
|
||||
$i++;
|
||||
}
|
||||
|
||||
$tip = $this->tips[array_rand($this->tips)];
|
||||
$text .= "\n<br>\n\n> [!TIP]\n> $tip\n\n";
|
||||
$text .= "\n<br>\n\n> [!TIP]\n> {$this->tip}\n\n";
|
||||
|
||||
return $text;
|
||||
}
|
||||
@@ -252,8 +260,15 @@ class Comment
|
||||
|
||||
$json = \base64_decode($state);
|
||||
|
||||
$builds = \json_decode($json, true);
|
||||
$this->builds = \is_array($builds) ? $builds : [];
|
||||
$data = \json_decode($json, true);
|
||||
|
||||
if (\is_array($data) && \array_key_exists('builds', $data)) {
|
||||
$this->builds = \is_array($data['builds']) ? $data['builds'] : [];
|
||||
$this->tip = $data['tip'] ?? null;
|
||||
} else {
|
||||
// Backward compatibility with old state format (builds array only)
|
||||
$this->builds = \is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Tests\E2E\Scopes\SideConsole;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class DatabasesConsoleClientTest extends Scope
|
||||
{
|
||||
@@ -258,55 +257,4 @@ class DatabasesConsoleClientTest extends Scope
|
||||
$this->assertIsArray($response['body']['documents']);
|
||||
}
|
||||
|
||||
#[Depends('testCreateCollection')]
|
||||
public function testGetCollectionLogs(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::limit(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::offset(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Tests\E2E\Scopes\SideConsole;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class VectorsDBConsoleClientTest extends Scope
|
||||
{
|
||||
@@ -258,55 +257,4 @@ class VectorsDBConsoleClientTest extends Scope
|
||||
$this->assertIsArray($response['body']['documents']);
|
||||
}
|
||||
|
||||
#[Depends('testCreateCollection')]
|
||||
public function testGetCollectionLogs(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::limit(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::offset(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2564,6 +2564,159 @@ trait OAuth2Base
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Google (clientId + clientSecret + optional prompt)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Default prompt MUST run before any other Google test that sets a custom
|
||||
* prompt value. The global resetProjectOAuth2() only clears Amazon state,
|
||||
* so Google state leaks across tests in the same class. Running this first
|
||||
* guarantees the stored JSON blob has no pre-existing "prompt" key.
|
||||
*/
|
||||
public function testUpdateOAuth2GoogleDefaultPrompt(): void
|
||||
{
|
||||
// When prompt is omitted and nothing is stored, the default is ['consent'].
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'google-default-client',
|
||||
'clientSecret' => 'google-default-secret',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(['consent'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2Google(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
|
||||
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
|
||||
'prompt' => ['select_account'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('google', $response['body']['$id']);
|
||||
$this->assertSame('120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com', $response['body']['clientId']);
|
||||
$this->assertSame(['select_account'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePartialPreservesPrompt(): void
|
||||
{
|
||||
// Seed clientSecret + prompt.
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => 'google-seed-client',
|
||||
'clientSecret' => 'google-seed-secret',
|
||||
'prompt' => ['consent', 'select_account'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
// Update only clientId.
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'google-rotated-client',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('google-rotated-client', $response['body']['clientId']);
|
||||
$this->assertSame(['consent', 'select_account'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePromptNoneAloneRejected(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'prompt' => ['none', 'consent'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePromptEmptyArrayRejected(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'prompt' => [],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePromptNoneAloneAccepted(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
|
||||
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
|
||||
'prompt' => ['none'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(['none'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GoogleEnableAndReadBack(): void
|
||||
{
|
||||
$update = $this->updateOAuth2('google', [
|
||||
'clientId' => 'google-enable-client',
|
||||
'clientSecret' => 'google-enable-secret',
|
||||
'prompt' => ['select_account'],
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
$this->assertTrue($update['body']['enabled']);
|
||||
|
||||
// GET must hide clientSecret while keeping clientId and prompt.
|
||||
$get = $this->getOAuth2Provider('google');
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['enabled']);
|
||||
$this->assertSame('google-enable-client', $get['body']['clientId']);
|
||||
$this->assertSame(['select_account'], $get['body']['prompt']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Smoke test: every plain (clientId + clientSecret) provider
|
||||
//
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Vcs;
|
||||
|
||||
use Appwrite\Vcs\Comment;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class CommentTest extends TestCase
|
||||
{
|
||||
public function testTipIsPreservedAcrossMultipleGenerations(): void
|
||||
{
|
||||
$comment = new Comment(['consoleHostname' => 'localhost']);
|
||||
$comment->addBuild(
|
||||
new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
|
||||
new Document(['$id' => 'func1', 'name' => 'Test Function']),
|
||||
'function',
|
||||
'ready',
|
||||
'dep1',
|
||||
['type' => 'logs'],
|
||||
''
|
||||
);
|
||||
|
||||
$first = $comment->generateComment();
|
||||
$firstTip = $this->extractTip($first);
|
||||
|
||||
$this->assertNotNull($firstTip);
|
||||
$this->assertNotEmpty($firstTip);
|
||||
|
||||
$second = $comment->generateComment();
|
||||
$secondTip = $this->extractTip($second);
|
||||
|
||||
$this->assertEquals($firstTip, $secondTip);
|
||||
}
|
||||
|
||||
public function testTipIsRestoredFromParsedComment(): void
|
||||
{
|
||||
$comment = new Comment(['consoleHostname' => 'localhost']);
|
||||
$comment->addBuild(
|
||||
new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
|
||||
new Document(['$id' => 'func1', 'name' => 'Test Function']),
|
||||
'function',
|
||||
'ready',
|
||||
'dep1',
|
||||
['type' => 'logs'],
|
||||
''
|
||||
);
|
||||
|
||||
$original = $comment->generateComment();
|
||||
$originalTip = $this->extractTip($original);
|
||||
|
||||
$parsed = new Comment(['consoleHostname' => 'localhost']);
|
||||
$parsed->parseComment($original);
|
||||
$parsed->addBuild(
|
||||
new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
|
||||
new Document(['$id' => 'func2', 'name' => 'Another Function']),
|
||||
'function',
|
||||
'building',
|
||||
'dep2',
|
||||
['type' => 'logs'],
|
||||
''
|
||||
);
|
||||
|
||||
$regenerated = $parsed->generateComment();
|
||||
$regeneratedTip = $this->extractTip($regenerated);
|
||||
|
||||
$this->assertEquals($originalTip, $regeneratedTip);
|
||||
}
|
||||
|
||||
public function testBackwardCompatibilityWithOldStateFormat(): void
|
||||
{
|
||||
$oldBuilds = [
|
||||
'project1_func1' => [
|
||||
'projectName' => 'Test Project',
|
||||
'projectId' => 'project1',
|
||||
'region' => 'default',
|
||||
'resourceName' => 'Test Function',
|
||||
'resourceId' => 'func1',
|
||||
'resourceType' => 'function',
|
||||
'buildStatus' => 'ready',
|
||||
'deploymentId' => 'dep1',
|
||||
'action' => ['type' => 'logs'],
|
||||
'previewUrl' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$oldState = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n";
|
||||
$oldState .= "> [!TIP]\n> Old tip that should be ignored\n\n";
|
||||
|
||||
$comment = new Comment(['consoleHostname' => 'localhost']);
|
||||
$comment->parseComment($oldState);
|
||||
|
||||
$new = $comment->generateComment();
|
||||
$newTip = $this->extractTip($new);
|
||||
|
||||
$this->assertNotNull($newTip);
|
||||
$this->assertNotEquals('Old tip that should be ignored', $newTip);
|
||||
$this->assertContains($newTip, $this->getTips());
|
||||
}
|
||||
|
||||
public function testParseOldStateFormatWithOnlyBuilds(): void
|
||||
{
|
||||
$oldBuilds = [
|
||||
'project1_func1' => [
|
||||
'projectName' => 'Test Project',
|
||||
'projectId' => 'project1',
|
||||
'region' => 'default',
|
||||
'resourceName' => 'Test Function',
|
||||
'resourceId' => 'func1',
|
||||
'resourceType' => 'function',
|
||||
'buildStatus' => 'ready',
|
||||
'deploymentId' => 'dep1',
|
||||
'action' => ['type' => 'logs'],
|
||||
'previewUrl' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$state = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n";
|
||||
|
||||
$comment = new Comment(['consoleHostname' => 'localhost']);
|
||||
$comment->parseComment($state);
|
||||
|
||||
$this->assertEquals(false, $comment->isEmpty());
|
||||
|
||||
$first = $comment->generateComment();
|
||||
$firstTip = $this->extractTip($first);
|
||||
|
||||
$this->assertNotNull($firstTip);
|
||||
$this->assertNotEmpty($firstTip);
|
||||
$this->assertContains($firstTip, $this->getTips());
|
||||
|
||||
$second = $comment->generateComment();
|
||||
$secondTip = $this->extractTip($second);
|
||||
|
||||
$this->assertEquals($firstTip, $secondTip);
|
||||
}
|
||||
|
||||
private function extractTip(string $comment): ?string
|
||||
{
|
||||
if (\preg_match('/> \[!TIP\]\n> (.+)/', $comment, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getTips(): array
|
||||
{
|
||||
$reflection = new \ReflectionClass(Comment::class);
|
||||
$property = $reflection->getProperty('tips');
|
||||
|
||||
return $property->getValue(new Comment(['consoleHostname' => 'localhost']));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user