Version 5.1 - All the GraphQL #32
39
src/API/APIClient.php
Normal file
39
src/API/APIClient.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package AnimeListClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use Amp;
|
||||
use Amp\Artax\{
|
||||
Client,
|
||||
Response,
|
||||
Request
|
||||
}
|
||||
|
||||
class APIClient {
|
||||
|
||||
/**
|
||||
* Get a syncronous response for a request
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
static public function syncResponse(Request $request): Response
|
||||
{
|
||||
$client = new Client();
|
||||
return wait($client->request($request));
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use Amp\Artax\{
|
||||
Client,
|
||||
FormBody,
|
||||
Client,
|
||||
FormBody,
|
||||
Request
|
||||
};
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
@ -30,56 +30,58 @@ use Psr\Log\LoggerAwareTrait;
|
||||
*/
|
||||
class APIRequestBuilder {
|
||||
use LoggerAwareTrait;
|
||||
|
||||
|
||||
/**
|
||||
* Url prefix for making url requests
|
||||
* @var string
|
||||
*/
|
||||
protected $baseUrl = '';
|
||||
|
||||
|
||||
/**
|
||||
* Url path of the request
|
||||
* @var string
|
||||
*/
|
||||
protected $path = '';
|
||||
|
||||
|
||||
/**
|
||||
* Query string for the request
|
||||
* @var string
|
||||
*/
|
||||
protected $query = '';
|
||||
|
||||
|
||||
/**
|
||||
* Default request headers
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultHeaders = [];
|
||||
|
||||
|
||||
/**
|
||||
* Valid HTTP request methos
|
||||
* @var array
|
||||
*/
|
||||
protected $validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
|
||||
|
||||
|
||||
/**
|
||||
* The current request
|
||||
* @var \Amp\Promise
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
|
||||
/**
|
||||
* Set body as form fields
|
||||
*
|
||||
* @param array $fields Mapping of field names to values
|
||||
* Set a basic authentication header
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return self
|
||||
*/
|
||||
public function setFormFields(array $fields): self
|
||||
public function setBasicAuth(string $username, string $password): self
|
||||
{
|
||||
$body = $this->fixBody((new FormBody)->addFields($createData));
|
||||
$this->setBody($body);
|
||||
$authString = 'Basic ' . base64_encode($username . ':' . $password);
|
||||
$this->setHeader('Authorization', $authString);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the request body
|
||||
*
|
||||
@ -91,7 +93,21 @@ class APIRequestBuilder {
|
||||
$this->request->setBody($body);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set body as form fields
|
||||
*
|
||||
* @param array $fields Mapping of field names to values
|
||||
* @return self
|
||||
*/
|
||||
public function setFormFields(array $fields): self
|
||||
{
|
||||
$this->setHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
$body = $this->fixBody((new FormBody)->addFields($fields));
|
||||
$this->setBody($body);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a request header
|
||||
*
|
||||
@ -104,10 +120,10 @@ class APIRequestBuilder {
|
||||
$this->request->setHeader($name, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set multiple request headers
|
||||
*
|
||||
*
|
||||
* name => value
|
||||
*
|
||||
* @param array $headers
|
||||
@ -119,10 +135,10 @@ class APIRequestBuilder {
|
||||
{
|
||||
$this->setHeader($name, $value);
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Append a query string in array format
|
||||
*
|
||||
@ -131,10 +147,10 @@ class APIRequestBuilder {
|
||||
*/
|
||||
public function setQuery(array $params): self
|
||||
{
|
||||
$this->query = http_build_query($params);
|
||||
$this->query = http_build_query($params);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the promise for the current request
|
||||
*
|
||||
@ -143,9 +159,19 @@ class APIRequestBuilder {
|
||||
public function getFullRequest()
|
||||
{
|
||||
$this->buildUri();
|
||||
|
||||
if ($this->logger)
|
||||
{
|
||||
$this->logger->debug('API Request', [
|
||||
'request_url' => $this->request->getUri(),
|
||||
'request_headers' => $this->request->getAllHeaders(),
|
||||
'request_body' => $this->request->getBody()
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new http request
|
||||
*
|
||||
@ -159,16 +185,23 @@ class APIRequestBuilder {
|
||||
{
|
||||
throw new InvalidArgumentException('Invalid HTTP methods');
|
||||
}
|
||||
|
||||
|
||||
$this->resetState();
|
||||
|
||||
|
||||
$this->request
|
||||
->setMethod($type)
|
||||
->setProtocol('1.1');
|
||||
|
||||
$this->path = $uri;
|
||||
|
||||
if ( ! empty($this->defaultHeaders))
|
||||
{
|
||||
$this->setHeaders($this->defaultHeaders);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create the full request url
|
||||
*
|
||||
@ -178,16 +211,16 @@ class APIRequestBuilder {
|
||||
{
|
||||
$url = (strpos($this->path, '//') !== FALSE)
|
||||
? $this->path
|
||||
: $this->baseUrl . $url;
|
||||
: $this->baseUrl . $this->path;
|
||||
|
||||
if ( ! empty($this->query))
|
||||
{
|
||||
$url .= '?' . $this->query;
|
||||
}
|
||||
|
||||
|
||||
$this->request->setUri($url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unencode the dual-encoded ampersands in the body
|
||||
*
|
||||
@ -202,7 +235,7 @@ class APIRequestBuilder {
|
||||
$rawBody = \Amp\wait($formBody->getBody());
|
||||
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the class state for a new request
|
||||
*
|
||||
|
@ -37,30 +37,35 @@ class ListItem extends AbstractListItem {
|
||||
|
||||
public function create(array $data): bool
|
||||
{
|
||||
$response = $this->getResponse('POST', 'library-entries', [
|
||||
'body' => Json::encode([
|
||||
'data' => [
|
||||
'type' => 'libraryEntries',
|
||||
'attributes' => [
|
||||
'status' => $data['status'],
|
||||
'progress' => $data['progress'] ?? 0
|
||||
$body = [
|
||||
'data' => [
|
||||
'type' => 'libraryEntries',
|
||||
'attributes' => [
|
||||
'status' => $data['status'],
|
||||
'progress' => $data['progress'] ?? 0
|
||||
],
|
||||
'relationships' => [
|
||||
'user' => [
|
||||
'data' => [
|
||||
'id' => $data['user_id'],
|
||||
'type' => 'users'
|
||||
]
|
||||
],
|
||||
'relationships' => [
|
||||
'user' => [
|
||||
'data' => [
|
||||
'id' => $data['user_id'],
|
||||
'type' => 'users'
|
||||
]
|
||||
],
|
||||
'media' => [
|
||||
'data' => [
|
||||
'id' => $data['id'],
|
||||
'type' => $data['type']
|
||||
]
|
||||
'media' => [
|
||||
'data' => [
|
||||
'id' => $data['id'],
|
||||
'type' => $data['type']
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
]
|
||||
];
|
||||
|
||||
$request = $this->requestBuilder->newRequest('POST', 'library-entries')
|
||||
->setJsonBody($body)
|
||||
->getFullRequest();
|
||||
$response = $this->getResponse('POST', 'library-entries', [
|
||||
'body' => Json::encode($body)
|
||||
]);
|
||||
|
||||
return ($response->getStatusCode() === 201);
|
||||
@ -74,11 +79,19 @@ class ListItem extends AbstractListItem {
|
||||
|
||||
public function get(string $id): array
|
||||
{
|
||||
return $this->getRequest("library-entries/{$id}", [
|
||||
$request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}")
|
||||
->setQuery([
|
||||
'include' => 'media,media.genres,media.mappings'
|
||||
])
|
||||
->getFullRequest();
|
||||
/*return $this->getRequest("library-entries/{$id}", [
|
||||
'query' => [
|
||||
'include' => 'media,media.genres,media.mappings'
|
||||
]
|
||||
]);
|
||||
]);*/
|
||||
|
||||
$response = \Amp\wait((new \Amp\Artax\Client)->request($request));
|
||||
return Json::decode($response->getBody());
|
||||
}
|
||||
|
||||
public function update(string $id, array $data): Response
|
||||
|
@ -116,7 +116,6 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'mal_id' => $item['mal_id'] ?? null,
|
||||
'data' => [
|
||||
'status' => $item['watching_status'],
|
||||
'rating' => $item['user_rating'] / 2,
|
||||
'reconsuming' => $rewatching,
|
||||
'reconsumeCount' => $item['rewatched'],
|
||||
'notes' => $item['notes'],
|
||||
@ -124,10 +123,10 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'private' => $privacy
|
||||
]
|
||||
];
|
||||
|
||||
if ((int) $untransformed['data']['rating'] === 0)
|
||||
|
||||
if ( ! empty($item['user_rating']))
|
||||
{
|
||||
unset($untransformed['data']['rating']);
|
||||
$untransformed['data']['rating'] = $item['user_rating'] / 2;
|
||||
}
|
||||
|
||||
return $untransformed;
|
||||
|
@ -30,7 +30,7 @@ class ListItem {
|
||||
use ContainerAware;
|
||||
use MALTrait;
|
||||
|
||||
public function create(array $data): bool
|
||||
public function create(array $data)
|
||||
{
|
||||
$id = $data['id'];
|
||||
$createData = [
|
||||
@ -40,11 +40,20 @@ class ListItem {
|
||||
])
|
||||
];
|
||||
|
||||
// $config = $this->container->get('config');
|
||||
|
||||
/*$request = $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml")
|
||||
->setFormFields($createData)
|
||||
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
|
||||
->getFullRequest();*/
|
||||
|
||||
$response = $this->getResponse('POST', "animelist/add/{$id}.xml", [
|
||||
'body' => $this->fixBody((new FormBody)->addFields($createData))
|
||||
]);
|
||||
|
||||
return $response->getBody() === 'Created';
|
||||
|
||||
// return $request;
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
|
@ -23,7 +23,7 @@ use Aviat\AnimeClient\API\{
|
||||
};
|
||||
|
||||
class MALRequestBuilder extends APIRequestBuilder {
|
||||
|
||||
|
||||
/**
|
||||
* The base url for api requests
|
||||
* @var string $base_url
|
||||
@ -41,7 +41,7 @@ class MALRequestBuilder extends APIRequestBuilder {
|
||||
'Content-type' => 'application/x-www-form-urlencoded',
|
||||
'User-Agent' => "Tim's Anime Client/4.0"
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Valid HTTP request methos
|
||||
* @var array
|
||||
|
@ -27,7 +27,7 @@ use Aviat\Ion\Json;
|
||||
use InvalidArgumentException;
|
||||
|
||||
trait MALTrait {
|
||||
|
||||
|
||||
/**
|
||||
* The request builder for the MAL API
|
||||
* @var MALRequestBuilder
|
||||
@ -51,7 +51,7 @@ trait MALTrait {
|
||||
'Content-type' => 'application/x-www-form-urlencoded',
|
||||
'User-Agent' => "Tim's Anime Client/4.0"
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Set the request builder object
|
||||
*
|
||||
@ -63,7 +63,7 @@ trait MALTrait {
|
||||
$this->requestBuilder = $requestBuilder;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unencode the dual-encoded ampersands in the body
|
||||
*
|
||||
@ -78,7 +78,7 @@ trait MALTrait {
|
||||
$rawBody = \Amp\wait($formBody->getBody());
|
||||
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a request object
|
||||
*
|
||||
@ -89,47 +89,23 @@ trait MALTrait {
|
||||
*/
|
||||
public function setUpRequest(string $type, string $url, array $options = [])
|
||||
{
|
||||
$this->defaultHeaders['User-Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? $this->defaultHeaders;
|
||||
|
||||
$type = strtoupper($type);
|
||||
$validTypes = ['GET', 'POST', 'DELETE'];
|
||||
|
||||
if ( ! in_array($type, $validTypes))
|
||||
{
|
||||
throw new InvalidArgumentException('Invalid http request type');
|
||||
}
|
||||
|
||||
$config = $this->container->get('config');
|
||||
$logger = $this->container->getLogger('mal-request');
|
||||
|
||||
$headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [
|
||||
'Authorization' => 'Basic ' .
|
||||
base64_encode($config->get(['mal','username']) . ':' .$config->get(['mal','password']))
|
||||
]);
|
||||
$request = $this->requestBuilder
|
||||
->newRequest($type, $url)
|
||||
->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password']));
|
||||
|
||||
$query = $options['query'] ?? [];
|
||||
|
||||
$url = (strpos($url, '//') !== FALSE)
|
||||
? $url
|
||||
: $this->baseUrl . $url;
|
||||
|
||||
if ( ! empty($query))
|
||||
if (array_key_exists('query', $options))
|
||||
{
|
||||
$url .= '?' . http_build_query($query);
|
||||
$request->setQuery($options['query']);
|
||||
}
|
||||
|
||||
$request = (new Request)
|
||||
->setMethod($type)
|
||||
->setUri($url)
|
||||
->setProtocol('1.1')
|
||||
->setAllHeaders($headers);
|
||||
|
||||
if (array_key_exists('body', $options))
|
||||
{
|
||||
$request->setBody($options['body']);
|
||||
}
|
||||
|
||||
return $request;
|
||||
|
||||
return $request->getFullRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,19 +123,16 @@ trait MALTrait {
|
||||
{
|
||||
$logger = $this->container->getLogger('mal-request');
|
||||
}
|
||||
|
||||
|
||||
$request = $this->setUpRequest($type, $url, $options);
|
||||
$response = \Amp\wait((new Client)->request($request));
|
||||
|
||||
$logger->debug('MAL api request', [
|
||||
'url' => $url,
|
||||
$logger->debug('MAL api response', [
|
||||
'status' => $response->getStatus(),
|
||||
'reason' => $response->getReason(),
|
||||
'body' => $response->getBody(),
|
||||
'headers' => $response->getAllHeaders(),
|
||||
'requestHeaders' => $request->getAllHeaders(),
|
||||
'requestBody' => $request->hasBody() ? $request->getBody() : 'No request body',
|
||||
'requestBodyBeforeEncode' => $request->hasBody() ? urldecode($request->getBody()) : '',
|
||||
'body' => $response->getBody()
|
||||
]);
|
||||
|
||||
return $response;
|
||||
|
@ -56,27 +56,33 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
* @return array
|
||||
*/
|
||||
public function untransform(array $item): array
|
||||
{
|
||||
$rewatching = (array_key_exists('reconsuming', $item['data']) && $item['data']['reconsuming']);
|
||||
|
||||
{
|
||||
$map = [
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'episode' => $item['data']['progress'],
|
||||
// 'enable_rewatching' => $rewatching,
|
||||
// 'times_rewatched' => $item['data']['reconsumeCount'],
|
||||
// 'comments' => $item['data']['notes'],
|
||||
'episode' => $item['data']['progress']
|
||||
]
|
||||
];
|
||||
|
||||
if (array_key_exists('rating', $item['data']))
|
||||
switch(TRUE)
|
||||
{
|
||||
$map['data']['score'] = $item['data']['rating'] * 2;
|
||||
}
|
||||
|
||||
if (array_key_exists('status', $item['data']))
|
||||
{
|
||||
$map['data']['status'] = self::statusMap[$item['data']['status']];
|
||||
case array_key_exists('notes', $item['data']):
|
||||
$map['data']['comments'] = $item['data']['notes'];
|
||||
|
||||
case array_key_exists('rating', $item['data']):
|
||||
$map['data']['score'] = $item['data']['rating'] * 2;
|
||||
|
||||
case array_key_exists('reconsuming', $item['data']):
|
||||
$map['data']['enable_rewatching'] = (bool) $item['data']['reconsuming'];
|
||||
|
||||
case array_key_exists('reconsumeCount', $item['data']):
|
||||
$map['data']['times_rewatched'] = $item['data']['reconsumeCount'];
|
||||
|
||||
case array_key_exists('status', $item['data']):
|
||||
$map['data']['status'] = self::statusMap[$item['data']['status']];
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return $map;
|
||||
|
136
tests/API/APIRequestBuilderTest.php
Normal file
136
tests/API/APIRequestBuilderTest.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package AnimeListClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Tests\API;
|
||||
|
||||
use Amp;
|
||||
use Amp\Artax\Client;
|
||||
use Aviat\AnimeClient\API\APIRequestBuilder;
|
||||
use Aviat\Ion\Json;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class APIRequestBuilderTest extends TestCase {
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->builder = new class extends APIRequestBuilder {
|
||||
protected $baseUrl = 'https://httpbin.org/';
|
||||
|
||||
protected $defaultHeaders = ['User-Agent' => "Tim's Anime Client Testsuite / 4.0"];
|
||||
};
|
||||
|
||||
$this->builder->setLogger(new NullLogger);
|
||||
}
|
||||
|
||||
public function testGzipRequest()
|
||||
{
|
||||
$request = $this->builder->newRequest('GET', 'gzip')
|
||||
->getFullRequest();
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
$this->assertEquals(1, $body['gzipped']);
|
||||
}
|
||||
|
||||
public function testInvalidRequestMethod()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->builder->newRequest('FOO', 'gzip')
|
||||
->getFullRequest();
|
||||
}
|
||||
|
||||
public function testRequestWithBasicAuth()
|
||||
{
|
||||
$request = $this->builder->newRequest('GET', 'headers')
|
||||
->setBasicAuth('username', 'password')
|
||||
->getFullRequest();
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
$this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
|
||||
}
|
||||
|
||||
public function testRequestWithQueryString()
|
||||
{
|
||||
$query = [
|
||||
'foo' => 'bar',
|
||||
'bar' => [
|
||||
'foo' => 'bar'
|
||||
],
|
||||
'baz' => [
|
||||
'bar' => 'foo'
|
||||
]
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'foo' => 'bar',
|
||||
'bar[foo]' => 'bar',
|
||||
'baz[bar]' => 'foo'
|
||||
];
|
||||
|
||||
$request = $this->builder->newRequest('GET', 'get')
|
||||
->setQuery($query)
|
||||
->getFullRequest();
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
$this->assertEquals($expected, $body['args']);
|
||||
}
|
||||
|
||||
public function testFormValueRequest()
|
||||
{
|
||||
$formValues = [
|
||||
'foo' => 'bar',
|
||||
'bar' => 'foo'
|
||||
];
|
||||
|
||||
$request = $this->builder->newRequest('POST', 'post')
|
||||
->setFormFields($formValues)
|
||||
->getFullRequest();
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
$this->assertEquals($formValues, $body['form']);
|
||||
}
|
||||
|
||||
public function testFullUrlRequest()
|
||||
{
|
||||
$data = [
|
||||
'foo' => [
|
||||
'bar' => 1,
|
||||
'baz' => [2, 3, 4],
|
||||
'bar' => [
|
||||
'a' => 1,
|
||||
'b' => 2
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$request = $this->builder->newRequest('PUT', 'https://httpbin.org/put')
|
||||
->setHeader('Content-Type', 'application/json')
|
||||
->setBody(Json::encode($data))
|
||||
->getFullRequest();
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
$this->assertEquals($data, $body['json']);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user