1
0
Fork 0
mirror of synced 2024-06-28 03:01:15 +12:00

Merge branch 'swoole-and-functions' of github.com:appwrite/appwrite into swoole-and-functions

This commit is contained in:
Eldad Fux 2020-09-11 08:57:14 +03:00
commit 4438be0627
14 changed files with 303 additions and 186 deletions

View file

@ -32,6 +32,7 @@
- API Key name max length is now 128 chars and not 256 for better API consistency
- Task name max length is now 128 chars and not 256 for better API consistency
- Platform name max length is now 128 chars and not 256 for better API consistency
- Webhooks payloads are now exactly the same as any of the API response objects
- New and consistent response format for all API object + new response examples in the docs
- Removed user roles attribute from user object (can be fetched from /v1/teams/memberships) **
- Removed type attribute from session object response (used only internally)

View file

@ -41,7 +41,7 @@ App::post('/v1/account')
->param('email', '', function () { return new Email(); }, 'User email.')
->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.')
->param('name', '', function () { return new Text(128); }, 'User name. Max length: 128 chars.', true)
->action(function ($email, $password, $name, $request, $response, $project, $projectDB, $webhooks, $audits) use ($oauth2Keys) {
->action(function ($email, $password, $name, $request, $response, $project, $projectDB, $webhooks, $audits) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $project */
@ -107,13 +107,6 @@ App::post('/v1/account')
throw new Exception('Failed saving user to DB', 500);
}
$webhooks
->setParam('payload', [
'name' => $name,
'email' => $email,
])
;
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.create')
@ -237,14 +230,7 @@ App::post('/v1/account/sessions')
if (false === $profile) {
throw new Exception('Failed saving user to DB', 500);
}
$webhooks
->setParam('payload', [
'name' => $profile->getAttribute('name', ''),
'email' => $profile->getAttribute('email', ''),
])
;
$audits
->setParam('userId', $profile->getId())
->setParam('event', 'account.sessions.create')
@ -990,10 +976,7 @@ App::delete('/v1/account')
;
$webhooks
->setParam('payload', [
'name' => $user->getAttribute('name', ''),
'email' => $user->getAttribute('email', ''),
])
->setParam('payload', $response->output($user, Response::MODEL_USER))
;
if (!Config::getParam('domainVerification')) {
@ -1048,10 +1031,7 @@ App::delete('/v1/account/sessions/:sessionId')
;
$webhooks
->setParam('payload', [
'name' => $user->getAttribute('name', ''),
'email' => $user->getAttribute('email', ''),
])
->setParam('payload', $response->output($user, Response::MODEL_USER))
;
if (!Config::getParam('domainVerification')) {
@ -1105,12 +1085,9 @@ App::delete('/v1/account/sessions')
->setParam('event', 'account.sessions.delete')
->setParam('resource', '/user/'.$user->getId())
;
$webhooks
->setParam('payload', [
'name' => $user->getAttribute('name', ''),
'email' => $user->getAttribute('email', ''),
])
->setParam('payload', $response->output($user, Response::MODEL_USER))
;
if (!Config::getParam('domainVerification')) {

View file

@ -2,7 +2,6 @@
use Utopia\App;
use Utopia\Exception;
use Utopia\Response;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
@ -20,6 +19,7 @@ use Appwrite\Database\Validator\Collection;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Exception\Authorization as AuthorizationException;
use Appwrite\Database\Exception\Structure as StructureException;
use Appwrite\Utopia\Response;
App::post('/v1/database/collections')
->desc('Create Collection')
@ -77,22 +77,14 @@ App::post('/v1/database/collections')
throw new Exception('Failed saving collection to DB', 500);
}
$data = $data->getArrayCopy();
$webhooks
->setParam('payload', $data)
;
$audits
->setParam('event', 'database.collections.create')
->setParam('resource', 'database/collection/'.$data['$id'])
->setParam('data', $data)
->setParam('resource', 'database/collection/'.$data->getId())
->setParam('data', $data->getArrayCopy())
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->json($data)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($data, Response::MODEL_COLLECTION);
}, ['response', 'projectDB', 'webhooks', 'audits']);
App::get('/v1/database/collections')
@ -123,7 +115,10 @@ App::get('/v1/database/collections')
],
]);
$response->json(['sum' => $projectDB->getSum(), 'collections' => $results]);
$response->dynamic(new Document([
'sum' => $projectDB->getSum(),
'collections' => $results
]), Response::MODEL_COLLECTION_LIST);
}, ['response', 'projectDB']);
App::get('/v1/database/collections/:collectionId')
@ -145,74 +140,9 @@ App::get('/v1/database/collections/:collectionId')
throw new Exception('Collection not found', 404);
}
$response->json($collection->getArrayCopy());
$response->dynamic($collection, Response::MODEL_COLLECTION);
}, ['response', 'projectDB']);
// App::get('/v1/database/collections/:collectionId/logs')
// ->desc('Get Collection Logs')
// ->groups(['api', 'database'])
// ->label('scope', 'collections.read')
// ->label('sdk.platform', [APP_PLATFORM_SERVER])
// ->label('sdk.namespace', 'database')
// ->label('sdk.method', 'getCollectionLogs')
// ->label('sdk.description', '/docs/references/database/get-collection-logs.md')
// ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.')
// ->action(
// function ($collectionId) use ($response, $register, $projectDB, $project) {
// $collection = $projectDB->getDocument($collectionId, false);
// if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
// throw new Exception('Collection not found', 404);
// }
// $adapter = new AuditAdapter($register->get('db'));
// $adapter->setNamespace('app_'.$project->getId());
// $audit = new Audit($adapter);
// $countries = Locale::getText('countries');
// $logs = $audit->getLogsByResource('database/collection/'.$collection->getId());
// $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb');
// $output = [];
// foreach ($logs as $i => &$log) {
// $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
// $dd = new DeviceDetector($log['userAgent']);
// $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
// $dd->parse();
// $output[$i] = [
// 'event' => $log['event'],
// 'ip' => $log['ip'],
// 'time' => strtotime($log['time']),
// 'OS' => $dd->getOs(),
// 'client' => $dd->getClient(),
// 'device' => $dd->getDevice(),
// 'brand' => $dd->getBrand(),
// 'model' => $dd->getModel(),
// 'geo' => [],
// ];
// try {
// $record = $reader->country($log['ip']);
// $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode);
// $output[$i]['geo']['country'] = $record->country->name;
// $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown');
// } catch (\Exception $e) {
// $output[$i]['geo']['isoCode'] = '--';
// $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown');
// }
// }
// $response->json($output);
// }
// );
App::put('/v1/database/collections/:collectionId')
->desc('Update Collection')
->groups(['api', 'database'])
@ -274,19 +204,13 @@ App::put('/v1/database/collections/:collectionId')
throw new Exception('Failed saving collection to DB', 500);
}
$data = $collection->getArrayCopy();
$webhooks
->setParam('payload', $data)
;
$audits
->setParam('event', 'database.collections.update')
->setParam('resource', 'database/collections/'.$data['$id'])
->setParam('data', $data)
->setParam('resource', 'database/collections/'.$collection->getId())
->setParam('data', $collection->getArrayCopy())
;
$response->json($collection->getArrayCopy());
$response->dynamic($collection, Response::MODEL_COLLECTION);
}, ['response', 'projectDB', 'webhooks', 'audits']);
App::delete('/v1/database/collections/:collectionId')
@ -315,16 +239,14 @@ App::delete('/v1/database/collections/:collectionId')
throw new Exception('Failed to remove collection from DB', 500);
}
$data = $collection->getArrayCopy();
$webhooks
->setParam('payload', $data)
->setParam('payload', $response->output($collection, Response::MODEL_COLLECTION))
;
$audits
->setParam('event', 'database.collections.delete')
->setParam('resource', 'database/collections/'.$data['$id'])
->setParam('data', $data)
->setParam('resource', 'database/collections/'.$collection->getId())
->setParam('data', $collection->getArrayCopy())
;
$response->noContent();
@ -433,22 +355,17 @@ App::post('/v1/database/collections/:collectionId/documents')
throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500);
}
$data = $data->getArrayCopy();
$webhooks
->setParam('payload', $data)
;
$audits
->setParam('event', 'database.documents.create')
->setParam('resource', 'database/document/'.$data['$id'])
->setParam('data', $data)
->setParam('data', $data->getArrayCopy())
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->json($data)
;
$response->dynamic($data, Response::MODEL_ANY);
}, ['response', 'projectDB', 'webhooks', 'audits']);
App::get('/v1/database/collections/:collectionId/documents')
@ -531,27 +448,7 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('No document found', 404);
}
$output = $document->getArrayCopy();
$paths = \explode('/', $request->getParam('q', ''));
$paths = \array_slice($paths, 7, \count($paths));
if (\count($paths) > 0) {
if (\count($paths) % 2 == 1) {
$output = $document->getAttribute(\implode('.', $paths));
} else {
$id = (int) \array_pop($paths);
$output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths)));
}
$output = ($output instanceof Document) ? $output->getArrayCopy() : $output;
if (!\is_array($output)) {
throw new Exception('No document found', 404);
}
}
$response->json($output);
$response->dynamic($document, Response::MODEL_ANY);
}, ['request', 'response', 'projectDB']);
App::patch('/v1/database/collections/:collectionId/documents/:documentId')
@ -620,19 +517,13 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Failed saving document to DB', 500);
}
$data = $data->getArrayCopy();
$webhooks
->setParam('payload', $data)
;
$audits
->setParam('event', 'database.documents.update')
->setParam('resource', 'database/document/'.$data['$id'])
->setParam('data', $data)
->setParam('resource', 'database/document/'.$data->getId())
->setParam('data', $data->getArrayCopy())
;
$response->json($data);
$response->dynamic($data, Response::MODEL_ANY);
}, ['response', 'projectDB', 'webhooks', 'audits']);
App::delete('/v1/database/collections/:collectionId/documents/:documentId')
@ -673,16 +564,14 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Failed to remove document from DB', 500);
}
$data = $document->getArrayCopy();
$webhooks
->setParam('payload', $data)
->setParam('payload', $response->output($document, Response::MODEL_ANY))
;
$audits
->setParam('event', 'database.documents.delete')
->setParam('resource', 'database/document/'.$data['$id'])
->setParam('data', $data) // Audit document in case of malicious or disastrous action
->setParam('resource', 'database/document/'.$document->getId())
->setParam('data', $document->getArrayCopy()) // Audit document in case of malicious or disastrous action
;
$response->noContent();

