Skip to content

Commit ab0e4e3

Browse files
committed
Add 62 new tests to increase coverage and fix Message::json() bug
- Fix TypeError in Message::json($key) when called on invalid JSON body - Add tests for filterParams, GenerateSignatureV3, error codes, URI factory, PendingRequest send/interceptors, PSR-7 decorator delegation, lazy singletons, WebSocket client/builder edge cases, and market/orderbook endpoints
1 parent bccafe0 commit ab0e4e3

15 files changed

Lines changed: 851 additions & 0 deletions

src/WebSocket/Message.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public function json($key = null)
4444
return $this->jsonDecoded;
4545
}
4646

47+
if ($this->jsonDecoded === null) {
48+
return null;
49+
}
50+
4751
return Arr::get($this->jsonDecoded, $key);
4852
}
4953

tests/AuthorizerTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,53 @@
3333

3434
expect($signature)->toBe('ae6fd3dc7d85ebea023e54292fa6eebaeea6dc02002433c51b57136eeb0a03e5');
3535
});
36+
37+
it('generates different signatures with non-empty query', function () {
38+
$secret = 'secret';
39+
$timestamp = 1630483200000;
40+
$method = 'GET';
41+
$path = '/api/market/trades';
42+
43+
$sigWithoutQuery = Utility::generateSignature($secret, $timestamp, $method, $path, '', '');
44+
$sigWithQuery = Utility::generateSignature($secret, $timestamp, $method, $path, '?sym=THB_BTC', '');
45+
46+
expect($sigWithoutQuery)->not->toBe($sigWithQuery);
47+
});
48+
49+
it('generates different signatures with non-empty payload', function () {
50+
$secret = 'secret';
51+
$timestamp = 1630483200000;
52+
$method = 'POST';
53+
$path = '/api/v3/market/place-bid';
54+
55+
$sigWithoutPayload = Utility::generateSignature($secret, $timestamp, $method, $path, '', '');
56+
$sigWithPayload = Utility::generateSignature($secret, $timestamp, $method, $path, '', '{"sym":"THB_BTC","amt":1000}');
57+
58+
expect($sigWithoutPayload)->not->toBe($sigWithPayload);
59+
});
60+
61+
it('generates different signatures for different methods', function () {
62+
$secret = 'secret';
63+
$timestamp = 1630483200000;
64+
$path = '/api/market/trades';
65+
66+
$sigGet = Utility::generateSignature($secret, $timestamp, 'GET', $path, '', '');
67+
$sigPost = Utility::generateSignature($secret, $timestamp, 'POST', $path, '', '');
68+
69+
expect($sigGet)->not->toBe($sigPost);
70+
});
71+
72+
it('generates consistent signatures for same inputs', function () {
73+
$secret = 'secret';
74+
$timestamp = 1630483200000;
75+
$method = 'POST';
76+
$path = '/api/v3/market/balances';
77+
$query = '?sym=THB_BTC';
78+
$payload = '{"amt":1000}';
79+
80+
$sig1 = Utility::generateSignature($secret, $timestamp, $method, $path, $query, $payload);
81+
$sig2 = Utility::generateSignature($secret, $timestamp, $method, $path, $query, $payload);
82+
83+
expect($sig1)->toBe($sig2);
84+
expect(strlen($sig1))->toBe(64); // SHA-256 hex
85+
});

