From fc7514bc3fc6ef2cec6098b6278eee3e0e09f536 Mon Sep 17 00:00:00 2001 From: parsilver Date: Sun, 17 Dec 2023 16:22:39 +0700 Subject: [PATCH 1/2] Use server timestamp --- src/Client.php | 35 ++++--- src/Endpoints/CryptoEndpoint.php | 12 +-- src/Endpoints/MarketEndpoint.php | 16 +-- src/Endpoints/SystemEndpoint.php | 19 +++- src/Endpoints/UserEndpoint.php | 4 +- src/Requests/GenerateSignatureV3.php | 26 +++-- src/Requests/PendingRequest.php | 14 +-- src/Utility.php | 31 ++++++ tests/AuthorizerTest.php | 26 +++-- tests/CryptoEndpointTest.php | 98 +++++++++++++++++ tests/MarketEndpointTest.php | 32 +++--- tests/MockHttpClient.php | 92 ++++++++++++++++ tests/PendingRequestTest.php | 151 +++++++++++++++++++++++++++ tests/ResponseErrorCodeTest.php | 34 ++---- tests/SystemEndpointTest.php | 29 +++++ tests/UserEndpointTest.php | 107 +++++++++++++++++++ 16 files changed, 621 insertions(+), 105 deletions(-) create mode 100644 src/Utility.php create mode 100644 tests/CryptoEndpointTest.php create mode 100644 tests/MockHttpClient.php create mode 100644 tests/PendingRequestTest.php create mode 100644 tests/SystemEndpointTest.php create mode 100644 tests/UserEndpointTest.php diff --git a/src/Client.php b/src/Client.php index 69b5918..83a3cc0 100644 --- a/src/Client.php +++ b/src/Client.php @@ -40,6 +40,26 @@ public function __construct( $this->logger = $logger; } + public function market(): Endpoints\MarketEndpoint + { + return new Endpoints\MarketEndpoint($this); + } + + public function crypto(): Endpoints\CryptoEndpoint + { + return new Endpoints\CryptoEndpoint($this); + } + + public function user(): Endpoints\UserEndpoint + { + return new Endpoints\UserEndpoint($this); + } + + public function system(): Endpoints\SystemEndpoint + { + return new Endpoints\SystemEndpoint($this); + } + public function getTransport(): Transport { return $this->transport; @@ -62,19 +82,4 @@ public function sendRequest(PsrRequestInterface $request): PsrResponseInterface { return $this->transport->sendRequest($request); } - - public function market(): Endpoints\MarketEndpoint - { - return new Endpoints\MarketEndpoint($this); - } - - public function system(): Endpoints\SystemEndpoint - { - return new Endpoints\SystemEndpoint($this); - } - - public function user(): Endpoints\UserEndpoint - { - return new Endpoints\UserEndpoint($this); - } } diff --git a/src/Endpoints/CryptoEndpoint.php b/src/Endpoints/CryptoEndpoint.php index a2e3dfb..9c7f197 100644 --- a/src/Endpoints/CryptoEndpoint.php +++ b/src/Endpoints/CryptoEndpoint.php @@ -42,7 +42,7 @@ public function addresses(array $params): ResponseInterface return $this->makeRequest('GET', '/api/v3/crypto/addresses') ->withQuery(array_filter($params)) ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -89,7 +89,7 @@ public function withdrawal(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/crypto/withdraw') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->withBody($params) ->send(); } @@ -131,7 +131,7 @@ public function internalWithdrawal(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/crypto/internal-withdraw') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->withBody($params) ->send(); } @@ -175,7 +175,7 @@ public function depositHistory(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/crypto/deposit-history') ->withQuery(array_filter($params)) ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -218,7 +218,7 @@ public function withdrawalHistory(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/crypto/withdrawal-history') ->withQuery(array_filter($params)) ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -247,7 +247,7 @@ public function generateAddress(string $symbol): ResponseInterface return $this->makeRequest('POST', '/api/v3/crypto/generate-address') ->withQuery(['sym' => $symbol]) ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } } diff --git a/src/Endpoints/MarketEndpoint.php b/src/Endpoints/MarketEndpoint.php index 6d3a8e9..3c6e0ed 100644 --- a/src/Endpoints/MarketEndpoint.php +++ b/src/Endpoints/MarketEndpoint.php @@ -232,7 +232,7 @@ public function wallet(): ResponseInterface return $this->makeRequest('POST', '/api/v3/market/wallet') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -276,7 +276,7 @@ public function placeBid(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/market/place-bid') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->withBody($params) ->send(); } @@ -321,7 +321,7 @@ public function placeAsk(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/market/place-ask') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->withBody($params) ->send(); } @@ -353,7 +353,7 @@ public function cancelOrder(array $params): ResponseInterface return $this->makeRequest('POST', '/api/v3/market/cancel-order') ->acceptJson() ->withBody($params) - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -380,7 +380,7 @@ public function balances(): ResponseInterface return $this->makeRequest('POST', '/api/v3/market/balances') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -419,7 +419,7 @@ public function openOrders(string $sym): ResponseInterface return $this->makeRequest('GET', '/api/v3/market/my-open-orders') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->withBody([ 'sym' => $sym, ]) @@ -473,7 +473,7 @@ public function myOrderHistory(array $params): ResponseInterface return $this->makeRequest('GET', '/api/v3/market/my-order-history') ->withQuery(array_filter($params)) ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -531,7 +531,7 @@ public function myOrderInfo(array $params): ResponseInterface return $this->makeRequest('GET', '/api/v3/market/order-info') ->withQuery(array_filter($params)) ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } } diff --git a/src/Endpoints/SystemEndpoint.php b/src/Endpoints/SystemEndpoint.php index 31b0e98..cee5691 100644 --- a/src/Endpoints/SystemEndpoint.php +++ b/src/Endpoints/SystemEndpoint.php @@ -6,6 +6,23 @@ class SystemEndpoint extends AbstractEndpoint { + /** + * Get server status. + * + * @response + * [ + * { + * "name":"Non-secure endpoints", + * "status":"ok", + * "message":"" + * }, + * { + * "name":"Secure endpoints", + * "status":"ok", + * "message":"" + * } + * ] + */ public function status(): ResponseInterface { return $this->makeRequest('GET', '/api/status')->send(); @@ -15,7 +32,7 @@ public function status(): ResponseInterface * Get server timestamp. * * @response - * 1701251212273 + * 1702793384662 */ public function serverTimestamp(): ResponseInterface { diff --git a/src/Endpoints/UserEndpoint.php b/src/Endpoints/UserEndpoint.php index 5dd7373..dc27b7c 100644 --- a/src/Endpoints/UserEndpoint.php +++ b/src/Endpoints/UserEndpoint.php @@ -22,7 +22,7 @@ public function tradingCredits(): ResponseInterface return $this->makeRequest('POST', '/api/v3/user/trading-credits') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } @@ -69,7 +69,7 @@ public function userLimits(): ResponseInterface return $this->makeRequest('POST', '/api/v3/user/limits') ->acceptJson() - ->withInterceptor(new GenerateSignatureV3($config)) + ->withInterceptor(new GenerateSignatureV3($config, $this->client)) ->send(); } } diff --git a/src/Requests/GenerateSignatureV3.php b/src/Requests/GenerateSignatureV3.php index 42eca4c..6bc675d 100644 --- a/src/Requests/GenerateSignatureV3.php +++ b/src/Requests/GenerateSignatureV3.php @@ -2,7 +2,9 @@ namespace Farzai\Bitkub\Requests; +use Farzai\Bitkub\Client; use Farzai\Bitkub\Contracts\RequestInterceptor; +use Farzai\Bitkub\Utility; use Psr\Http\Message\RequestInterface as PsrRequestInterface; class GenerateSignatureV3 implements RequestInterceptor @@ -14,17 +16,18 @@ class GenerateSignatureV3 implements RequestInterceptor */ private array $config; - public function __construct(array $config) - { - $this->config = $config; - } + /** + * The client instance. + */ + private Client $client; - public function generate($timestamp, $method, $path, $query, $payload) + /** + * Create a new client instance. + */ + public function __construct(array $config, Client $client) { - $message = sprintf('%s%s%s%s%s', $timestamp, $method, $path, $query, $payload); - $signature = hash_hmac('sha256', $message, $this->config['secret']); - - return $signature; + $this->config = $config; + $this->client = $client; } /** @@ -32,7 +35,8 @@ public function generate($timestamp, $method, $path, $query, $payload) */ public function apply(PsrRequestInterface $request): PsrRequestInterface { - $timestamp = (int) (microtime(true) * 1000); + $timestamp = (int) Utility::getServerTimestamp($this->client)->format('U'); + $method = strtoupper($request->getMethod()); $path = '/'.trim($request->getUri()->getPath(), '/'); $payload = $request->getBody()->getContents() ?: ''; @@ -42,7 +46,7 @@ public function apply(PsrRequestInterface $request): PsrRequestInterface $query = '?'.$query; } - $signature = $this->generate($timestamp, $method, $path, $query, $payload); + $signature = Utility::generateSignature($this->config['secret'], $timestamp, $method, $path, $query, $payload); return $request->withHeader('X-BTK-APIKEY', $this->config['api_key']) ->withHeader('X-BTK-SIGN', $signature) diff --git a/src/Requests/PendingRequest.php b/src/Requests/PendingRequest.php index e7783fd..ac2f055 100644 --- a/src/Requests/PendingRequest.php +++ b/src/Requests/PendingRequest.php @@ -13,20 +13,20 @@ class PendingRequest { - private ClientInterface $client; + public ClientInterface $client; - private string $method; + public string $method; - private string $path; + public string $path; - private array $options; + public array $options; /** * The request interceptors. * * @var array<\Farzai\Bitkub\Contracts\RequestInterceptor> */ - private $interceptors = []; + public $interceptors = []; /** * Create a new pending request instance. @@ -141,7 +141,7 @@ public function send(): ResponseInterface /** * Create a new request instance. */ - protected function createRequest(string $method, string $path, array $options = []): PsrRequestInterface + public function createRequest(string $method, string $path, array $options = []): PsrRequestInterface { // Normalize path $path = '/'.trim($path, '/'); @@ -170,7 +170,7 @@ protected function createRequest(string $method, string $path, array $options = /** * Create a new response instance. */ - protected function createResponse(PsrRequestInterface $request, PsrResponseInterface $baseResponse): ResponseInterface + public function createResponse(PsrRequestInterface $request, PsrResponseInterface $baseResponse): ResponseInterface { $response = new Response($request, $baseResponse); $response = new ResponseWithValidateErrorCode($response); diff --git a/src/Utility.php b/src/Utility.php new file mode 100644 index 0000000..0e89d9c --- /dev/null +++ b/src/Utility.php @@ -0,0 +1,31 @@ +system() + ->serverTimestamp() + ->throw()->body(); + + return \DateTimeImmutable::createFromFormat('U', $timestamp); + } +} diff --git a/tests/AuthorizerTest.php b/tests/AuthorizerTest.php index 9216cbb..44941e4 100644 --- a/tests/AuthorizerTest.php +++ b/tests/AuthorizerTest.php @@ -1,22 +1,30 @@ 'test', - 'secret' => $secret, - ]); + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, (string) $timestamp)); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $timestamp = (int) Utility::getServerTimestamp($client)->format('U'); + + expect($timestamp)->toBe(1630483200000); - $signature = $authorizer->generate($timestamp, $method, $path, $query, $payload); + $signature = Utility::generateSignature($secret, $timestamp, $method, $path, $query, $payload); - expect($signature)->toBe('b8403c345ce41b25b47885254fb8aeed9ad7ceb9e30425b86a9a151dd6ac2e35'); + expect($signature)->toBe('ae6fd3dc7d85ebea023e54292fa6eebaeea6dc02002433c51b57136eeb0a03e5'); }); diff --git a/tests/CryptoEndpointTest.php b/tests/CryptoEndpointTest.php new file mode 100644 index 0000000..b33d3d1 --- /dev/null +++ b/tests/CryptoEndpointTest.php @@ -0,0 +1,98 @@ +setCredentials('test', 'secret') + ->build(); + + $user = $client->crypto(); + + expect($user)->toBeInstanceOf(CryptoEndpoint::class); +}); + +it('can call addresses success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + [ + 'currency' => 'BTC', + 'address' => '3BtxdKw6XSbneNvmJTLVHS9XfNYM7VAe8k', + 'tag' => 0, + 'time' => 1570893867, + ], + ], + 'pagination' => [ + 'page' => 1, + 'last' => 1, + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->crypto()->addresses([ + 'p' => 1, + 'lmt' => 1, + ])->throw(); + + expect($response->getStatusCode())->toBe(200); + expect($response->json('result'))->toBeArray(); + expect($response->json('result.0'))->toBeArray(); + + expect($response->json('result.0.currency'))->toBe('BTC'); + expect($response->json('result.0.address'))->toBe('3BtxdKw6XSbneNvmJTLVHS9XfNYM7VAe8k'); + expect($response->json('result.0.tag'))->toBe(0); + expect($response->json('result.0.time'))->toBe(1570893867); + + expect($response->json('pagination'))->toBeArray(); + expect($response->json('pagination.page'))->toBe(1); + expect($response->json('pagination.last'))->toBe(1); +}); + +it('can call withdrawal success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + 'txid' => 'BTCWD0000012345', + 'adr' => '4asyjKw6XScneNvhJTLVHS9XfNYM7VBf8x', + 'mem' => '', + 'cur' => 'BTC', + 'amt' => 0.1, + 'fee' => 0.0002, + 'ts' => 1569999999, + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->crypto()->withdrawal([ + 'sym' => 'BTC', + 'amt' => 0.1, + 'adr' => '4asyjKw6XScneNvhJTLVHS9XfNYM7VBf8x', + 'mem' => '', + ])->throw(); + + expect($response->getStatusCode())->toBe(200); + expect($response->json('result'))->toBeArray(); + + expect($response->json('result.txid'))->toBe('BTCWD0000012345'); + expect($response->json('result.adr'))->toBe('4asyjKw6XScneNvhJTLVHS9XfNYM7VBf8x'); + expect($response->json('result.mem'))->toBe(''); + expect($response->json('result.cur'))->toBe('BTC'); + expect($response->json('result.amt'))->toBe(0.1); + expect($response->json('result.fee'))->toBe(0.0002); + expect($response->json('result.ts'))->toBe(1569999999); +}); diff --git a/tests/MarketEndpointTest.php b/tests/MarketEndpointTest.php index e3a09d3..7044d8a 100644 --- a/tests/MarketEndpointTest.php +++ b/tests/MarketEndpointTest.php @@ -2,6 +2,7 @@ use Farzai\Bitkub\ClientBuilder; use Farzai\Bitkub\Endpoints\MarketEndpoint; +use Farzai\Bitkub\Tests\MockHttpClient; use Farzai\Transport\Contracts\ResponseInterface; it('should create market endpoint success', function () { @@ -15,34 +16,25 @@ }); it('should get balance success', function () { - $httpClient = $this->createMock(\Psr\Http\Client\ClientInterface::class); - - $stream = $this->createMock(\Psr\Http\Message\StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode([ - 'error' => 0, - 'result' => [ - 'THB' => [ - 'available' => 1000, - 'reserved' => 0, + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'THB' => [ + 'available' => 1000, + 'reserved' => 0, + ], ], - ], - ])); - - $psrResponse = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $psrResponse->method('getStatusCode')->willReturn(200); - $psrResponse->method('getBody')->willReturn($stream); + ]))); $client = ClientBuilder::create() ->setCredentials('test', 'secret') - ->setHttpClient($httpClient) + ->setHttpClient($psrClient) ->build(); $market = $client->market(); - $httpClient->expects($this->once()) - ->method('sendRequest') - ->willReturn($psrResponse); - $response = $market->balances(); expect($response)->toBeInstanceOf(ResponseInterface::class); diff --git a/tests/MockHttpClient.php b/tests/MockHttpClient.php new file mode 100644 index 0000000..4fbe779 --- /dev/null +++ b/tests/MockHttpClient.php @@ -0,0 +1,92 @@ + + */ + private array $sequence = []; + + /** + * Create a new mock http client instance. + */ + public static function make(): self + { + return new static('mock-http-client'); + } + + /** + * Add a sequence of responses. + * + * @param PsrResponseInterface|callable ...$responses + */ + public function addSequence(PsrResponseInterface|callable ...$responses): self + { + foreach ($responses as $response) { + if (is_callable($response)) { + $response = $response($this); + } + + $this->sequence[] = $response; + } + + return $this; + } + + /** + * Send a PSR-7 request and return a PSR-7 response. + */ + public function sendRequest(PsrRequestInterface $request): PsrResponseInterface + { + return array_shift($this->sequence); + } + + public function createStream(string $contents): PsrStreamInterface + { + $stream = $this->createMock(PsrStreamInterface::class); + $stream->method('getContents')->willReturn($contents); + + return $stream; + } + + public function createResponse(int $statusCode, string $contents, array $headers = []): PsrResponseInterface + { + $stream = $this->createStream($contents); + + $response = $this->createMock(PsrResponseInterface::class); + $response->method('getStatusCode')->willReturn($statusCode); + $response->method('getBody')->willReturn($stream); + $response->method('getHeaders')->willReturn($headers); + + return $response; + } + + public static function response(int $statusCode, array|string $contents, array $headers = []): PsrResponseInterface + { + $client = static::make(); + + if (is_array($contents)) { + $contents = json_encode($contents); + } + + return $client->createResponse($statusCode, $contents, $headers); + } + + /** + * Create a response with server timestamp. + */ + public static function responseServerTimestamp() + { + return static::response(200, (string) ((int) (microtime(true) * 1000))); + } +} diff --git a/tests/PendingRequestTest.php b/tests/PendingRequestTest.php new file mode 100644 index 0000000..c88fc3f --- /dev/null +++ b/tests/PendingRequestTest.php @@ -0,0 +1,151 @@ +setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + + expect($request)->toBeInstanceOf(PendingRequest::class); +}); + +it('can set request method', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->method('POST'); + + expect($request->method)->toBe('POST'); +}); + +it('can set request path', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->path('/api/market/orders'); + + expect($request->path)->toBe('/api/market/orders'); +}); + +it('can set request options', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->options(['query' => ['symbol' => 'BTC']]); + + expect($request->options)->toBe(['query' => ['symbol' => 'BTC']]); +}); + +it('can add request interceptor', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = $this->createMock(\Psr\Http\Message\RequestInterface::class); + + $pending = new PendingRequest($client, 'GET', '/api/market/balances'); + + $interceptor = $this->createMock(\Farzai\Bitkub\Contracts\RequestInterceptor::class); + $interceptor->method('apply')->willReturn($request); + + $pending->withInterceptor($interceptor); + + expect($pending->interceptors)->toContain($interceptor); +}); + +it('can set request body', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'POST', '/api/market/orders'); + $request->withBody(['symbol' => 'BTC', 'quantity' => 1]); + + expect($request->options['body'])->toBe(['symbol' => 'BTC', 'quantity' => 1]); +}); + +it('can set request query', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->withQuery(['symbol' => 'BTC']); + + expect($request->options['query'])->toBe(['symbol' => 'BTC']); +}); + +it('can set request headers', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->withHeaders(['Authorization' => 'Bearer token']); + + expect($request->options['headers'])->toBe(['Authorization' => 'Bearer token']); +}); + +it('can set request header', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->withHeader('Content-Type', 'application/json'); + + expect($request->options['headers']['Content-Type'])->toBe('application/json'); +}); + +it('can set request to accept JSON', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'GET', '/api/market/balances'); + $request->acceptJson(); + + expect($request->options['headers']['Accept'])->toBe('application/json'); +}); + +it('can set request to send JSON', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $request = new PendingRequest($client, 'POST', '/api/market/orders'); + $request->asJson(); + + expect($request->options['headers']['Content-Type'])->toBe('application/json'); +}); + +it('it can createRequest success', function () { + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->build(); + + $pending = new PendingRequest($client, 'GET', '/api/market/orders'); + + $request = $pending->createRequest('POST', '/api/market/trades', [ + 'body' => ['symbol' => 'BTC'], + 'query' => ['symbol' => 'BTC'], + 'headers' => ['Content-Type' => 'application/json'], + ]); + + expect($request)->toBeInstanceOf(\Psr\Http\Message\RequestInterface::class); + expect($request->getMethod())->toBe('POST'); + expect($request->getUri()->getPath())->toBe('/api/market/trades'); + expect($request->getBody()->getContents())->toBe(json_encode(['symbol' => 'BTC'])); + expect($request->getUri()->getQuery())->toBe('symbol=BTC'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); +}); diff --git a/tests/ResponseErrorCodeTest.php b/tests/ResponseErrorCodeTest.php index 7005780..f88b74a 100644 --- a/tests/ResponseErrorCodeTest.php +++ b/tests/ResponseErrorCodeTest.php @@ -2,21 +2,15 @@ use Farzai\Bitkub\Exceptions\BitkubResponseErrorCodeException; use Farzai\Bitkub\Responses\ResponseWithValidateErrorCode; +use Farzai\Bitkub\Tests\MockHttpClient; use Farzai\Transport\Response; use Psr\Http\Message\RequestInterface as PsrRequestInterface; -use Psr\Http\Message\ResponseInterface as PsrResponseInterface; it('decorator must be instance of ResponseInterface', function () { - $stream = $this->createMock(\Psr\Http\Message\StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode([ - 'error' => 0, - ])); - $psrRequest = $this->createMock(PsrRequestInterface::class); - $psrResponse = $this->createMock(PsrResponseInterface::class); - $psrResponse->method('getStatusCode')->willReturn(200); - $psrResponse->method('getBody')->willReturn($stream); - $psrResponse->method('getHeaders')->willReturn([ + $psrResponse = MockHttpClient::response(200, json_encode([ + 'error' => 0, + ]), [ 'Content-Type' => 'application/json', ]); @@ -38,17 +32,11 @@ }); it('should throw exception when error code is not 0', function () { - $stream = $this->createMock(\Psr\Http\Message\StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode([ + $psrRequest = $this->createMock(PsrRequestInterface::class); + $psrResponse = MockHttpClient::response(200, json_encode([ 'error' => 1, ])); - $psrResponse = $this->createMock(PsrResponseInterface::class); - $psrResponse->method('getStatusCode')->willReturn(200); - $psrResponse->method('getBody')->willReturn($stream); - - $psrRequest = $this->createMock(PsrRequestInterface::class); - $response = new Response($psrRequest, $psrResponse); $response = new ResponseWithValidateErrorCode($response); @@ -57,17 +45,11 @@ })->throws(BitkubResponseErrorCodeException::class, 'Invalid JSON payload'); it('should not throw exception when error code is 0', function () { - $stream = $this->createMock(\Psr\Http\Message\StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode([ + $psrRequest = $this->createMock(PsrRequestInterface::class); + $psrResponse = MockHttpClient::response(200, json_encode([ 'error' => 0, ])); - $psrResponse = $this->createMock(PsrResponseInterface::class); - $psrResponse->method('getStatusCode')->willReturn(200); - $psrResponse->method('getBody')->willReturn($stream); - - $psrRequest = $this->createMock(PsrRequestInterface::class); - $response = new Response($psrRequest, $psrResponse); $response = new ResponseWithValidateErrorCode($response); diff --git a/tests/SystemEndpointTest.php b/tests/SystemEndpointTest.php new file mode 100644 index 0000000..8351a0d --- /dev/null +++ b/tests/SystemEndpointTest.php @@ -0,0 +1,29 @@ +setCredentials('test', 'secret') + ->build(); + + $system = $client->system(); + + expect($system)->toBeInstanceOf(SystemEndpoint::class); +}); + +it('can get server timestamp', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, '1702793384662')); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $system = $client->system(); + + expect((int) $system->serverTimestamp()->body())->toBe(1702793384662); +}); diff --git a/tests/UserEndpointTest.php b/tests/UserEndpointTest.php new file mode 100644 index 0000000..a1135e2 --- /dev/null +++ b/tests/UserEndpointTest.php @@ -0,0 +1,107 @@ +setCredentials('test', 'secret') + ->build(); + + $user = $client->user(); + + expect($user)->toBeInstanceOf(UserEndpoint::class); +}); + +it('can call tradingCredits success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => 100, + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->user()->tradingCredits()->throw(); + + expect($response->json('result'))->toBe(100); +}); + +it('can call userLimits success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + 'limits' => [ + 'crypto' => [ + 'deposit' => 0.88971929, + 'withdraw' => 0.88971929, + ], + 'fiat' => [ + 'deposit' => 200000, + 'withdraw' => 200000, + ], + ], + 'usage' => [ + 'crypto' => [ + 'deposit' => 0, + 'withdraw' => 0, + 'deposit_percentage' => 0, + 'withdraw_percentage' => 0, + 'deposit_thb_equivalent' => 0, + 'withdraw_thb_equivalent' => 0, + ], + 'fiat' => [ + 'deposit' => 0, + 'withdraw' => 0, + 'deposit_percentage' => 0, + 'withdraw_percentage' => 0, + ], + ], + 'rate' => 224790, + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->user()->userLimits()->throw(); + + expect($response->json('result'))->toBe([ + 'limits' => [ + 'crypto' => [ + 'deposit' => 0.88971929, + 'withdraw' => 0.88971929, + ], + 'fiat' => [ + 'deposit' => 200000, + 'withdraw' => 200000, + ], + ], + 'usage' => [ + 'crypto' => [ + 'deposit' => 0, + 'withdraw' => 0, + 'deposit_percentage' => 0, + 'withdraw_percentage' => 0, + 'deposit_thb_equivalent' => 0, + 'withdraw_thb_equivalent' => 0, + ], + 'fiat' => [ + 'deposit' => 0, + 'withdraw' => 0, + 'deposit_percentage' => 0, + 'withdraw_percentage' => 0, + ], + ], + 'rate' => 224790, + ]); +}); From 9400bbbfeaaa6693b1ba30b4bbee05c78d669919 Mon Sep 17 00:00:00 2001 From: parsilver Date: Sun, 17 Dec 2023 16:44:43 +0700 Subject: [PATCH 2/2] Add more coverage --- tests/CryptoEndpointTest.php | 151 +++++++++++++++- tests/MarketEndpointTest.php | 341 ++++++++++++++++++++++++++++++++++- 2 files changed, 482 insertions(+), 10 deletions(-) diff --git a/tests/CryptoEndpointTest.php b/tests/CryptoEndpointTest.php index b33d3d1..1db06e3 100644 --- a/tests/CryptoEndpointTest.php +++ b/tests/CryptoEndpointTest.php @@ -48,12 +48,9 @@ expect($response->json('result.0'))->toBeArray(); expect($response->json('result.0.currency'))->toBe('BTC'); - expect($response->json('result.0.address'))->toBe('3BtxdKw6XSbneNvmJTLVHS9XfNYM7VAe8k'); expect($response->json('result.0.tag'))->toBe(0); expect($response->json('result.0.time'))->toBe(1570893867); - expect($response->json('pagination'))->toBeArray(); - expect($response->json('pagination.page'))->toBe(1); expect($response->json('pagination.last'))->toBe(1); }); @@ -88,11 +85,155 @@ expect($response->getStatusCode())->toBe(200); expect($response->json('result'))->toBeArray(); - expect($response->json('result.txid'))->toBe('BTCWD0000012345'); - expect($response->json('result.adr'))->toBe('4asyjKw6XScneNvhJTLVHS9XfNYM7VBf8x'); expect($response->json('result.mem'))->toBe(''); expect($response->json('result.cur'))->toBe('BTC'); expect($response->json('result.amt'))->toBe(0.1); expect($response->json('result.fee'))->toBe(0.0002); expect($response->json('result.ts'))->toBe(1569999999); }); + +it('can call internalWithdrawal success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + 'txn' => 'BTCWD0000012345', + 'adr' => '4asyjKw6XScneNvhJTLVHS9XfNYM7VBf8x', + 'mem' => '', + 'cur' => 'BTC', + 'amt' => 0.1, + 'fee' => 0.0002, + 'ts' => 1569999999, + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->crypto()->internalWithdrawal([ + 'sym' => 'BTC', + 'amt' => 0.1, + 'adr' => '4asyjKw6XScneNvhJTLVHS9XfNYM7VBf8x', + 'mem' => '', + ])->throw(); + + expect($response->getStatusCode())->toBe(200); + expect($response->json('result'))->toBeArray(); + + expect($response->json('result.txn'))->toBe('BTCWD0000012345'); + expect($response->json('result.mem'))->toBe(''); + expect($response->json('result.cur'))->toBe('BTC'); + expect($response->json('result.fee'))->toBe(0.0002); + expect($response->json('result.ts'))->toBe(1569999999); +}); + +it('can call depositHistory success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + [ + 'hash' => 'XRPWD0000100276', + 'currency' => 'XRP', + 'amount' => 5.75111474, + 'from_address' => 'sender address', + 'to_address' => 'recipient address', + 'confirmations' => 1, + 'status' => 'complete', + 'time' => 1570893867, + ], + ], + 'pagination' => [ + 'page' => 1, + 'last' => 1, + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->crypto()->depositHistory([ + 'p' => 1, + 'lmt' => 1, + ])->throw(); + + expect($response->getStatusCode())->toBe(200); + expect($response->json('result'))->toBeArray(); + expect($response->json('result.0'))->toBeArray(); + + expect($response->json('result.0.hash'))->toBe('XRPWD0000100276'); + expect($response->json('result.0.time'))->toBe(1570893867); + + expect($response->json('pagination.last'))->toBe(1); +}); + +it('can call withdrawalHistory success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + [ + 'txn_id' => 'XRPWD0000100276', + 'hash' => 'send_internal', + 'currency' => 'XRP', + 'amount' => 5.75111474, + 'fee' => 0.01, + 'address' => 'rpXTzCuXtjiPDFysxq8uNmtZBe9Xo97JbW', + 'status' => 'complete', + 'time' => 1570893493, + ], + ], + 'pagination' => [ + 'page' => 1, + 'last' => 1, + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->crypto()->withdrawalHistory([ + 'p' => 1, + 'lmt' => 1, + ])->throw(); + + expect($response->getStatusCode())->toBe(200); + expect($response->json('result'))->toBeArray(); + expect($response->json('result.0'))->toBeArray(); + + expect($response->json('result.0.txn_id'))->toBe('XRPWD0000100276'); +}); + +it('can call generateAddress success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn () => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn () => MockHttpClient::response(200, [ + 'error' => 0, + 'result' => [ + 'currency' => 'ETH', + 'address' => '0x520165471daa570ab632dd504c6af257bd36edfb', + 'memo' => '', + ], + ])); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $response = $client->crypto()->generateAddress('THB_ETH')->throw(); + + expect($response->getStatusCode())->toBe(200); + expect($response->json('result'))->toBeArray(); + + expect($response->json('result.currency'))->toBe('ETH'); +}); diff --git a/tests/MarketEndpointTest.php b/tests/MarketEndpointTest.php index 7044d8a..c929463 100644 --- a/tests/MarketEndpointTest.php +++ b/tests/MarketEndpointTest.php @@ -38,10 +38,341 @@ $response = $market->balances(); expect($response)->toBeInstanceOf(ResponseInterface::class); - expect($response->json('result'))->toBe([ - 'THB' => [ - 'available' => 1000, - 'reserved' => 0, - ], + expect($response->json('result.THB.available'))->toBe(1000); +}); + +it('should get symbols success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'THB_BTC' => [ + 'id' => 1, + 'last' => 1000, + 'lowestAsk' => 1000, + 'highestBid' => 1000, + 'percentChange' => 0, + 'baseVolume' => 0, + 'quoteVolume' => 0, + 'isFrozen' => 0, + 'high24hr' => 0, + 'low24hr' => 0, + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->symbols(); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.THB_BTC.id'))->toBe(1); +}); + +it('should get ticker success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'THB_BTC' => [ + 'id' => 1, + 'last' => 1000, + 'lowestAsk' => 1000, + 'highestBid' => 1000, + 'percentChange' => 0, + 'baseVolume' => 0, + 'quoteVolume' => 0, + 'isFrozen' => 0, + 'high24hr' => 0, + 'low24hr' => 0, + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->ticker(); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.THB_BTC.id'))->toBe(1); +}); + +it('should get trades success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + [ + 1529516287, + 10000.00, + 0.09975000, + 'BUY', + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->trades([ + 'sym' => 'THB_BTC', + ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.0.0'))->toBe(1529516287); + expect($response->json('result.0.2'))->toBe(0.09975000); +}); + +it('should get bids success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + [ + '1', // order id + 1529453033, // timestamp + 997.50, // volume + 10000.00, // rate + 0.09975000, // amount + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->bids([ + 'sym' => 'THB_BTC', + ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.0.0'))->toBe('1'); + expect($response->json('result.0.2'))->toBe(997.50); +}); + +it('should get asks success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + [ + '1', // order id + 1529453033, // timestamp + 997.50, // volume + 10000.00, // rate + 0.09975000, // amount + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->asks([ + 'sym' => 'THB_BTC', ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.0.0'))->toBe('1'); + expect($response->json('result.0.2'))->toBe(997.50); +}); + +it('should get books success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'bids' => [ + [ + '1', // order id + 1529453033, // timestamp + 997.50, // volume + 10000.00, // rate + 0.09975000, // amount + ], + ], + 'asks' => [ + [ + '1', // order id + 1529453033, // timestamp + 997.50, // volume + 10000.00, // rate + 0.09975000, // amount + ], + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->books([ + 'sym' => 'THB_BTC', + ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.bids.0.0'))->toBe('1'); + expect($response->json('result.bids.0.2'))->toBe(997.50); + expect($response->json('result.asks.0.0'))->toBe('1'); + expect($response->json('result.asks.0.2'))->toBe(997.50); +}); + +it('should get wallet success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'THB' => [ + 'available' => 1000, + 'reserved' => 0, + ], + 'BTC' => [ + 'available' => 0.09975000, + 'reserved' => 0, + ], + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->wallet(); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.THB.available'))->toBe(1000); + expect($response->json('result.BTC.available'))->toBe(0.09975000); +}); + +it('should call placeBid success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'id' => '1', + 'hash' => 'fwQ6dnQWQPs4cbatF5Am2xCDP1J', + 'typ' => 'limit', + 'amt' => 1000, + 'rat' => 10000, + 'fee' => 2.5, + 'cre' => 2.5, + 'rec' => 0.06666666, + 'ts' => 1533834547, + 'ci' => 'input_client_id', + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->placeBid([ + 'sym' => 'THB_BTC', + 'amt' => 1000, + 'rat' => 10000, + 'typ' => 'limit', + 'client_id' => 'xxxx', + ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.id'))->toBe('1'); + expect($response->json('result.hash'))->toBe('fwQ6dnQWQPs4cbatF5Am2xCDP1J'); +}); + +it('should call placeAsk success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + 'result' => [ + 'id' => '1', + 'hash' => 'fwQ6dnQWQPs4cbatF5Am2xCDP1J', + 'typ' => 'limit', + 'amt' => 1000, + 'rat' => 10000, + 'fee' => 2.5, + 'cre' => 2.5, + 'rec' => 0.06666666, + 'ts' => 1533834547, + 'ci' => 'input_client_id', + ], + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->placeAsk([ + 'sym' => 'THB_BTC', + 'amt' => 1000, + 'rat' => 10000, + 'typ' => 'limit', + 'client_id' => 'xxxx', + ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('result.id'))->toBe('1'); + expect($response->json('result.hash'))->toBe('fwQ6dnQWQPs4cbatF5Am2xCDP1J'); +}); + +it('should call cancelOrder success', function () { + $psrClient = MockHttpClient::make() + ->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp()) + ->addSequence(fn ($client) => $client->createResponse(200, json_encode([ + 'error' => 0, + ]))); + + $client = ClientBuilder::create() + ->setCredentials('test', 'secret') + ->setHttpClient($psrClient) + ->build(); + + $market = $client->market(); + + $response = $market->cancelOrder([ + 'sym' => 'THB_BTC', + 'id' => '1', + 'hash' => 'fwQ6dnQWQPs4cbatF5Am2xCDP1J', + 'sd' => 'buy', + ]); + + expect($response)->toBeInstanceOf(ResponseInterface::class); + expect($response->json('error'))->toBe(0); });