View file

@ -141,10 +141,6 @@ App::post('/v1/storage/files')
throw new Exception('Failed saving file to DB', 500);
}
$webhooks
->setParam('payload', $file->getArrayCopy())
;
$audits
->setParam('event', 'storage.files.create')
->setParam('resource', 'storage/files/'.$file->getId())
@ -504,10 +500,6 @@ App::put('/v1/storage/files/:fileId')
throw new Exception('Failed saving file to DB', 500);
}
$webhooks
->setParam('payload', $file->getArrayCopy())
;
$audits
->setParam('event', 'storage.files.update')
->setParam('resource', 'storage/files/'.$file->getId())
@ -546,11 +538,7 @@ App::delete('/v1/storage/files/:fileId')
throw new Exception('Failed to remove file from DB', 500);
}
}
$webhooks
->setParam('payload', $file->getArrayCopy())
;
$audits
->setParam('event', 'storage.files.delete')
->setParam('resource', 'storage/files/'.$file->getId())

View file

@ -274,11 +274,18 @@ App::shutdown(function ($utopia, $request, $response, $project, $webhooks, $audi
/** @var bool $mode */
if (!empty($functions->getParam('event'))) {
$functions->setParam('payload', $webhooks->getParam('payload'));
if(empty($functions->getParam('payload'))) {
$functions->setParam('payload', $response->getPayload());
}
$functions->trigger();
}
if (!empty($webhooks->getParam('event'))) {
if(empty($webhooks->getParam('payload'))) {
$webhooks->setParam('payload', $response->getPayload());
}
$webhooks->trigger();
}

View file

@ -136,7 +136,7 @@ $events = array_keys($this->getParam('events', []));
<button class="danger reverse">Delete</button>
</form>
<span data-ls-bind="{{webhook.name}}"></span> &nbsp; (<span data-ls-bind="{{webhook.events.sum}}"></span> events)
<span data-ls-bind="{{webhook.name}}"></span> &nbsp; (<span data-ls-bind="{{webhook.events.length}}"></span> events)
<span data-ls-if="false === {{webhook.security}}">
&nbsp; <small class="text-danger">(SSL/TLS Disabled)</small>
</span>

View file

@ -134,6 +134,7 @@ services:
depends_on:
- redis
- mariadb
- request-catcher
environment:
- _APP_ENV
- _APP_REDIS_HOST
@ -292,7 +293,7 @@ services:
- MYSQL_PASSWORD=password
command: 'mysqld --innodb-flush-method=fsync' # add ' --query_cache_size=0' for DB tests
maildev:
maildev: # used mainly for dev tests
image: djfarrelly/maildev
container_name: appwrite-maildev
restart: unless-stopped
@ -301,6 +302,15 @@ services:
networks:
- appwrite
request-catcher: # used mainly for dev tests
image: smarterdm/http-request-catcher
container_name: appwrite-request-catcher
restart: unless-stopped
ports:
- '5000:5000'
networks:
- appwrite
# smtp:
# image: appwrite/smtp:1.0.1
# container_name: appwrite-smtp

View file

@ -21,7 +21,7 @@ class Document extends ArrayObject
* @param int $flags
* @param string $iterator_class
*/
public function __construct($input = null, $flags = 0, $iterator_class = 'ArrayIterator')
public function __construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator')
{
foreach ($input as $key => &$value) {
if (\is_array($value)) {

View file

@ -7,6 +7,7 @@ use Utopia\Swoole\Response as SwooleResponse;
use Swoole\Http\Response as SwooleHTTPResponse;
use Appwrite\Database\Document;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Continent;
@ -36,6 +37,7 @@ use Appwrite\Utopia\Response\Model\Webhook;
class Response extends SwooleResponse
{
// General
const MODEL_ANY = 'any';
const MODEL_LOG = 'log';
const MODEL_LOG_LIST = 'logList';
const MODEL_ERROR = 'error';
@ -101,6 +103,11 @@ class Response extends SwooleResponse
const MODEL_DOMAIN = 'domain';
const MODEL_DOMAIN_LIST = 'domainList';
/**
* @var array
*/
protected $payload = [];
/**
* Response constructor.
*/
@ -111,7 +118,7 @@ class Response extends SwooleResponse
->setModel(new Error())
->setModel(new ErrorDev())
// Lists
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'users', self::MODEL_COLLECTION))
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG, false))
@ -133,6 +140,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY))
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
// Entities
->setModel(new Any())
->setModel(new Collection())
->setModel(new Rule())
->setModel(new Log())
@ -210,12 +218,16 @@ class Response extends SwooleResponse
/**
* Generate valid response object from document data
*/
protected function output(Document $document, string $model): array
public function output(Document $document, string $model): array
{
$data = $document;
$model = $this->getModel($model);
$output = [];
if($model->isAny()) {
return $document->getArrayCopy();
}
foreach($model->getRules() as $key => $rule) {
if(!$document->isSet($key)) {
if(!is_null($rule['default'])) {
@ -245,6 +257,8 @@ class Response extends SwooleResponse
$output[$key] = $data[$key];
}
$this->payload = $output;
return $output;
}
@ -269,4 +283,12 @@ class Response extends SwooleResponse
->send(yaml_emit($data, YAML_UTF8_ENCODING))
;
}
/**
* @return array
*/
public function getPayload():array
{
return $this->payload;
}
}