tests/ClientBuilderTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,94 @@
6464
ClientBuilder::create()
6565
->setRetries(-1);
6666
})->throws(\InvalidArgumentException::class, 'Retries must be greater than or equal to 0.');
67+
68+
it('can set retries to zero', function () {
69+
$client = ClientBuilder::create()
70+
->setCredentials('test', 'secret')
71+
->setRetries(0)
72+
->build();
73+
74+
expect($client)->toBeInstanceOf(Client::class);
75+
});
76+
77+
it('returns same market endpoint instance (lazy singleton)', function () {
78+
$client = ClientBuilder::create()
79+
->setCredentials('test', 'secret')
80+
->build();
81+
82+
$first = $client->market();
83+
$second = $client->market();
84+
85+
expect($first)->toBe($second);
86+
});
87+
88+
it('returns same crypto endpoint instance (lazy singleton)', function () {
89+
$client = ClientBuilder::create()
90+
->setCredentials('test', 'secret')
91+
->build();
92+
93+
$first = $client->crypto();
94+
$second = $client->crypto();
95+
96+
expect($first)->toBe($second);
97+
});
98+
99+
it('returns same user endpoint instance (lazy singleton)', function () {
100+
$client = ClientBuilder::create()
101+
->setCredentials('test', 'secret')
102+
->build();
103+
104+
$first = $client->user();
105+
$second = $client->user();
106+
107+
expect($first)->toBe($second);
108+
});
109+
110+
it('returns same system endpoint instance (lazy singleton)', function () {
111+
$client = ClientBuilder::create()
112+
->setCredentials('test', 'secret')
113+
->build();
114+
115+
$first = $client->system();
116+
$second = $client->system();
117+
118+
expect($first)->toBe($second);
119+
});
120+
121+
it('can access getTransport', function () {
122+
$client = ClientBuilder::create()
123+
->setCredentials('test', 'secret')
124+
->build();
125+
126+
expect($client->getTransport())->toBeInstanceOf(\Farzai\Transport\Transport::class);
127+
});
128+
129+
it('can access getConfig with correct keys', function () {
130+
$client = ClientBuilder::create()
131+
->setCredentials('my-api-key', 'my-secret')
132+
->build();
133+
134+
$config = $client->getConfig();
135+
136+
expect($config)->toHaveKey('api_key', 'my-api-key');
137+
expect($config)->toHaveKey('secret', 'my-secret');
138+
});
139+
140+
it('can access getLogger', function () {
141+
$client = ClientBuilder::create()
142+
->setCredentials('test', 'secret')
143+
->build();
144+
145+
expect($client->getLogger())->toBeInstanceOf(\Psr\Log\LoggerInterface::class);
146+
});
147+
148+
it('uses custom logger when set', function () {
149+
$logger = new \Psr\Log\NullLogger;
150+
151+
$client = ClientBuilder::create()
152+
->setCredentials('test', 'secret')
153+
->setLogger($logger)
154+
->build();
155+
156+
expect($client->getLogger())->toBe($logger);
157+
});

tests/ErrorCodesTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,22 @@
1212
expect(ErrorCodes::getDescription(ErrorCodes::INVALID_USER))
1313
->toBe('Invalid user');
1414
});
15+
16+
it('should return unknown error code message for unrecognized code', function () {
17+
expect(ErrorCodes::getDescription(9999))
18+
->toBe('Unknown error code: 9999');
19+
});
20+
21+
it('should return correct descriptions for all known codes', function () {
22+
expect(ErrorCodes::getDescription(ErrorCodes::NO_ERROR))->toBe('No error');
23+
expect(ErrorCodes::getDescription(ErrorCodes::SERVER_ERROR))->toBe('Server error (please contact support)');
24+
expect(ErrorCodes::getDescription(ErrorCodes::INSUFFICIENT_BALANCE))->toBe('Insufficient balance');
25+
});
26+
27+
it('all() includes both constant values and the DESCRIPTIONS array', function () {
28+
$all = ErrorCodes::all();
29+
30+
expect($all)->toContain(ErrorCodes::NO_ERROR);
31+
expect($all)->toContain(ErrorCodes::INVALID_JSON_PAYLOAD);
32+
expect($all)->toContain(ErrorCodes::SERVER_ERROR);
33+
});

