routesFile = ROOTPATH . 'app/Config/Routes.php'; } public function testFinancialBrowserWriteRoutesArePostOnlyAndFiltered(): void { $routes = file($this->routesFile, FILE_IGNORE_NEW_LINES) ?: []; $financialWrites = array_values(array_filter($routes, static function (string $line): bool { return str_contains($line, '$routes->post(') && preg_match('/(refunds|expenses|reimbursements|discount|charges|payment|invoice|purchase|inventory)/i', $line); })); $this->assertNotEmpty($financialWrites); foreach ($financialWrites as $line) { $this->assertStringContainsString("'filter'", $line, $line); $this->assertStringContainsString('auth:', $line, $line); } } public function testFinancialStateChangingRoutesDoNotUseGet(): void { $routes = file($this->routesFile, FILE_IGNORE_NEW_LINES) ?: []; foreach ($routes as $line) { if (!str_contains($line, '$routes->get(')) { continue; } if (! preg_match('/\$routes->get\(\s*[\'"]([^\'"]+)/', $line, $matches)) { continue; } $routePath = $matches[1]; if (!preg_match('/(refunds|expenses|reimbursements|discount|charges|payment|invoice|purchase|inventory)/i', $routePath)) { continue; } $this->assertDoesNotMatchRegularExpression('/(update|store|approve|reverse|void|delete|apply|recalculate)/i', $routePath, $line); } } public function testFinancialWritesDoNotUseGetPostMatchRoutes(): void { $routes = file($this->routesFile, FILE_IGNORE_NEW_LINES) ?: []; $checked = 0; foreach ($routes as $line) { if (!str_contains($line, '$routes->match(') || !str_contains($line, "'get'") || !str_contains($line, "'post'")) { continue; } if (!preg_match('/(refunds|expenses|reimbursements|discount|charges|payment|invoice|purchase|inventory)/i', $line)) { continue; } $checked++; $this->fail('Financial routes must split read GET and write POST handlers: ' . $line); } $this->assertSame(0, $checked); } public function testGenericApiDoesNotExposeFinancialMutations(): void { $text = file_get_contents($this->routesFile) ?: ''; $forbidden = [ "post('payments'", "put('payments/", "post('payment-transactions'", "get('payment-transactions'", "post('payment-notifications/send'", "post('expenses'", "put('expenses/", "post('reimbursements'", "put('reimbursements/", "post('refunds'", "put('refunds/", "post('discounts/apply'", "post('extra-charges'", "put('extra-charges/", "post('purchase-orders'", "put('purchase-orders/", "post('inventory'", "put('inventory/", "delete('inventory/", ]; $apiV1Start = strpos($text, "\$routes->group('api/v1'"); $this->assertIsInt($apiV1Start); $apiV1Text = substr($text, $apiV1Start); foreach ($forbidden as $needle) { $this->assertStringNotContainsString($needle, $apiV1Text, $needle); } } }