View file

@ -4,6 +4,14 @@ namespace Appwrite\Utopia\Response;
abstract class Model
{
/**
* @var bool
*/
protected $any = false;
/**
* @var array
*/
protected $rules = [];
/**
@ -45,4 +53,9 @@ abstract class Model
return $this;
}
public function isAny(): bool
{
return $this->any;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Any extends Model
{
/**
* @var bool
*/
protected $any = true;
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'Any';
}
/**
* Get Collection
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ANY;
}
}

View file

@ -15,6 +15,11 @@ class Rule extends Model
'description' => 'Rule ID.',
'example' => '5e5ea5c16897e',
])
->addRule('$collection', [ // TODO remove this from public response
'type' => 'string',
'description' => 'Rule Collection.',
'example' => '5e5e66c16897e',
])
->addRule('type', [
'type' => 'string',
'description' => 'Rule type. Possible values: ',
@ -34,6 +39,7 @@ class Rule extends Model
'type' => 'string',
'description' => 'Rule default value.',
'example' => 'Movie Name',
'default' => '',
])
->addRule('array', [
'type' => 'boolean',
@ -45,6 +51,13 @@ class Rule extends Model
'description' => 'Is required?',
'example' => true,
])
->addRule('list', [
'type' => 'string',
'description' => 'List of allowed values',
'array' => true,
'default' => [],
'example' => ['5e5ea5c168099'],
])
;
}

View file

@ -34,6 +34,7 @@ abstract class Scope extends TestCase
protected function getLastEmail():array
{
sleep(10);
$emails = json_decode(file_get_contents('http://maildev/email'), true);
if($emails && is_array($emails)) {
@ -43,6 +44,16 @@ abstract class Scope extends TestCase
return [];
}
protected function getLastRequest():array
{
sleep(10);
$resquest = json_decode(file_get_contents('http://request-catcher:5000/__last_request__'), true);
$resquest['data'] = json_decode($resquest['data'], true);
return $resquest;
}
/**
* @return array
*/

