238 lines
15 KiB
PHP
238 lines
15 KiB
PHP
<?php
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ApplicationContractTest extends TestCase
|
|
{
|
|
public function testInternalControllersUseAuthenticatedBaseController(): void
|
|
{
|
|
$exceptions = array('Auth.php', 'Migrate.php', 'Accountingrepair.php', 'AttendanceWebhook.php');
|
|
foreach (glob(APPPATH . 'controllers/*.php') as $file) {
|
|
if (in_array(basename($file), $exceptions, true) || stripos(basename($file), 'check.php') !== false) continue;
|
|
$source = file_get_contents($file);
|
|
if (strpos($source, 'class ') === false) continue;
|
|
$isAuthenticated = preg_match('/extends\s+(MY_Controller|MY_Admin_Controller)/', $source);
|
|
$isCliOnly = strpos($source, 'is_cli_request()') !== false || strpos($source, 'is_cli()') !== false;
|
|
self::assertTrue((bool) ($isAuthenticated || $isCliOnly), basename($file) . ' melewati kontrol session/permission terpusat dan bukan controller CLI.');
|
|
}
|
|
}
|
|
|
|
public function testAjaxSessionExpiryReturnsJson401(): void
|
|
{
|
|
$source = file_get_contents(APPPATH . 'core/MY_Controller.php');
|
|
self::assertStringContainsString('is_ajax_request()', $source);
|
|
self::assertStringContainsString('set_status_header(401)', $source);
|
|
self::assertStringContainsString("'status' => false", $source);
|
|
}
|
|
|
|
public function testResponsiveShellHasDesktopAndMobileRules(): void
|
|
{
|
|
$css = file_get_contents(dirname(APPPATH) . '/assets/css/app-final.css');
|
|
self::assertStringContainsString('@media', $css);
|
|
self::assertMatchesRegularExpression('/\.mobile-action-bar\s*\{display\s*:\s*none!important\}/i', $css, 'Menu bawah wajib disembunyikan pada desktop.');
|
|
self::assertMatchesRegularExpression('/max-width\s*:\s*991(?:\.98)?px[\s\S]*\.mobile-action-bar\s*\{[\s\S]*display\s*:\s*grid!important/i', $css, 'Menu bawah wajib tersedia pada HP.');
|
|
self::assertMatchesRegularExpression('/max-width\s*:\s*(991|768|767)(?:\.98)?px/i', $css, 'Breakpoint HP belum tersedia.');
|
|
}
|
|
|
|
public function testUiPreventsRepeatedSubmitAndHandlesAjaxFailure(): void
|
|
{
|
|
$js = file_get_contents(dirname(APPPATH) . '/assets/js/app-ui.min.js');
|
|
$network = file_get_contents(dirname(APPPATH) . '/assets/js/app-network.js');
|
|
self::assertMatchesRegularExpression('/submitting|is-submitting|disabled/i', $js);
|
|
self::assertMatchesRegularExpression('/ajaxError|status\s*===\s*401|\.fail\s*\(/i', $network);
|
|
}
|
|
|
|
public function testMoneyInputsUseCentralParserBeforeAjax(): void
|
|
{
|
|
$ui = file_get_contents(dirname(APPPATH) . '/assets/js/app-ui.min.js');
|
|
$journal = file_get_contents(APPPATH . 'views/jurnal/layout.php');
|
|
self::assertStringContainsString('window.parseMoney=', $ui);
|
|
self::assertStringContainsString("replace(/,/g,'')", $ui);
|
|
self::assertStringContainsString("+(hasDecimal?','+decimal:'')", $ui);
|
|
self::assertStringNotContainsString("parseFloat($(this).find('.debit').val())", $journal);
|
|
self::assertStringContainsString("window.parseMoney($(this).find('.debit').val())", $journal);
|
|
}
|
|
|
|
public function testMasterAdminHasFullAccessAndApprovalOverride(): void
|
|
{
|
|
$base = file_get_contents(APPPATH . 'core/MY_Controller.php');
|
|
$approval = file_get_contents(APPPATH . 'libraries/ApprovalService.php');
|
|
$roles = file_get_contents(APPPATH . 'controllers/Roles.php');
|
|
self::assertStringContainsString("userdata('role') === 'Admin'", $base);
|
|
self::assertStringContainsString('$masterAdmin', $approval);
|
|
self::assertStringContainsString('is_super_admin', $approval);
|
|
self::assertStringContainsString('Role wajib mempunyai minimal satu permission.', $roles);
|
|
self::assertStringContainsString('Role bawaan aplikasi tidak dapat dihapus.', $roles);
|
|
}
|
|
|
|
public function testProfessionalReportsHaveIndonesianFinancialStatements(): void
|
|
{
|
|
$navigation = file_get_contents(APPPATH . 'libraries/NavigationService.php');
|
|
$view = file_get_contents(APPPATH . 'views/professional_reports/index.php');
|
|
self::assertStringContainsString("'label'=>'Laporan Profesional'", $navigation);
|
|
self::assertStringContainsString("'balance_sheet'=>'Laporan Neraca'", $view);
|
|
self::assertStringContainsString("'profit_loss'=>'Laporan Laba Rugi'", $view);
|
|
self::assertStringContainsString('TOTAL AKTIVA', $view);
|
|
self::assertStringContainsString('TOTAL PASIVA', $view);
|
|
self::assertStringContainsString('Laba Bersih Berjalan', $view);
|
|
self::assertStringContainsString('TOTAL PENDAPATAN', $view);
|
|
self::assertStringContainsString('TOTAL BEBAN', $view);
|
|
self::assertStringContainsString('$accountCode', $view);
|
|
}
|
|
|
|
public function testPurchasePaymentsSeparatePrincipalAndBankCharge(): void
|
|
{
|
|
$service = file_get_contents(APPPATH . 'libraries/PurchaseService.php');
|
|
$purchases = file_get_contents(APPPATH . 'controllers/Purchases.php');
|
|
$payables = file_get_contents(APPPATH . 'controllers/Payables.php');
|
|
$migration = file_get_contents(APPPATH . 'migrations/20260905000700_add_purchase_payment_bank_charge.php');
|
|
|
|
self::assertStringContainsString('bank_charge_amount', $migration);
|
|
self::assertStringContainsString('total_cash_out', $migration);
|
|
self::assertStringContainsString("accountmappingservice->get('bank_charge_expense')", $service);
|
|
self::assertStringContainsString('$principal=round($gross-$charge,2)', $service);
|
|
self::assertStringContainsString('prepayOrderWithBankCharge', $purchases);
|
|
self::assertStringContainsString('settleSupplierInvoiceMultiWithBankCharge', $purchases);
|
|
self::assertStringContainsString('paymentMultiWithBankCharge', $payables);
|
|
}
|
|
|
|
public function testPurchaseReturnsAreSeparatedAndIntegrated(): void
|
|
{
|
|
$navigation=file_get_contents(APPPATH.'libraries/NavigationService.php');
|
|
$workflow=file_get_contents(APPPATH.'views/purchases/workflow.php');
|
|
$view=file_get_contents(APPPATH.'views/purchase_returns/index.php');
|
|
$service=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php');
|
|
$controller=file_get_contents(APPPATH.'controllers/Purchasereturns.php');
|
|
|
|
self::assertStringContainsString("'label'=>'Retur & Refund'",$navigation);
|
|
self::assertStringNotContainsString("'adjustment'=>'6",$workflow);
|
|
self::assertStringContainsString('purchase-workflow-stepper',$workflow);
|
|
self::assertStringContainsString('adjustment-stepper',$view);
|
|
self::assertStringContainsString('purchase_return_status_history',$service);
|
|
self::assertStringContainsString("movement_type'=>'purchase_return'",$service);
|
|
self::assertStringContainsString("writeLedger('purchase_return'",$service);
|
|
self::assertStringContainsString("accountmappingservice->get('supplier_refund_receivable')",$service);
|
|
self::assertStringContainsString('FOR UPDATE',$service);
|
|
self::assertStringContainsString('idempotency_key',$service);
|
|
self::assertStringContainsString("requirePermission('purchase_returns'",$controller);
|
|
self::assertStringContainsString("requirePermission('purchase_refunds'",$controller);
|
|
self::assertStringContainsString("requirePermission('supplier_debit_notes'",$controller);
|
|
self::assertStringContainsString('attachment-input',$view);
|
|
self::assertStringContainsString('DataTransfer',$view);
|
|
self::assertStringContainsString('<iframe',$view);
|
|
}
|
|
|
|
public function testPurchaseAdjustmentsSupportReversalAuditAndProfessionalReports(): void
|
|
{
|
|
$service=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php');
|
|
$purchaseService=file_get_contents(APPPATH.'libraries/PurchaseService.php');
|
|
$reportService=file_get_contents(APPPATH.'libraries/ProfessionalReportService.php');
|
|
$reportView=file_get_contents(APPPATH.'views/professional_reports/index.php');
|
|
$adjustmentView=file_get_contents(APPPATH.'views/purchase_returns/index.php');
|
|
|
|
self::assertStringContainsString('reverseReturn',$service);
|
|
self::assertStringContainsString('reverseRefundReceipt',$service);
|
|
self::assertStringContainsString('reverseDebitNote',$service);
|
|
self::assertStringContainsString('supplier_refund_status_history',$service);
|
|
self::assertStringContainsString('supplier_debit_note_status_history',$service);
|
|
self::assertStringContainsString("'purchase_returns'",$reportService);
|
|
self::assertStringContainsString("'outstanding_supplier_refunds'",$reportService);
|
|
self::assertStringContainsString('Laporan Retur Pembelian',$reportView);
|
|
self::assertStringContainsString('Outstanding Refund Supplier',$reportView);
|
|
self::assertStringContainsString('preInvoiceReturnDeduction',$purchaseService);
|
|
self::assertStringContainsString("join('goods_receipt_lines grl0'",$purchaseService);
|
|
self::assertStringContainsString('si0.created_at<=pr.shipped_at',$purchaseService);
|
|
self::assertStringNotContainsString('window.confirm(',$adjustmentView);
|
|
self::assertStringNotContainsString('window.alert(',$adjustmentView);
|
|
}
|
|
|
|
public function testProfessionalInventoryUsesOneLedgerAndSafeWorkflow(): void
|
|
{
|
|
$service=file_get_contents(APPPATH.'libraries/ProfessionalInventoryService.php');
|
|
$model=file_get_contents(APPPATH.'models/InventoryModel.php');
|
|
$controller=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php');
|
|
$operations=file_get_contents(APPPATH.'views/inventory_professional/operations.php');
|
|
$pending=file_get_contents(APPPATH.'views/items/draft_items.php');
|
|
self::assertStringContainsString("FROM inventory_ledger WHERE item_id=?",$model);
|
|
self::assertStringContainsString('FOR UPDATE',$service);
|
|
self::assertStringContainsString('idempotency_key',$service);
|
|
self::assertStringContainsString('reservationAction',$service);
|
|
self::assertStringContainsString('reversal_of_id',$service);
|
|
self::assertStringContainsString("requirePermission('items','can_post')",$controller);
|
|
self::assertStringContainsString('serverSide:true',$operations);
|
|
self::assertStringContainsString('Satu dokumen dapat memuat beberapa barang.',$operations);
|
|
self::assertStringNotContainsString('Tambah Master Barang',$pending);
|
|
self::assertStringNotContainsString('window.alert(',$operations);
|
|
self::assertStringNotContainsString('window.confirm(',$operations);
|
|
}
|
|
|
|
public function testInventoryFinalizationCoversRequiredOperationalScenarios(): void
|
|
{
|
|
$service=file_get_contents(APPPATH.'libraries/ProfessionalInventoryService.php');
|
|
$purchase=file_get_contents(APPPATH.'libraries/PurchaseService.php');
|
|
$sales=file_get_contents(APPPATH.'libraries/ReceivableService.php');
|
|
$returns=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php');
|
|
$asset=file_get_contents(APPPATH.'libraries/FixedAssetService.php');
|
|
$technician=file_get_contents(APPPATH.'models/PeralatanTeknisi_model.php');
|
|
$items=file_get_contents(APPPATH.'controllers/Items.php');
|
|
$reports=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php');
|
|
|
|
self::assertStringContainsString("\$tracking==='UNIT'?1",$purchase,'Penerimaan UNIT wajib menghasilkan satu unit per barcode.');
|
|
self::assertStringContainsString('qty_per_barcode',$purchase,'Penerimaan QTY wajib menghormati isi per barcode.');
|
|
self::assertStringContainsString("'posted_count'=>\$posted",$items,'Aktivasi barang berserial harus dapat diposting bertahap.');
|
|
self::assertStringContainsString("\$doc->document_type==='transfer'",$service,'Transfer gudang harus melalui dua sisi ledger.');
|
|
self::assertStringContainsString('reservationAction',$service);
|
|
self::assertStringContainsString("array('release','consume','cancel')",$service);
|
|
self::assertStringContainsString('stok negatif ditolak',$service);
|
|
self::assertStringContainsString('idempotency_key',$service);
|
|
self::assertStringContainsString("\$doc->document_type==='opname'",$service);
|
|
self::assertStringContainsString('reversal_of_id',$service);
|
|
self::assertStringContainsString("writeLedger('purchase_return'",$returns);
|
|
self::assertStringContainsString('purchase_replacements',$returns);
|
|
self::assertStringContainsString("ib.status = 'available'",$technician);
|
|
self::assertStringContainsString('FOR UPDATE',$technician);
|
|
self::assertStringContainsString("where('i.company_id'",$technician);
|
|
self::assertStringContainsString("'document_type'=>'invoice_details'",$sales);
|
|
self::assertStringContainsString("'document_type'=>'asset_capitalization'",$asset);
|
|
self::assertStringContainsString("'reconciliation'",$reports);
|
|
self::assertStringContainsString("'stock_card'",$reports);
|
|
self::assertStringContainsString('limit($length, $start)',$items,'Daftar barcode wajib dipaginasi di server.');
|
|
}
|
|
|
|
public function testInventoryWriteEndpointsAndButtonsArePermissionAware(): void
|
|
{
|
|
$controller=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php');
|
|
$items=file_get_contents(APPPATH.'controllers/Items.php');
|
|
$operations=file_get_contents(APPPATH.'views/inventory_professional/operations.php');
|
|
$pending=file_get_contents(APPPATH.'views/items/draft_items.php');
|
|
self::assertStringContainsString("input->method())!=='POST'",$controller);
|
|
self::assertStringContainsString("input->method()) !== 'POST'",$items);
|
|
self::assertStringContainsString("requirePermission('items','can_approve')",$controller);
|
|
self::assertStringContainsString('permissions.approve',$operations);
|
|
self::assertStringContainsString('$canPost',$items);
|
|
self::assertStringNotContainsString('btn-adjust',$pending);
|
|
self::assertStringNotContainsString('btn-delete',$pending);
|
|
}
|
|
|
|
public function testFixedAssetsUseSingleProfessionalWorkflow(): void
|
|
{
|
|
$controller=file_get_contents(APPPATH.'controllers/Fixedassets.php');
|
|
$legacy=file_get_contents(APPPATH.'controllers/Asset.php');
|
|
$service=file_get_contents(APPPATH.'libraries/FixedAssetService.php');
|
|
$model=file_get_contents(APPPATH.'models/FixedAssetModel.php');
|
|
$view=file_get_contents(APPPATH.'views/fixed_assets/index.php');
|
|
self::assertStringContainsString('FixedAssetService',$controller);
|
|
self::assertStringContainsString("redirect('fixedassets'",$legacy);
|
|
self::assertStringNotContainsString("insert('assets'",$legacy);
|
|
self::assertStringContainsString('FOR UPDATE',$service);
|
|
self::assertStringContainsString("'document_type'=>'asset_capitalization'",$service);
|
|
self::assertStringContainsString("'movement_type'=>'installation'",$service);
|
|
self::assertStringContainsString('idempotency_key',$service);
|
|
self::assertStringContainsString("event_type==='depreciation'",$service);
|
|
self::assertStringContainsString("lifecycle_status NOT IN('disposed','lost','damaged')",$model);
|
|
self::assertStringContainsString('serverSide:true',file_get_contents(dirname(APPPATH).'/assets/js/fixed-assets.js'));
|
|
self::assertStringContainsString('Laporan & Rekonsiliasi',$view);
|
|
self::assertStringNotContainsString('window.alert(',$view);
|
|
self::assertStringNotContainsString('window.confirm(',$view);
|
|
}
|
|
}
|