259 lines
12 KiB
PHP
259 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Services\ShorakaService;
|
|
use App\ShorakaTransaction;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Http\Client\Factory;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ShorakaIntegrationTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->app['config']->set('services.shoraka', [
|
|
'base_url' => 'https://shoraka.test',
|
|
'api_id' => 'API_ID',
|
|
'secret_key' => 'SECRET_KEY',
|
|
'timeout' => 5,
|
|
]);
|
|
// All database writes stay in memory; never run migrations on branch databases.
|
|
$this->app['config']->set('database.default', 'shoraka_test');
|
|
$this->app['config']->set('database.connections.shoraka_test', [
|
|
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
|
|
]);
|
|
DB::purge('shoraka_test');
|
|
require_once __DIR__ . '/../database/migrations/2026_09_15_094818_create_shoraka_transactions_table.php';
|
|
(new CreateShorakaTransactionsTable())->up();
|
|
require_once __DIR__ . '/../database/migrations/2026_10_05_120000_add_kodcaw_to_shoraka_transactions.php';
|
|
(new AddKodcawToShorakaTransactions())->up();
|
|
Http::swap(new Factory());
|
|
// Fail any HTTP request that a test did not explicitly stub.
|
|
Log::shouldReceive('error')->zeroOrMoreTimes();
|
|
}
|
|
|
|
protected function fakeResponse($body, array $headers = [], int $status = 200): void
|
|
{
|
|
Http::fake(function ($request) use ($body, $headers, $status) {
|
|
$this->assertStringStartsWith('https://shoraka.test/api/', $request->url());
|
|
return Http::response($body, $status, $headers);
|
|
});
|
|
}
|
|
|
|
protected function submission(): array
|
|
{
|
|
return [
|
|
'norujukan' => 'SAG-001', 'ownership' => 'Abu Samad',
|
|
'order_amount' => '1000.00', 'murabaha_amount' => '1200.00',
|
|
'tenor_other' => 6,
|
|
];
|
|
}
|
|
|
|
protected function transaction(string $status = 'Active', $orderId = '9999'): ShorakaTransaction
|
|
{
|
|
return ShorakaTransaction::create([
|
|
'norujukan' => 'SAG-001', 'order_id' => $orderId, 'status' => $status,
|
|
'product_type' => 'FINANCING', 'commodity_type' => '000-COPPER',
|
|
'ownership_name' => 'Abu Samad', 'order_amount' => '1000.00',
|
|
'murabaha_amount' => '1200.00', 'attempt_count' => 1,
|
|
]);
|
|
}
|
|
|
|
protected function callbackPayload(): array
|
|
{
|
|
// Published signature example, v1.5 page 14.
|
|
return [
|
|
'apiId' => 'API_ID', 'orderId' => '9999', 'status' => 'Completed',
|
|
'bankName' => 'Test Bank Bhd', 'ownershipName' => 'Abu Samad',
|
|
'commodityType' => '000-COPPER', 'unit' => 'Tonne', 'volume' => '245.678901',
|
|
'productType' => 'FINANCING', 'valueDate' => '08/12/2025', 'cancelDate' => '',
|
|
'orderType' => 'Buy & Sell', 'orderCurrency' => 'MYR',
|
|
'orderAmount' => '1000000.00', 'murabahaAmount' => '1000000.00',
|
|
'tenor' => 'O/N', 'tenorOther' => '', 'tenorOtherUnit' => '',
|
|
'signature' => 'bbc6f59e9c64aea9e30f3c03d30f63e8fc37898d2699c0cdd41e416a518aa122',
|
|
];
|
|
}
|
|
|
|
public function testSubmitSignatureMatchesPublishedExample(): void
|
|
{
|
|
$this->fakeResponse(['orderId' => '12345', 'status' => 'Completed']);
|
|
(new ShorakaService())->submitOrder([
|
|
'product_type' => 'DEPOSIT', 'commodity_type' => '000-COPPER',
|
|
'ownership' => 'Abu Samad', 'value_date' => '10/11/2022',
|
|
'order_currency' => 'MYR', 'order_amount' => 1000000,
|
|
'murabaha_amount' => 1000000, 'tenor' => 'O/N',
|
|
'order_type' => 'Buy & Sell', 'ownership_doc_no' => 'SAG-001',
|
|
]);
|
|
Http::assertSent(function ($request) {
|
|
return $request->method() === 'POST'
|
|
&& $request->url() === 'https://shoraka.test/api/submitorder'
|
|
&& $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded')
|
|
&& $request['order_amount'] === '1000000.00'
|
|
&& $request['ownership_doc_no'] === 'SAG-001'
|
|
&& $request['signature'] === '43970ac7a5c3867133af0dd92fd5eef9a7b415ebdb8b0d1025d4686bc25693bd';
|
|
});
|
|
}
|
|
|
|
public function testStatusUsesPublishedSignatureAndChecksOrderIdentity(): void
|
|
{
|
|
$this->fakeResponse(['orderId' => '12345', 'status' => 'Active']);
|
|
(new ShorakaService())->orderStatus('12345');
|
|
Http::assertSent(function ($request) {
|
|
return $request->method() === 'POST'
|
|
&& $request->url() === 'https://shoraka.test/api/orderstatus'
|
|
&& $request['signature'] === 'add9890b2a08cf47290e8869d4a25b7f4b73cb075c20aa95aa47d1bbfe6b62fc';
|
|
});
|
|
$this->expectException(RuntimeException::class);
|
|
(new ShorakaService())->orderStatus('another-order');
|
|
}
|
|
|
|
public function testCallbackSignatureMatchesPublishedExampleAndRejectsTampering(): void
|
|
{
|
|
$service = new ShorakaService();
|
|
$payload = $this->callbackPayload();
|
|
$this->assertTrue($service->validCallback($payload));
|
|
foreach (ShorakaService::CALLBACK_FIELDS as $field) {
|
|
$changed = $payload;
|
|
$changed[$field] .= 'x';
|
|
$this->assertFalse($service->validCallback($changed), $field);
|
|
$missing = $payload;
|
|
unset($missing[$field]);
|
|
$this->assertFalse($service->validCallback($missing), $field);
|
|
}
|
|
$payload['apiId'] = 'wrong';
|
|
$this->assertFalse($service->validCallback($payload));
|
|
}
|
|
|
|
public function testSubmitPersistsOrderAndRejectsDuplicate(): void
|
|
{
|
|
$this->fakeResponse(['orderId' => '9999', 'status' => 'Completed']);
|
|
$this->json('POST', '/api/shoraka/orders', $this->submission())->seeStatusCode(200);
|
|
$record = ShorakaTransaction::first();
|
|
$this->assertSame('9999', $record->order_id);
|
|
$this->assertSame('Completed', $record->status);
|
|
$this->assertNotNull($record->completed_at);
|
|
$this->assertArrayNotHasKey('signature', $record->request_payload);
|
|
$this->assertSame('OTHERS', $record->request_payload['tenor']);
|
|
$this->json('POST', '/api/shoraka/orders', $this->submission())->seeStatusCode(409);
|
|
Http::assertSentCount(1);
|
|
}
|
|
|
|
public function testAmbiguousFailureBlocksResubmissionAndHidesUpstreamBody(): void
|
|
{
|
|
$this->fakeResponse('SECRET_KEY confidential', [], 500);
|
|
$this->json('POST', '/api/shoraka/orders', $this->submission())->seeStatusCode(502);
|
|
$this->assertStringNotContainsString('SECRET_KEY', $this->response->getContent());
|
|
$record = ShorakaTransaction::first();
|
|
$this->assertSame('UNKNOWN', $record->status);
|
|
$this->assertStringNotContainsString('SECRET_KEY', $record->error_message);
|
|
$this->json('POST', '/api/shoraka/orders', $this->submission())->seeStatusCode(409);
|
|
Http::assertSentCount(1);
|
|
}
|
|
|
|
public function testPendingSubmissionCannotBeSubmittedAgain(): void
|
|
{
|
|
$this->transaction('PENDING', null);
|
|
Http::fake();
|
|
$this->json('POST', '/api/shoraka/orders', $this->submission())->seeStatusCode(409);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function testRefreshUpdatesStatus(): void
|
|
{
|
|
$record = $this->transaction();
|
|
$this->fakeResponse(['orderId' => '9999', 'status' => 'Completed']);
|
|
$this->json('POST', '/api/shoraka/orders/SAG-001/refresh')->seeStatusCode(200);
|
|
$this->assertSame('Completed', $record->fresh()->status);
|
|
$this->assertNotNull($record->fresh()->completed_at);
|
|
}
|
|
|
|
public function testCallbackAcknowledgesAndUsesCurrentStatusNotUnsignedFields(): void
|
|
{
|
|
$record = $this->transaction();
|
|
$payload = $this->callbackPayload();
|
|
$payload['certificateUrl'] = 'https://untrusted.test/file';
|
|
$payload['ownershipDocNo'] = 'other-reference';
|
|
$this->fakeResponse([
|
|
'orderId' => '9999', 'status' => 'Cancelled',
|
|
'certificateUrl' => 'https://shoraka.test/certificate/9999',
|
|
]);
|
|
$this->json('POST', '/api/shoraka/callback', $payload)->seeStatusCode(200);
|
|
$this->assertSame('OK', $this->response->getContent());
|
|
$this->assertSame('Cancelled', $record->fresh()->status);
|
|
$this->assertSame('https://shoraka.test/certificate/9999', $record->fresh()->certificate_url);
|
|
$this->assertArrayNotHasKey('signature', $record->fresh()->callback_payload);
|
|
// Repeated delivery is acknowledged without creating another order.
|
|
$this->json('POST', '/api/shoraka/callback', $payload)->seeStatusCode(200);
|
|
$this->assertSame(1, ShorakaTransaction::count());
|
|
}
|
|
|
|
public function testInvalidCallbackDoesNotMutateOrCallUpstream(): void
|
|
{
|
|
$record = $this->transaction();
|
|
Http::fake();
|
|
$payload = $this->callbackPayload();
|
|
$payload['status'] = 'Cancelled';
|
|
$this->json('POST', '/api/shoraka/callback', $payload)->seeStatusCode(401);
|
|
$this->assertSame('Active', $record->fresh()->status);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function testUnknownCallbackIsNotAcknowledged(): void
|
|
{
|
|
Http::fake();
|
|
$this->json('POST', '/api/shoraka/callback', $this->callbackPayload())->seeStatusCode(404);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function testFailedCallbackRefreshPreservesRecord(): void
|
|
{
|
|
$record = $this->transaction();
|
|
$this->fakeResponse('unavailable', [], 503);
|
|
$this->json('POST', '/api/shoraka/callback', $this->callbackPayload())->seeStatusCode(502);
|
|
$this->assertSame('Active', $record->fresh()->status);
|
|
$this->assertNull($record->fresh()->callback_payload);
|
|
}
|
|
|
|
public function testCertificateReturnsPdfFromFixedEndpoint(): void
|
|
{
|
|
$this->transaction('Completed', '12345');
|
|
$pdf = "%PDF-1.4\nexample";
|
|
$this->fakeResponse($pdf, ['Content-Type' => 'application/pdf']);
|
|
$this->get('/api/shoraka/orders/SAG-001/certificate')->seeStatusCode(200);
|
|
$this->assertSame($pdf, $this->response->getContent());
|
|
$this->assertSame('application/pdf', $this->response->headers->get('Content-Type'));
|
|
Http::assertSent(function ($request) {
|
|
return $request->method() === 'POST'
|
|
&& $request->url() === 'https://shoraka.test/api/certificate'
|
|
&& $request['signature'] === 'add9890b2a08cf47290e8869d4a25b7f4b73cb075c20aa95aa47d1bbfe6b62fc';
|
|
});
|
|
}
|
|
|
|
public function testCertificateRejectsNonPdf(): void
|
|
{
|
|
$this->transaction();
|
|
$this->fakeResponse(['error' => 'not ready']);
|
|
$this->get('/api/shoraka/orders/SAG-001/certificate')->seeStatusCode(502);
|
|
}
|
|
|
|
public function testMissingCredentialsPreventRequests(): void
|
|
{
|
|
$this->app['config']->set('services.shoraka.secret_key', '');
|
|
Http::fake();
|
|
$this->json('POST', '/api/shoraka/orders', $this->submission())->seeStatusCode(503);
|
|
$this->json('POST', '/api/shoraka/callback', $this->callbackPayload())->seeStatusCode(503);
|
|
$this->assertSame(0, ShorakaTransaction::count());
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function testMissingOrderIdCannotBeRefreshedOrDownloaded(): void
|
|
{
|
|
$this->transaction('UNKNOWN', null);
|
|
Http::fake();
|
|
$this->json('POST', '/api/shoraka/orders/SAG-001/refresh')->seeStatusCode(409);
|
|
$this->get('/api/shoraka/orders/SAG-001/certificate')->seeStatusCode(409);
|
|
Http::assertNothingSent();
|
|
}
|
|
}
|