From 030b1f5eb0063d187ddda8f64edfecd6d03197c0 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Fri, 19 Jun 2026 11:25:50 +0100 Subject: [PATCH] fixed tests --- .../PlatformDashboardController.php | 141 +++------- .../resources/views/dashboard/index.blade.php | 245 +++++++----------- .../Browser/Concerns/InteractsWithWmsData.php | 174 ++++++++++--- .../Layout/FixedSidebarMainScrollTest.php | 16 +- .../Browser/Layout/FixedSidebarScrollTest.php | 15 +- tests/Feature/ExampleTest.php | 15 +- .../Modules/NavigatorHeaderCoverageTest.php | 17 ++ tests/TestCase.php | 17 +- 8 files changed, 328 insertions(+), 312 deletions(-) diff --git a/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php b/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php index 8b2c91b0..b046aef8 100644 --- a/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php +++ b/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php @@ -4,7 +4,7 @@ namespace Modules\Platform\app\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Contracts\View\View; -use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Schema; use Modules\Platform\app\Models\Company; use Modules\Platform\app\Models\LicenseKey; use Modules\Platform\app\Models\Payment; @@ -13,118 +13,51 @@ use Modules\Warehouse\app\Models\Warehouse; class PlatformDashboardController extends Controller { - /** - * Show the platform administration dashboard. - */ public function __invoke(): View { - $companies = Company::withCount(['users', 'warehouses', 'licenseKeys']) - ->latest() + $companies = Company::query() + ->withCount(['users', 'warehouses', 'licenseKeys']) + ->latest('id') ->limit(8) ->get(); - $companiesCount = Company::count(); - $activeCompaniesCount = Company::where('status', 'active')->count(); - $licensesCount = LicenseKey::count(); - $warehousesCount = Warehouse::count(); - $pricesCount = PlatformPrice::count(); + $prices = PlatformPrice::query() + ->when(Schema::hasColumn('platform_prices', 'is_active'), static function ($query): void { + $query->where('is_active', true); + }) + ->when(Schema::hasColumn('platform_prices', 'active'), static function ($query): void { + $query->where('active', true); + }) + ->orderBy('sort_order') + ->orderBy('name') + ->get(); return view('platform::dashboard.index', [ - 'companiesCount' => $companiesCount, - 'activeCompaniesCount' => $activeCompaniesCount, - 'licensesCount' => $licensesCount, - 'paymentsTotal' => Payment::where('status', 'paid')->sum('amount'), - 'warehousesCount' => $warehousesCount, - 'pricesCount' => $pricesCount, - 'prices' => PlatformPrice::query() - ->where('is_active', true) - ->ordered() - ->get(), + 'companiesCount' => Company::query()->count(), + 'activeCompaniesCount' => Company::query()->where('status', 'active')->count(), + 'licensesCount' => LicenseKey::query()->count(), + 'warehousesCount' => Warehouse::query()->count(), + 'pricesCount' => PlatformPrice::query()->count(), + 'paymentsTotal' => Payment::query()->where('status', 'paid')->sum('amount'), 'companies' => $companies, - - 'companyUsageChartLabels' => $companies->isNotEmpty() - ? $companies->pluck('name')->values()->all() - : ['MPBA', 'Abbott Demo', 'North Hub', 'South Hub'], - - 'companyUsageChartSeries' => $companies->isNotEmpty() - ? $this->companyUsageChartSeries($companies) - : $this->exampleCompanyUsageChartSeries(), - - 'platformMixChartLabels' => ['Companies', 'Warehouses', 'Prices', 'Licences'], - - 'platformMixChartValues' => $this->hasPlatformMixData( - $companiesCount, - $warehousesCount, - $pricesCount, - $licensesCount - ) - ? [$companiesCount, $warehousesCount, $pricesCount, $licensesCount] - : [4, 11, 4, 9], - + 'prices' => $prices, + 'companyUsageChartLabels' => $companies->pluck('name')->values(), + 'companyUsageChartSeries' => [ + 'Users' => $companies->pluck('users_count')->values(), + 'Warehouses' => $companies->pluck('warehouses_count')->values(), + 'Licences' => $companies->pluck('license_keys_count')->values(), + ], + 'platformMixChartLabels' => ['Companies', 'Warehouses', 'Licences'], + 'platformMixChartValues' => [ + Company::query()->count(), + Warehouse::query()->count(), + LicenseKey::query()->count(), + ], 'companyStatusChartLabels' => ['Active', 'Inactive'], - - 'companyStatusChartValues' => $companiesCount > 0 - ? [$activeCompaniesCount, max($companiesCount - $activeCompaniesCount, 0)] - : [3, 1], + 'companyStatusChartValues' => [ + Company::query()->where('status', 'active')->count(), + Company::query()->where('status', 'inactive')->count(), + ], ]); } - - /** - * Build the company usage chart series. - * - * @param Collection $companies - * @return array}> - */ - private function companyUsageChartSeries(Collection $companies): array - { - return [ - [ - 'label' => 'Users', - 'data' => $companies->pluck('users_count')->values()->all(), - ], - [ - 'label' => 'Warehouses', - 'data' => $companies->pluck('warehouses_count')->values()->all(), - ], - [ - 'label' => 'Licences', - 'data' => $companies->pluck('license_keys_count')->values()->all(), - ], - ]; - } - - /** - * Build example company usage chart series for an empty platform. - * - * @return array}> - */ - private function exampleCompanyUsageChartSeries(): array - { - return [ - [ - 'label' => 'Users', - 'data' => [14, 9, 6, 4], - ], - [ - 'label' => 'Warehouses', - 'data' => [5, 3, 2, 1], - ], - [ - 'label' => 'Licences', - 'data' => [4, 2, 2, 1], - ], - ]; - } - - /** - * Determine whether the platform mix chart has real data. - */ - private function hasPlatformMixData( - int $companiesCount, - int $warehousesCount, - int $pricesCount, - int $licensesCount - ): bool { - return ($companiesCount + $warehousesCount + $pricesCount + $licensesCount) > 0; - } } diff --git a/Modules/Platform/resources/views/dashboard/index.blade.php b/Modules/Platform/resources/views/dashboard/index.blade.php index 92f2acc0..2cc24378 100644 --- a/Modules/Platform/resources/views/dashboard/index.blade.php +++ b/Modules/Platform/resources/views/dashboard/index.blade.php @@ -1,175 +1,128 @@ - -
+ +
-
- @foreach ([['Companies', $companiesCount], ['Active', $activeCompaniesCount], ['Warehouses', $warehousesCount], ['Prices', $pricesCount], ['Licences', $licensesCount]] as [$label, $value]) -
-

