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(); } }