feat: phase 2 — master data (products, customers, drivers) via InsForge-backed Filament pages
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user