tests/GenerateSignatureV3Test.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Farzai\Bitkub\ClientBuilder;
6+
use Farzai\Bitkub\Requests\GenerateSignatureV3;
7+
use Farzai\Bitkub\Tests\MockHttpClient;
8+
9+
it('applies signature headers to request', function () {
10+
$psrClient = MockHttpClient::make()
11+
->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp());
12+
13+
$client = ClientBuilder::create()
14+
->setCredentials('my-api-key', 'my-secret')
15+
->setHttpClient($psrClient)
16+
->build();
17+
18+
$config = $client->getConfig();
19+
$interceptor = new GenerateSignatureV3($config, $client);
20+
21+
// Build a simple request
22+
$request = (new \Farzai\Transport\RequestBuilder)
23+
->method('POST')
24+
->uri('/api/v3/market/balances')
25+
->build();
26+
27+
$signedRequest = $interceptor->apply($request);
28+
29+
expect($signedRequest->getHeaderLine('X-BTK-APIKEY'))->toBe('my-api-key');
30+
expect($signedRequest->getHeaderLine('X-BTK-SIGN'))->not->toBeEmpty();
31+
expect($signedRequest->getHeaderLine('X-BTK-TIMESTAMP'))->not->toBeEmpty();
32+
});
33+
34+
it('applies signature with query string present', function () {
35+
$psrClient = MockHttpClient::make()
36+
->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp());
37+
38+
$client = ClientBuilder::create()
39+
->setCredentials('my-api-key', 'my-secret')
40+
->setHttpClient($psrClient)
41+
->build();
42+
43+
$config = $client->getConfig();
44+
$interceptor = new GenerateSignatureV3($config, $client);
45+
46+
// Build a request with query params
47+
$request = (new \Farzai\Transport\RequestBuilder)
48+
->method('GET')
49+
->uri('/api/v3/market/my-open-orders')
50+
->withQuery(['sym' => 'THB_BTC'])
51+
->build();
52+
53+
$signedRequest = $interceptor->apply($request);
54+
55+
expect($signedRequest->getHeaderLine('X-BTK-APIKEY'))->toBe('my-api-key');
56+
expect($signedRequest->getHeaderLine('X-BTK-SIGN'))->not->toBeEmpty();
57+
// Signature should differ from a request without query
58+
$signedRequest2 = $interceptor->apply(
59+
(new \Farzai\Transport\RequestBuilder)
60+
->method('GET')
61+
->uri('/api/v3/market/my-open-orders')
62+
->build()
63+
);
64+
65+
expect($signedRequest->getHeaderLine('X-BTK-SIGN'))
66+
->not->toBe($signedRequest2->getHeaderLine('X-BTK-SIGN'));
67+
});
68+
69+
it('applies signature with request body', function () {
70+
$psrClient = MockHttpClient::make()
71+
->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp());
72+
73+
$client = ClientBuilder::create()
74+
->setCredentials('my-api-key', 'my-secret')
75+
->setHttpClient($psrClient)
76+
->build();
77+
78+
$config = $client->getConfig();
79+
$interceptor = new GenerateSignatureV3($config, $client);
80+
81+
// Build a request with a body
82+
$request = (new \Farzai\Transport\RequestBuilder)
83+
->method('POST')
84+
->uri('/api/v3/market/place-bid')
85+
->withJson(['sym' => 'THB_BTC', 'amt' => 1000, 'rat' => 15000, 'typ' => 'limit'])
86+
->build();
87+
88+
$signedRequest = $interceptor->apply($request);
89+
90+
expect($signedRequest->getHeaderLine('X-BTK-APIKEY'))->toBe('my-api-key');
91+
expect($signedRequest->getHeaderLine('X-BTK-SIGN'))->not->toBeEmpty();
92+
expect($signedRequest->getHeaderLine('X-BTK-TIMESTAMP'))->not->toBeEmpty();
93+
});
94+
95+
it('reuses timestamp within sync interval', function () {
96+
$psrClient = MockHttpClient::make()
97+
->addSequence(fn ($client) => MockHttpClient::responseServerTimestamp());
98+
99+
$client = ClientBuilder::create()
100+
->setCredentials('my-api-key', 'my-secret')
101+
->setHttpClient($psrClient)
102+
->build();
103+
104+
$config = $client->getConfig();
105+
$interceptor = new GenerateSignatureV3($config, $client);
106+
107+
$request = (new \Farzai\Transport\RequestBuilder)
108+
->method('POST')
109+
->uri('/api/v3/market/balances')
110+
->build();
111+
112+
// First call syncs with server
113+
$signed1 = $interceptor->apply($request);
114+
115+
// Second call should reuse the cached drift (no second HTTP call needed)
116+
$signed2 = $interceptor->apply($request);
117+
118+
expect($signed1->getHeaderLine('X-BTK-APIKEY'))->toBe('my-api-key');
119+
expect($signed2->getHeaderLine('X-BTK-APIKEY'))->toBe('my-api-key');
120+
});