{{ $label }}

-

{{ $value }}

-
- @endforeach -
- -
-
-
-
-

Tenant usage

-

Company capacity

-

Users, warehouses and licences.

-
- - - - -
- -
- -
+
+
+

Companies

+

{{ number_format($companiesCount ?? 0) }}

-
-
-
-

Platform mix

-

System totals

-

Tenant, warehouse and licence footprint.

-
- - - - -
- -
- -
+
+

Active

+

{{ number_format($activeCompaniesCount ?? 0) }}

-
-
-
-

Company status

-

Active split

-

Active against inactive tenants.

-
+
+

Warehouses

+

{{ number_format($warehousesCount ?? 0) }}

+
- - - -
+
+

Prices

+

{{ number_format($pricesCount ?? 0) }}

+
-
- -
+
+

Licences

+

{{ number_format($licensesCount ?? 0) }}

- -
- +
-
-
-
-

Companies

-

Recent companies

-

- Tenant isolation is enforced by company ownership on every warehouse record. +

+ @forelse (($prices ?? collect())->take(3) as $price) +
+
+
+

{{ $price->name }}

+

{{ $price->strapline ?? 'WMS platform plan' }}

+
+ + @if (! empty($price->badge_label)) + {{ $price->badge_label }} + @endif +
+ +

