400 lines
12 KiB
PHP
400 lines
12 KiB
PHP
<?php
|
|
|
|
namespace PhpOption {
|
|
/**
|
|
* Lightweight stub of PhpOption\Option used only for testing env() handling.
|
|
* This is intentionally minimal and should not be relied on in application code.
|
|
*/
|
|
class Option
|
|
{
|
|
private $value;
|
|
|
|
public function __construct($value)
|
|
{
|
|
$this->value = $value;
|
|
}
|
|
|
|
public static function fromValue($value): self
|
|
{
|
|
return new self($value);
|
|
}
|
|
|
|
public function map($callback): self
|
|
{
|
|
if ($this->value !== null) {
|
|
$this->value = $callback($this->value);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getOrCall($callback)
|
|
{
|
|
return $this->value !== null ? $this->value : $callback();
|
|
}
|
|
|
|
public function getOrThrow($exception)
|
|
{
|
|
if ($this->value === null) {
|
|
throw $exception;
|
|
}
|
|
|
|
return $this->value;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace App\Filters {
|
|
use CodeIgniter\Filters\FilterInterface;
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
// Test stubs for application filters to bypass logging, ACL, and security checks during tests.
|
|
|
|
class AclFilter implements FilterInterface
|
|
{
|
|
public function before(RequestInterface $request, $arguments = null) { return null; }
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
|
}
|
|
|
|
class HttpRequestLog implements FilterInterface
|
|
{
|
|
public function before(RequestInterface $request, $arguments = null) { return null; }
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
|
}
|
|
|
|
class SecurityInputFilter implements FilterInterface
|
|
{
|
|
public function before(RequestInterface $request, $arguments = null) { return null; }
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
|
}
|
|
|
|
class GlobalPostFileUploadGuard implements FilterInterface
|
|
{
|
|
public function before(RequestInterface $request, $arguments = null) { return null; }
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
|
}
|
|
|
|
class Cors implements FilterInterface
|
|
{
|
|
public function before(RequestInterface $request, $arguments = null) { return null; }
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
|
}
|
|
}
|
|
|
|
namespace Dotenv\Repository {
|
|
interface RepositoryInterface
|
|
{
|
|
public function get($name);
|
|
|
|
public function set($name, $value);
|
|
}
|
|
|
|
class RepositoryBuilder
|
|
{
|
|
public static function createWithDefaultAdapters(): self
|
|
{
|
|
return new self();
|
|
}
|
|
|
|
public function addAdapter($adapter): self
|
|
{
|
|
// No-op for testing.
|
|
return $this;
|
|
}
|
|
|
|
public function immutable(): self
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function make(): RepositoryInterface
|
|
{
|
|
return new class() implements RepositoryInterface {
|
|
public function get($name)
|
|
{
|
|
$value = getenv($name);
|
|
|
|
return $value === false ? null : $value;
|
|
}
|
|
|
|
public function set($name, $value)
|
|
{
|
|
putenv("$name=$value");
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace Dotenv\Repository\Adapter {
|
|
class PutenvAdapter
|
|
{
|
|
// Stub class used only to satisfy Illuminate\Support\Env references.
|
|
}
|
|
}
|
|
|
|
namespace Tests\unit {
|
|
|
|
use CodeIgniter\Test\CIUnitTestCase;
|
|
use CodeIgniter\Test\FeatureTestTrait;
|
|
|
|
class ExpenseControllerTest extends CIUnitTestCase
|
|
{
|
|
use FeatureTestTrait;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Override routes for testing to avoid filters/middleware.
|
|
$this->withRoutes([
|
|
['get', 'expense', 'ExpenseController::index'],
|
|
['post', 'expense/save', 'ExpenseController::save'],
|
|
['get', 'expense/get/(:num)', 'ExpenseController::getExpense/$1'],
|
|
['post', 'expense/delete/(:num)','ExpenseController::delete/$1'],
|
|
['get', 'expense/client-policies', 'ExpenseController::clientPolicies'],
|
|
]);
|
|
|
|
// Clean up any test data from previous runs
|
|
$db = db_connect('default');
|
|
$db->table('expenses')->like('description', 'CI4_TEST_', 'after')->delete();
|
|
}
|
|
|
|
/**
|
|
* Helper to insert an expense row directly into the database.
|
|
*/
|
|
private function createExpenseRecord(array $overrides = []): int
|
|
{
|
|
$db = db_connect('default');
|
|
|
|
$data = array_merge([
|
|
'client_id' => 1,
|
|
'client_policy_id' => 1,
|
|
'description' => 'CI4_TEST_' . uniqid('', true),
|
|
'approved_by' => 1,
|
|
'amount' => 100.00,
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'is_active' => 1,
|
|
], $overrides);
|
|
|
|
$db->table('expenses')->insert($data);
|
|
|
|
return (int) $db->insertID();
|
|
}
|
|
|
|
public function testCreateExpenseSuccess(): void
|
|
{
|
|
$payload = [
|
|
'client_id' => '1',
|
|
'client_policy_id' => '1',
|
|
'approved_by' => '1',
|
|
'description' => 'CI4_TEST_Valid Description 123-ABC',
|
|
'expense_date' => '12-01-2026',
|
|
'amount' => '250.75',
|
|
];
|
|
|
|
$result = $this->post('expense/save', $payload);
|
|
|
|
$result->assertStatus(200);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertIsArray($json);
|
|
$this->assertTrue($json['status'] ?? false);
|
|
$this->assertNotEmpty($json['id'] ?? null);
|
|
|
|
$insertedId = (int) $json['id'];
|
|
|
|
$db = db_connect('default');
|
|
$row = $db->table('expenses')
|
|
->where('description', $payload['description'])
|
|
->orderBy('id', 'DESC')
|
|
->get()
|
|
->getRowArray();
|
|
|
|
$this->assertNotEmpty($row);
|
|
$this->assertSame(1, (int) $row['is_active']);
|
|
$this->assertSame($payload['description'], $row['description']);
|
|
$this->assertEquals(250.75, (float) $row['amount']);
|
|
}
|
|
|
|
public function testCreateExpenseValidationFailureMissingFields(): void
|
|
{
|
|
$payload = [
|
|
// all required fields missing / empty
|
|
];
|
|
|
|
$result = $this->post('expense/save', $payload);
|
|
|
|
$result->assertStatus(400);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertFalse($json['status'] ?? true);
|
|
$this->assertSame('Input validation failed', $json['message'] ?? '');
|
|
$this->assertArrayHasKey('client_id', $json['errors'] ?? []);
|
|
$this->assertArrayHasKey('client_policy_id', $json['errors'] ?? []);
|
|
$this->assertArrayHasKey('approved_by', $json['errors'] ?? []);
|
|
$this->assertArrayHasKey('description', $json['errors'] ?? []);
|
|
$this->assertArrayHasKey('amount', $json['errors'] ?? []);
|
|
$this->assertArrayHasKey('expense_date', $json['errors'] ?? []);
|
|
}
|
|
|
|
public function testCreateExpenseValidationFailureInvalidDescription(): void
|
|
{
|
|
$payload = [
|
|
'client_id' => '1',
|
|
'client_policy_id' => '1',
|
|
'approved_by' => '1',
|
|
'description' => 'Invalid @ Description <>', // invalid characters
|
|
'expense_date' => '12-01-2026',
|
|
'amount' => '100.00',
|
|
];
|
|
|
|
$result = $this->post('expense/save', $payload);
|
|
|
|
$result->assertStatus(400);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertFalse($json['status'] ?? true);
|
|
$this->assertArrayHasKey('description', $json['errors'] ?? []);
|
|
$this->assertStringContainsString(
|
|
'invalid characters',
|
|
strtolower($json['errors']['description'] ?? '')
|
|
);
|
|
}
|
|
|
|
public function testUpdateExpenseSuccess(): void
|
|
{
|
|
$id = $this->createExpenseRecord([
|
|
'description' => 'CI4_TEST_Original Description',
|
|
'amount' => 50.00,
|
|
]);
|
|
|
|
$payload = [
|
|
'id' => (string) $id,
|
|
'client_id' => '1',
|
|
'client_policy_id'=> '1',
|
|
'approved_by' => '1',
|
|
'description' => 'Updated Description 456',
|
|
'expense_date' => '13-01-2026',
|
|
'amount' => '75.50',
|
|
];
|
|
|
|
$result = $this->post('expense/save', $payload);
|
|
|
|
$result->assertStatus(200);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertTrue($json['status'] ?? false);
|
|
$this->assertSame($id, (int) ($json['id'] ?? 0));
|
|
|
|
$db = db_connect('default');
|
|
$row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
|
|
|
|
$this->assertNotEmpty($row);
|
|
$this->assertSame('Updated Description 456', $row['description']);
|
|
$this->assertEquals(75.50, (float) $row['amount']);
|
|
}
|
|
|
|
public function testUpdateExpenseValidationFailureInvalidAmount(): void
|
|
{
|
|
$id = $this->createExpenseRecord();
|
|
|
|
$payload = [
|
|
'id' => (string) $id,
|
|
'client_id' => '1',
|
|
'client_policy_id'=> '1',
|
|
'approved_by' => '1',
|
|
'description' => 'Another Valid Description',
|
|
'expense_date' => '14-01-2026',
|
|
'amount' => '-10', // invalid negative
|
|
];
|
|
|
|
$result = $this->post('expense/save', $payload);
|
|
|
|
$result->assertStatus(400);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertFalse($json['status'] ?? true);
|
|
$this->assertArrayHasKey('amount', $json['errors'] ?? []);
|
|
$this->assertStringContainsString(
|
|
'cannot be negative',
|
|
strtolower($json['errors']['amount'] ?? '')
|
|
);
|
|
}
|
|
|
|
public function testDeleteExpenseSoftDeleteSuccess(): void
|
|
{
|
|
$id = $this->createExpenseRecord();
|
|
|
|
$result = $this->post('expense/delete/' . $id);
|
|
|
|
$result->assertStatus(200);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertTrue($json['status'] ?? false);
|
|
|
|
$db = db_connect();
|
|
$row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
|
|
|
|
$this->assertNotEmpty($row);
|
|
$this->assertSame(0, (int) $row['is_active']);
|
|
}
|
|
|
|
public function testDeleteExpenseInvalidId(): void
|
|
{
|
|
$result = $this->post('expense/delete/0');
|
|
|
|
$result->assertStatus(400);
|
|
|
|
$json = json_decode($result->getJSON(), true);
|
|
|
|
$this->assertFalse($json['status'] ?? true);
|
|
$this->assertSame('Invalid expense id', $json['message'] ?? '');
|
|
}
|
|
|
|
public function testSearchFilterByClientAndDescription(): void
|
|
{
|
|
// Create two distinct expenses
|
|
$this->createExpenseRecord([
|
|
'client_id' => 10,
|
|
'description' => 'FilterMatch Description',
|
|
]);
|
|
|
|
$this->createExpenseRecord([
|
|
'client_id' => 20,
|
|
'description' => 'Other Description',
|
|
]);
|
|
|
|
$result = $this->get('expense?client_id=10&description=FilterMatch');
|
|
|
|
$result->assertStatus(200);
|
|
|
|
$body = $result->getBody();
|
|
|
|
$this->assertStringContainsString('FilterMatch Description', $body);
|
|
$this->assertStringNotContainsString('Other Description', $body);
|
|
}
|
|
|
|
public function testSearchFilterValidationInvalidCharacters(): void
|
|
{
|
|
$result = $this->get('expense?description=<script>alert(1)</script>');
|
|
|
|
$result->assertStatus(200);
|
|
|
|
$body = $result->getBody();
|
|
|
|
$this->assertStringContainsString('Description filter contains invalid characters.', $body);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|