feat: phase 2 — master data (products, customers, drivers) via InsForge-backed Filament pages

This commit is contained in:
sean
2026-08-25 18:33:56 +07:00
parent aad2b5e256
commit 4298544381
12 changed files with 870 additions and 38 deletions
+5 -3
View File
@@ -59,14 +59,16 @@ class AuthenticationTest extends TestCase
public function test_mirror_profile_is_created_when_missing(): void
{
InsForgeFake::fakeLoginSuccess($this->adminProfile());
unset(InsForgeFake::$profiles['admin@beraspro.test']);
InsForgeFake::reset();
Livewire::test(Login::class)
->fillForm(['email' => 'admin@beraspro.test', 'password' => 'secret'])
->call('authenticate');
$this->assertArrayHasKey('admin@beraspro.test', InsForgeFake::$profiles);
$this->assertSame('operator', InsForgeFake::$profiles['admin@beraspro.test']['role']);
$profiles = array_filter(InsForgeFake::$store['users'] ?? [], fn (array $row): bool => $row['email'] === 'admin@beraspro.test');
$this->assertCount(1, $profiles);
$this->assertSame('operator', reset($profiles)['role']);
}
public function test_invalid_credentials_do_not_authenticate(): void
+176
View File
@@ -0,0 +1,176 @@
<?php
namespace Tests\Feature\MasterData;
use App\Filament\Pages\Masters\Customers;
use App\Filament\Pages\Masters\Drivers;
use App\Filament\Pages\Masters\Products;
use App\Models\User;
use App\Services\CustomerService;
use App\Services\DriverService;
use App\Services\ProductService;
use Filament\Auth\Pages\Login;
use Livewire\Livewire;
use Tests\Support\InsForgeFake;
use Tests\TestCase;
class MasterDataTest extends TestCase
{
private function loginAsAdmin(): void
{
InsForgeFake::fakeLoginSuccess([
'id' => '11111111-1111-1111-1111-111111111111',
'name' => 'Admin BerasPro',
'email' => 'admin@beraspro.test',
'password' => '$2y$04$hash',
'role' => 'admin',
'is_active' => true,
]);
Livewire::test(Login::class)
->fillForm(['email' => 'admin@beraspro.test', 'password' => 'secret'])
->call('authenticate')
->assertHasNoErrors();
$this->assertAuthenticated();
}
private function actingAsRole(string $role): User
{
$user = new User;
$user->forceFill([
'id' => "33333333-3333-3333-3333-{$role}0000000",
'name' => ucfirst($role),
'email' => "{$role}@beraspro.test",
'role' => $role,
'is_active' => true,
]);
InsForgeFake::reset();
InsForgeFake::fakeRest();
$this->actingAs($user);
return $user;
}
public function test_products_page_lists_records_from_insforge(): void
{
$this->loginAsAdmin();
$this->assertTrue(auth()->check(), 'admin should be authenticated after login');
$this->assertTrue(auth()->user()->isAdmin(), 'authenticated user should have admin role');
InsForgeFake::seed('products', [
'id' => '00000000-0000-0000-0000-000000000101',
'code' => 'BERAS-001',
'name' => 'Beras Premium 5kg',
'unit' => 'sak',
'weight' => 5,
'stock_minimum' => 100,
'cost_price' => 13000,
'is_active' => true,
]);
InsForgeFake::fakeRest();
Livewire::test(Products::class)
->assertSuccessful()
->assertSee('Beras Premium 5kg');
}
public function test_products_page_is_denied_for_operator(): void
{
$this->actingAsRole('operator');
$this->get('/admin/'.(new Products)->getSlug())->assertForbidden();
}
public function test_customers_page_is_accessible_for_operator(): void
{
$this->actingAsRole('operator');
Livewire::test(Customers::class)->assertSuccessful();
}
public function test_drivers_page_is_denied_for_operator(): void
{
$this->actingAsRole('operator');
$this->get('/admin/'.(new Drivers)->getSlug())->assertForbidden();
}
public function test_drivers_page_is_accessible_for_admin(): void
{
$this->loginAsAdmin();
InsForgeFake::fakeRest();
Livewire::test(Drivers::class)->assertSuccessful();
}
public function test_product_service_creates_record_via_post(): void
{
InsForgeFake::reset();
InsForgeFake::fakeRest();
app(ProductService::class)->create([
'code' => 'BERAS-002',
'name' => 'Beras Merah',
'unit' => 'kg',
'weight' => 25,
'stock_minimum' => 50,
'cost_price' => 12000,
'is_active' => true,
]);
$stored = array_values(InsForgeFake::$store['products'])[0];
$this->assertSame('BERAS-002', $stored['code']);
$this->assertSame(12000, $stored['cost_price']);
}
public function test_customer_service_updates_and_deletes(): void
{
InsForgeFake::reset();
InsForgeFake::fakeRest();
InsForgeFake::seed('customers', [
'id' => 'aaaa1111-0000-0000-0000-000000000001',
'customer_code' => 'CUST-0001',
'name' => 'Andi',
'phone' => '0812',
]);
$service = app(CustomerService::class);
$found = $service->findBy(['customer_code' => 'CUST-0001']);
$this->assertNotNull($found);
$service->update($found['id'], ['name' => 'Andi Updated']);
$this->assertSame('Andi Updated', $service->find($found['id'])['name']);
$service->delete($found['id']);
$this->assertNull($service->find($found['id']));
}
public function test_driver_service_paginates_with_total(): void
{
InsForgeFake::reset();
InsForgeFake::fakeRest();
for ($i = 1; $i <= 3; $i++) {
InsForgeFake::seed('drivers', [
'id' => sprintf('bbbb2222-0000-0000-0000-00000000000%d', $i),
'driver_code' => sprintf('DRV-%04d', $i),
'name' => "Driver {$i}",
'is_active' => true,
]);
}
$paginator = app(DriverService::class)->paginate(page: 1, perPage: 2);
$this->assertCount(2, $paginator->items());
$this->assertSame(3, $paginator->total());
}
}
+67 -29
View File
@@ -7,13 +7,19 @@ use Illuminate\Support\Facades\Http;
class InsForgeFake
{
public static array $profiles = [];
/** @var array<string, array<string, array<string, mixed>>> table => id => row */
public static array $store = [];
public static string $accessToken = 'fake.header.';
public static string $accessToken = '';
public static function reset(): void
{
self::$profiles = [];
self::$store = [];
}
public static function seed(string $table, array $row): void
{
self::$store[$table][$row['id']] = $row;
}
public static function fakeLoginSuccess(array $profile): void
@@ -25,7 +31,7 @@ class InsForgeFake
'exp' => now()->addHour()->getTimestamp(),
])).'.sig';
self::$profiles[$profile['email']] = $profile;
self::seed('users', $profile);
Http::fake([
'*/api/auth/sessions' => Http::response([
@@ -39,12 +45,8 @@ class InsForgeFake
], 200, [
'Set-Cookie' => 'insforge_refresh_token=fake-refresh-cookie; Path=/; HttpOnly',
]),
'*/api/auth/refresh' => Http::response([
'accessToken' => self::$accessToken,
], 200),
'*/api/database/records/users*' => function (Request $request) {
return self::handleUsersRequest($request);
},
'*/api/auth/refresh' => Http::response(['accessToken' => self::$accessToken], 200),
'*/api/database/records/*' => fn (Request $request) => self::handleRecordsRequest($request),
]);
}
@@ -61,25 +63,54 @@ class InsForgeFake
]);
}
public static function fakeProfile(array $profile): void
public static function fakeRest(): void
{
self::$profiles[$profile['email']] = $profile;
Http::fake([
'*/api/auth/refresh' => Http::response(['accessToken' => self::$accessToken], 200),
'*/api/database/records/*' => fn (Request $request) => self::handleRecordsRequest($request),
]);
}
private static function handleUsersRequest(Request $request)
public static function handleRecordsRequest(Request $request)
{
$path = parse_url($request->url(), PHP_URL_PATH) ?? '';
$table = str_replace('/api/database/records/', '', $path);
parse_str((string) parse_url($request->url(), PHP_URL_QUERY) ?? '', $query);
$method = strtoupper($request->method());
$rows = array_values(self::$store[$table] ?? []);
if ($method === 'GET') {
foreach (self::$profiles as $profile) {
if (($query['email'] ?? null) === 'eq.'.$profile['email']
|| ($query['id'] ?? null) === 'eq.'.$profile['id']) {
return Http::response([$profile], 200);
foreach ($query as $key => $value) {
if (in_array($key, ['select', 'limit', 'offset', 'order', 'or'], true)) {
continue;
}
if (str_starts_with((string) $value, 'eq.')) {
$column = $key;
$expected = substr((string) $value, 3);
$rows = array_values(array_filter($rows, fn (array $row): bool => (string) ($row[$column] ?? '') === $expected));
}
}
return Http::response([], 200);
$total = count($rows);
$rows = array_slice(
$rows,
(int) ($query['offset'] ?? 0),
(int) ($query['limit'] ?? 25),
);
return Http::response(
array_map(
fn (array $row): array => isset($query['select']) && $query['select'] !== '*'
? array_intersect_key($row, array_flip(explode(',', (string) $query['select']))) ?: $row
: $row,
$rows,
),
200,
['Content-Range' => sprintf('0-%d/%d', max(count($rows) - 1, 0), $total)],
);
}
if ($method === 'POST') {
@@ -89,25 +120,32 @@ class InsForgeFake
return Http::response([], 201);
}
self::$profiles[$payload['email']] = array_merge([
'role' => 'operator',
'is_active' => true,
], $payload);
$payload = array_merge($payload, ['id' => $payload['id'] ?? fake()->uuid()]);
self::$store[$table][$payload['id']] = $payload;
return Http::response([], 201);
}
if ($method === 'PATCH') {
$targetId = str_replace('eq.', '', $query['id'] ?? '');
$targetId = str_replace('eq.', '', $query['id'] ?? '');
foreach (self::$profiles as &$profile) {
if ($profile['id'] === $targetId) {
$profile = array_merge($profile, $request->data());
if ($method === 'PATCH') {
$payload = $request->data();
foreach (self::$store[$table] as &$row) {
if (($row['id'] ?? null) === $targetId) {
$row = array_merge($row, $payload);
}
}
unset($profile);
unset($row);
return Http::response([], 200);
return Http::response([], 204);
}
if ($method === 'DELETE') {
unset(self::$store[$table][$targetId]);
return Http::response([], 204);
}
return Http::response([], 200);