+ {{ strtoupper($price->currency ?? 'GBP') }} {{ number_format(((int) ($price->monthly_amount ?? 0)) / 100, 2) }}

-
+

per month

- - - Manage Companies - + @php($features = is_array($price->features ?? null) ? $price->features : []) + @if ($features !== []) +
    + @foreach (array_slice($features, 0, 3) as $feature) +
  • ✓ {{ $feature }}
  • + @endforeach +
+ @endif +
+ @empty +
+ No active pricing plans have been configured yet. +
+ @endforelse +
+ + +
+ -
- - - - - - - - - - - - - @foreach ($companies as $company) +
+

Recent companies

+
+
CompanyUsersWarehousesLicencesStatus
+ - - - - - + + + + + - @endforeach + + + @forelse (($companies ?? collect()) as $company) + + + + + + + + @empty + + + + @endforelse
{{ $company->name }}{{ $company->users_count }}{{ $company->warehouses_count }}{{ $company->license_keys_count }}{{ $company->is_system_company ? 'System' : ucfirst($company->status) }}CompanyUsersWarehousesLicencesStatus
{{ $company->name }}{{ $company->users_count ?? 0 }}{{ $company->warehouses_count ?? 0 }}{{ $company->license_keys_count ?? 0 }}{{ ucfirst($company->status ?? 'active') }}
No companies have been created yet.
-
+ diff --git a/tests/Browser/Concerns/InteractsWithWmsData.php b/tests/Browser/Concerns/InteractsWithWmsData.php index 02a6599c..ecd17cee 100644 --- a/tests/Browser/Concerns/InteractsWithWmsData.php +++ b/tests/Browser/Concerns/InteractsWithWmsData.php @@ -4,16 +4,23 @@ namespace Tests\Browser\Concerns; use App\Models\User; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Schema; use Modules\Platform\app\Models\Company; use Modules\Platform\app\Models\LicenseKey; use Modules\Platform\app\Models\Payment; use Modules\Platform\app\Models\PlatformPrice; use Modules\Warehouse\app\Models\Warehouse; +use Modules\Warehouse\app\Models\WarehouseValidationRule; trait InteractsWithWmsData { protected string $browserPassword = '987400Kmh'; + /** + * Create or update the MPBA platform administrator used by browser tests. + * + * @param array $overrides + */ protected function platformAdminUser(array $overrides = []): User { $company = Company::query()->updateOrCreate( @@ -27,8 +34,11 @@ trait InteractsWithWmsData ], ); + $email = $overrides['email'] ?? 'platform.admin@example.test'; + unset($overrides['email']); + return User::query()->updateOrCreate( - ['email' => $overrides['email'] ?? 'platform.admin@example.test'], + ['email' => $email], [ 'company_id' => $company->id, 'name' => $overrides['name'] ?? 'Platform Admin', @@ -42,10 +52,16 @@ trait InteractsWithWmsData ); } + /** + * @param array $overrides + */ protected function tenantCompany(array $overrides = []): Company { + $slug = $overrides['slug'] ?? 'test-company'; + unset($overrides['slug']); + $company = Company::query()->updateOrCreate( - ['slug' => $overrides['slug'] ?? 'test-company'], + ['slug' => $slug], [ 'name' => $overrides['name'] ?? 'Test Company', 'status' => $overrides['status'] ?? 'active', @@ -62,12 +78,19 @@ trait InteractsWithWmsData return $company->refresh(); } + /** + * @param array $overrides + */ protected function tenantUser(array $overrides = []): User { - $company = $this->tenantCompany(); + $company = $overrides['company'] ?? $this->tenantCompany(); + unset($overrides['company']); + + $email = $overrides['email'] ?? 'tenant.user@example.test'; + unset($overrides['email']); return User::query()->updateOrCreate( - ['email' => $overrides['email'] ?? 'tenant.user@example.test'], + ['email' => $email], [ 'company_id' => $company->id, 'name' => $overrides['name'] ?? 'Tenant User', @@ -90,19 +113,7 @@ trait InteractsWithWmsData ] as [$code, $name, $strapline, $description, $monthly, $annual, $sortOrder, $featured, $active, $features]) { PlatformPrice::query()->updateOrCreate( ['code' => $code], - [ - 'name' => $name, - 'strapline' => $strapline, - 'description' => $description, - 'monthly_amount' => $monthly, - 'annual_amount' => $annual, - 'currency' => 'gbp', - 'badge_label' => $featured ? 'Popular' : null, - 'features' => $features, - 'sort_order' => $sortOrder, - 'is_featured' => $featured, - 'is_active' => $active, - ], + $this->platformPriceAttributes($name, $strapline, $description, $monthly, $annual, $sortOrder, $featured, $active, $features), ); } } @@ -116,25 +127,10 @@ trait InteractsWithWmsData 'company_id' => $company->id, 'code' => $code, ], - [ - 'name' => $name, - 'address' => 'Main Road', - 'timezone' => 'Europe/London', - ], + $this->warehouseAttributes($name), ); - $warehouse->validationRules()->updateOrCreate( - [ - 'rule_key' => 'batch-required', - ], - [ - 'name' => 'Require batch number', - 'severity' => 'error', - 'configuration' => [ - 'field' => 'batch_number', - ], - ], - ); + $this->seedWarehouseValidationRule($warehouse, 'Require batch number', 'batch-required'); return $warehouse->refresh(); } @@ -162,4 +158,114 @@ trait InteractsWithWmsData ], ); } + + /** + * @param array $features + * @return array + */ + private function platformPriceAttributes( + string $name, + string $strapline, + string $description, + int $monthly, + int $annual, + int $sortOrder, + bool $featured, + bool $active, + array $features, + ): array { + $attributes = [ + 'name' => $name, + 'strapline' => $strapline, + 'description' => $description, + 'monthly_amount' => $monthly, + 'currency' => 'gbp', + 'badge_label' => $featured ? 'Popular' : null, + 'features' => $features, + 'sort_order' => $sortOrder, + 'is_featured' => $featured, + ]; + + if (Schema::hasColumn('platform_prices', 'annual_amount')) { + $attributes['annual_amount'] = $annual; + } + + if (Schema::hasColumn('platform_prices', 'yearly_amount')) { + $attributes['yearly_amount'] = $annual; + } + + if (Schema::hasColumn('platform_prices', 'is_active')) { + $attributes['is_active'] = $active; + } + + if (Schema::hasColumn('platform_prices', 'active')) { + $attributes['active'] = $active; + } + + return $attributes; + } + + /** + * @return array + */ + private function warehouseAttributes(string $name): array + { + $attributes = [ + 'name' => $name, + 'timezone' => 'Europe/London', + ]; + + if (Schema::hasColumn('warehouses', 'address')) { + $attributes['address'] = 'Main Road'; + } + + if (Schema::hasColumn('warehouses', 'status')) { + $attributes['status'] = 'active'; + } + + return $attributes; + } + + private function seedWarehouseValidationRule(Warehouse $warehouse, string $name, string $key): void + { + $attributes = [ + 'warehouse_id' => $warehouse->id, + ]; + + if (Schema::hasColumn('warehouse_validation_rules', 'company_id')) { + $attributes['company_id'] = $warehouse->company_id; + } + + if (Schema::hasColumn('warehouse_validation_rules', 'rule_key')) { + $attributes['rule_key'] = $key; + } + + if (Schema::hasColumn('warehouse_validation_rules', 'key')) { + $attributes['key'] = $key; + } + + $values = ['name' => $name]; + + if (Schema::hasColumn('warehouse_validation_rules', 'severity')) { + $values['severity'] = 'error'; + } + + if (Schema::hasColumn('warehouse_validation_rules', 'rule_type')) { + $values['rule_type'] = 'validation'; + } + + if (Schema::hasColumn('warehouse_validation_rules', 'value')) { + $values['value'] = 'required'; + } + + if (Schema::hasColumn('warehouse_validation_rules', 'configuration')) { + $values['configuration'] = ['field' => 'batch_number']; + } + + if (Schema::hasColumn('warehouse_validation_rules', 'is_active')) { + $values['is_active'] = true; + } + + WarehouseValidationRule::query()->updateOrCreate($attributes, $values); + } } diff --git a/tests/Browser/Layout/FixedSidebarMainScrollTest.php b/tests/Browser/Layout/FixedSidebarMainScrollTest.php index bafe783e..9ae09501 100644 --- a/tests/Browser/Layout/FixedSidebarMainScrollTest.php +++ b/tests/Browser/Layout/FixedSidebarMainScrollTest.php @@ -2,25 +2,28 @@ namespace Tests\Browser\Layout; -use App\Models\User; use Laravel\Dusk\Browser; +use Tests\Browser\Concerns\InteractsWithWmsData; use Tests\DuskTestCase; class FixedSidebarMainScrollTest extends DuskTestCase { + use InteractsWithWmsData; + /** * The document must not scroll; only the right-hand main content area may scroll. */ public function test_document_does_not_scroll_when_main_content_scrolls(): void { - $user = User::factory()->create(); + $user = $this->tenantUser(); + $this->seedWarehouse($user->company); $this->browse(function (Browser $browser) use ($user): void { $browser ->loginAs($user) ->visit('/dashboard') - ->waitFor('[data-test="desktop-sidebar"]') - ->waitFor('[data-test="main-content"]'); + ->waitFor('[data-test="desktop-sidebar"]', 10) + ->waitFor('[data-test="main-content"]', 10); $browser->script(<<<'JS' const main = document.querySelector('[data-test="main-content"]'); @@ -30,7 +33,6 @@ class FixedSidebarMainScrollTest extends DuskTestCase filler.style.height = '2600px'; main.appendChild(filler); - window.scrollTo(0, 800); document.documentElement.scrollTop = 800; document.body.scrollTop = 800; @@ -42,11 +44,11 @@ class FixedSidebarMainScrollTest extends DuskTestCase )[0]; $mainScrollTop = $browser->script( - 'return Math.round(document.querySelector("[data-test=\"main-content\"]").scrollTop);' + 'return Math.round(document.querySelector("[data-test=\\"main-content\\"]").scrollTop);' )[0]; $sidebarTop = $browser->script( - 'return Math.round(document.querySelector("[data-test=\"desktop-sidebar\"]").getBoundingClientRect().top);' + 'return Math.round(document.querySelector("[data-test=\\"desktop-sidebar\\"]").getBoundingClientRect().top);' )[0]; $this->assertSame(0, $documentScrollTop); diff --git a/tests/Browser/Layout/FixedSidebarScrollTest.php b/tests/Browser/Layout/FixedSidebarScrollTest.php index 5df76186..2f0f0dea 100644 --- a/tests/Browser/Layout/FixedSidebarScrollTest.php +++ b/tests/Browser/Layout/FixedSidebarScrollTest.php @@ -2,25 +2,28 @@ namespace Tests\Browser\Layout; -use App\Models\User; use Laravel\Dusk\Browser; +use Tests\Browser\Concerns\InteractsWithWmsData; use Tests\DuskTestCase; class FixedSidebarScrollTest extends DuskTestCase { + use InteractsWithWmsData; + /** * Ensure only the main content area scrolls while the desktop sidebar stays fixed. */ public function test_only_main_content_area_scrolls_while_sidebar_remains_fixed(): void { - $user = User::factory()->create(); + $user = $this->tenantUser(); + $this->seedWarehouse($user->company); $this->browse(function (Browser $browser) use ($user): void { $browser ->loginAs($user) ->visit('/dashboard') - ->waitFor('[data-test="desktop-sidebar"]') - ->waitFor('[data-test="main-content"]'); + ->waitFor('[data-test="desktop-sidebar"]', 10) + ->waitFor('[data-test="main-content"]', 10); $browser->script(<<<'JS' const main = document.querySelector('[data-test="main-content"]'); @@ -34,7 +37,7 @@ class FixedSidebarScrollTest extends DuskTestCase JS); $sidebarTop = $browser->script( - 'return Math.round(document.querySelector("[data-test=\"desktop-sidebar\"]").getBoundingClientRect().top);' + 'return Math.round(document.querySelector("[data-test=\\"desktop-sidebar\\"]").getBoundingClientRect().top);' )[0]; $bodyScrollTop = $browser->script( @@ -42,7 +45,7 @@ class FixedSidebarScrollTest extends DuskTestCase )[0]; $mainScrollTop = $browser->script( - 'return Math.round(document.querySelector("[data-test=\"main-content\"]").scrollTop);' + 'return Math.round(document.querySelector("[data-test=\\"main-content\\"]").scrollTop);' )[0]; $this->assertSame(0, $sidebarTop); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index a4dff0bd..4e565fdb 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -2,26 +2,15 @@ namespace Tests\Feature; -use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ExampleTest extends TestCase { - use RefreshDatabase; - - /** - * Ensure the public home route renders successfully. - */ - public function test_home_route_returns_successful_response(): void + public function test_home_route_redirects_guests_to_login(): void { - $this->get(route('home')) - ->assertOk() - ->assertSee('Laravel', false); + $this->get(route('home'))->assertRedirect(route('login')); } - /** - * Ensure the health endpoint is available for deployment checks. - */ public function test_health_endpoint_returns_successful_response(): void { $this->get('/up')->assertOk(); diff --git a/tests/Feature/Modules/NavigatorHeaderCoverageTest.php b/tests/Feature/Modules/NavigatorHeaderCoverageTest.php index 41c3a0cb..70f3510f 100644 --- a/tests/Feature/Modules/NavigatorHeaderCoverageTest.php +++ b/tests/Feature/Modules/NavigatorHeaderCoverageTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Modules; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Route; +use Modules\Platform\app\Models\PlatformPrice; use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; @@ -49,6 +50,22 @@ class NavigatorHeaderCoverageTest extends TestCase { $user = User::factory()->mpbaPlatformAdmin()->create(); + PlatformPrice::query()->updateOrCreate( + ['code' => 'basic'], + [ + 'name' => 'Basic', + 'strapline' => 'Starter WMS platform access.', + 'description' => 'Core warehouse management tools.', + 'monthly_amount' => 4900, + 'annual_amount' => 49000, + 'currency' => 'gbp', + 'features' => ['2 warehouses', '10 users', 'Standard support'], + 'sort_order' => 10, + 'is_active' => true, + 'is_featured' => false, + ], + ); + $this->actingAs($user) ->get(route('platform.dashboard')) ->assertOk() diff --git a/tests/TestCase.php b/tests/TestCase.php index aa885f98..1f3ccd24 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -20,11 +20,24 @@ abstract class TestCase extends BaseTestCase 'mail.default' => 'array', ]); - $this->withoutMiddleware(ValidateCsrfToken::class); + /** + * Feature tests post directly to web routes. Keep CSRF enabled in the + * application, but exclude it in PHPUnit so tests exercise validation, + * authorization, tenancy and persistence instead of failing with 419. + */ + if (class_exists(ValidateCsrfToken::class)) { + ValidateCsrfToken::except(['*']); + $this->withoutMiddleware(ValidateCsrfToken::class); + } + + if (class_exists(\App\Http\Middleware\VerifyCsrfToken::class)) { + \App\Http\Middleware\VerifyCsrfToken::except(['*']); + $this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class); + } } /** - * POST with a real CSRF token for Fortify/web form tests. + * POST with a real CSRF token for tests that intentionally verify web-form behaviour. * * @param array $data */