View file

@ -0,0 +1,152 @@
<?php
namespace Tests\E2E\Services\Workers;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Scopes\SideServer;
class WebhooksTest extends Scope
{
use ProjectConsole;
use SideClient;
public function testCreateProject(): array
{
/**
* Test for SUCCESS
*/
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertEquals('Project Test', $team['body']['name']);
$this->assertNotEmpty($team['body']['$id']);
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('Project Test', $response['body']['name']);
$this->assertEquals($team['body']['$id'], $response['body']['teamId']);
$this->assertArrayHasKey('platforms', $response['body']);
$this->assertArrayHasKey('webhooks', $response['body']);
$this->assertArrayHasKey('keys', $response['body']);
$this->assertArrayHasKey('tasks', $response['body']);
$projectId = $response['body']['$id'];
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => '',
'teamId' => $team['body']['$id'],
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Project Test',
]);
$this->assertEquals(400, $response['headers']['status-code']);
return ['projectId' => $projectId];
}
/**
* @depends testCreateProject
*/
public function testCreateWebhook($data): array
{
$id = (isset($data['projectId'])) ? $data['projectId'] : '';
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/webhooks', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Webhook Worker Test',
'events' => ['account.create', 'account.update.email'],
'url' => 'http://request-catcher:5000/webhook',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertContains('account.create', $response['body']['events']);
$this->assertContains('account.update.email', $response['body']['events']);
$this->assertCount(2, $response['body']['events']);
$this->assertEquals('http://request-catcher:5000/webhook', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(true, $response['body']['security']);
$this->assertEquals('username', $response['body']['httpUser']);
$data = array_merge($data, ['webhookId' => $response['body']['$id']]);
/**
* Test for FAILURE
*/
return $data;
}
/**
* @depends testCreateWebhook
*/
public function testCreateAccount($data)
{
$projectId = (isset($data['projectId'])) ? $data['projectId'] : '';
$email = uniqid().'webhook.user@localhost.test';
$password = 'password';
$name = 'User Name';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
]), [
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$webhook = $this->getLastRequest();
$this->assertNotEmpty($webhook['data']);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertIsNumeric($webhook['data']['status']);
$this->assertIsNumeric($webhook['data']['registration']);
$this->assertEquals($webhook['data']['email'], $email);
$this->assertEquals($webhook['data']['name'], $name);
$this->assertIsBool($webhook['data']['emailVerification']);
$this->assertIsArray($webhook['data']['prefs']);
$this->assertIsArray($webhook['data']['roles']);
}
}