tests/PendingRequestTest.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,95 @@
210210
expect($response)->toBeInstanceOf(\Farzai\Bitkub\Responses\ResponseWithValidateErrorCode::class);
211211
expect($response->statusCode())->toBe(200);
212212
});
213+
214+
it('can send request with interceptors applied', function () {
215+
$psrClient = \Farzai\Bitkub\Tests\MockHttpClient::make()
216+
->addSequence(\Farzai\Bitkub\Tests\MockHttpClient::response(200, json_encode([
217+
'error' => 0,
218+
'result' => 'ok',
219+
])));
220+
221+
$client = ClientBuilder::create()
222+
->setCredentials('test', 'secret')
223+
->setHttpClient($psrClient)
224+
->build();
225+
226+
$interceptorCalled = false;
227+
$interceptor = $this->createMock(\Farzai\Bitkub\Contracts\RequestInterceptor::class);
228+
$interceptor->method('apply')->willReturnCallback(function ($request) use (&$interceptorCalled) {
229+
$interceptorCalled = true;
230+
231+
return $request->withHeader('X-Custom', 'test');
232+
});
233+
234+
$pending = new PendingRequest($client, 'GET', '/api/market/symbols');
235+
$response = $pending->withInterceptor($interceptor)->send();
236+
237+
expect($interceptorCalled)->toBeTrue();
238+
expect($response)->toBeInstanceOf(\Farzai\Transport\Contracts\ResponseInterface::class);
239+
expect($response->json('result'))->toBe('ok');
240+
});
241+
242+
it('can send request with multiple interceptors applied in order', function () {
243+
$psrClient = \Farzai\Bitkub\Tests\MockHttpClient::make()
244+
->addSequence(\Farzai\Bitkub\Tests\MockHttpClient::response(200, json_encode([
245+
'error' => 0,
246+
])));
247+
248+
$client = ClientBuilder::create()
249+
->setCredentials('test', 'secret')
250+
->setHttpClient($psrClient)
251+
->build();
252+
253+
$order = [];
254+
255+
$interceptor1 = $this->createMock(\Farzai\Bitkub\Contracts\RequestInterceptor::class);
256+
$interceptor1->method('apply')->willReturnCallback(function ($request) use (&$order) {
257+
$order[] = 'first';
258+
259+
return $request;
260+
});
261+
262+
$interceptor2 = $this->createMock(\Farzai\Bitkub\Contracts\RequestInterceptor::class);
263+
$interceptor2->method('apply')->willReturnCallback(function ($request) use (&$order) {
264+
$order[] = 'second';
265+
266+
return $request;
267+
});
268+
269+
$pending = new PendingRequest($client, 'GET', '/api/market/symbols');
270+
$pending->withInterceptor($interceptor1)->withInterceptor($interceptor2)->send();
271+
272+
expect($order)->toBe(['first', 'second']);
273+
});
274+
275+
it('withHeaders merges with existing headers', function () {
276+
$client = ClientBuilder::create()
277+
->setCredentials('test', 'secret')
278+
->build();
279+
280+
$pending = new PendingRequest($client, 'GET', '/api/market/balances');
281+
$pending->withHeaders(['X-First' => 'one']);
282+
$pending->withHeaders(['X-Second' => 'two']);
283+
284+
$request = $pending->createRequest('GET', '/api/market/balances', [
285+
'headers' => ['X-First' => 'one', 'X-Second' => 'two'],
286+
]);
287+
288+
expect($request->getHeaderLine('X-First'))->toBe('one');
289+
expect($request->getHeaderLine('X-Second'))->toBe('two');
290+
});
291+
292+
it('createRequest with empty query array omits query string', function () {
293+
$client = ClientBuilder::create()
294+
->setCredentials('test', 'secret')
295+
->build();
296+
297+
$pending = new PendingRequest($client, 'GET', '/api/market/ticker');
298+
299+
$request = $pending->createRequest('GET', '/api/market/ticker', [
300+
'query' => [],
301+
]);
302+
303+
expect($request->getUri()->getQuery())->toBe('');
304+
});

0 commit comments

Comments
 (0)