Updated Tests
This commit is contained in:
@@ -18,17 +18,6 @@ class PlatformAdministrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Disable only CSRF for controller behaviour tests; auth/tenant/licence middleware still runs.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class);
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure guests are redirected away from platform administration.
|
||||
*/
|
||||
@@ -80,11 +69,11 @@ class PlatformAdministrationTest extends TestCase
|
||||
{
|
||||
$admin = User::factory()->mpbaPlatformAdmin()->create();
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.companies.store'), [
|
||||
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.companies.store'), $this->csrfPayload([
|
||||
'name' => 'Abbott Logistics Limited',
|
||||
'billing_email' => 'billing@abbott.example',
|
||||
'status' => 'active',
|
||||
])->assertRedirect();
|
||||
]))->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('companies', [
|
||||
'name' => 'Abbott Logistics Limited',
|
||||
@@ -104,11 +93,12 @@ class PlatformAdministrationTest extends TestCase
|
||||
|
||||
$this->actingAs($admin)
|
||||
->from(route('platform.companies.index'))
|
||||
->post(route('platform.companies.store'), [
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->post(route('platform.companies.store'), $this->csrfPayload([
|
||||
'name' => '',
|
||||
'billing_email' => 'not-an-email',
|
||||
'status' => 'unknown',
|
||||
])
|
||||
]))
|
||||
->assertRedirect(route('platform.companies.index'))
|
||||
->assertSessionHasErrors(['name', 'billing_email', 'status']);
|
||||
}
|
||||
@@ -121,12 +111,12 @@ class PlatformAdministrationTest extends TestCase
|
||||
$admin = User::factory()->mpbaPlatformAdmin()->create();
|
||||
$company = Company::create(['name' => 'Tenant A', 'slug' => 'tenant-a']);
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('platform.licenses.store'), [
|
||||
$response = $this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.licenses.store'), $this->csrfPayload([
|
||||
'company_id' => $company->id,
|
||||
'label' => 'Production licence',
|
||||
'plan' => 'enterprise',
|
||||
'max_users' => 500,
|
||||
]);
|
||||
]));
|
||||
|
||||
$response->assertRedirect()->assertSessionHas('plain_license_key');
|
||||
|
||||
@@ -148,12 +138,13 @@ class PlatformAdministrationTest extends TestCase
|
||||
|
||||
$this->actingAs($admin)
|
||||
->from(route('platform.licenses.index'))
|
||||
->post(route('platform.licenses.store'), [
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->post(route('platform.licenses.store'), $this->csrfPayload([
|
||||
'company_id' => 999,
|
||||
'label' => '',
|
||||
'plan' => 'ultimate',
|
||||
'max_users' => 0,
|
||||
])
|
||||
]))
|
||||
->assertRedirect(route('platform.licenses.index'))
|
||||
->assertSessionHasErrors(['company_id', 'label', 'plan', 'max_users']);
|
||||
}
|
||||
@@ -166,13 +157,13 @@ class PlatformAdministrationTest extends TestCase
|
||||
$admin = User::factory()->mpbaPlatformAdmin()->create();
|
||||
$company = Company::create(['name' => 'Tenant Payment', 'slug' => 'tenant-payment']);
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.payments.store'), [
|
||||
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.payments.store'), $this->csrfPayload([
|
||||
'company_id' => $company->id,
|
||||
'amount' => 12345,
|
||||
'currency' => 'gbp',
|
||||
'status' => 'paid',
|
||||
'description' => 'Manual bank transfer',
|
||||
])->assertRedirect();
|
||||
]))->assertRedirect();
|
||||
|
||||
$payment = Payment::firstOrFail();
|
||||
|
||||
@@ -190,13 +181,13 @@ class PlatformAdministrationTest extends TestCase
|
||||
$admin = User::factory()->mpbaPlatformAdmin()->create();
|
||||
$company = Company::create(['name' => 'Tenant Pending', 'slug' => 'tenant-pending']);
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.payments.store'), [
|
||||
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.payments.store'), $this->csrfPayload([
|
||||
'company_id' => $company->id,
|
||||
'amount' => 5000,
|
||||
'currency' => 'gbp',
|
||||
'status' => 'pending',
|
||||
'description' => 'Awaiting payment',
|
||||
])->assertRedirect();
|
||||
]))->assertRedirect();
|
||||
|
||||
$this->assertNull(Payment::firstOrFail()->paid_at);
|
||||
}
|
||||
@@ -224,7 +215,7 @@ class PlatformAdministrationTest extends TestCase
|
||||
],
|
||||
];
|
||||
|
||||
$this->postJson(route('stripe.webhook'), $payload)
|
||||
$this->withSession(['_token' => 'test-token'])->withHeader('X-CSRF-TOKEN', 'test-token')->postJson(route('stripe.webhook'), $payload)
|
||||
->assertOk()
|
||||
->assertJson(['received' => true]);
|
||||
|
||||
@@ -242,7 +233,7 @@ class PlatformAdministrationTest extends TestCase
|
||||
*/
|
||||
public function test_stripe_webhook_ignores_unhandled_events(): void
|
||||
{
|
||||
$this->postJson(route('stripe.webhook'), ['type' => 'customer.created'])
|
||||
$this->withSession(['_token' => 'test-token'])->withHeader('X-CSRF-TOKEN', 'test-token')->postJson(route('stripe.webhook'), ['type' => 'customer.created'])
|
||||
->assertOk()
|
||||
->assertJson(['received' => true]);
|
||||
|
||||
@@ -271,4 +262,25 @@ class PlatformAdministrationTest extends TestCase
|
||||
...$overrides,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return payload with a valid test CSRF token.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function csrfPayload(array $payload = []): array
|
||||
{
|
||||
return ['_token' => 'test-token'] + $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the test session with the matching CSRF token.
|
||||
*/
|
||||
private function withCsrfSession(): self
|
||||
{
|
||||
$this->withSession(['_token' => 'test-token']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,6 @@ class PlatformPriceCrudTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Disable only CSRF for controller behaviour tests; auth/tenant/licence middleware still runs.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class);
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the customer-facing Platform pricing page only shows active prices in display order.
|
||||
*/
|
||||
@@ -97,7 +86,7 @@ class PlatformPriceCrudTest extends TestCase
|
||||
{
|
||||
$admin = User::factory()->mpbaPlatformAdmin()->create();
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('platform.prices.store'), [
|
||||
$response = $this->actingAs($admin)->withSession(['_token' => 'test-token'])->post(route('platform.prices.store'), $this->csrfPayload([
|
||||
'code' => '',
|
||||
'name' => 'Ultimate Plan',
|
||||
'strapline' => 'Full control',
|
||||
@@ -110,7 +99,7 @@ class PlatformPriceCrudTest extends TestCase
|
||||
'sort_order' => 40,
|
||||
'is_featured' => '1',
|
||||
'is_active' => '1',
|
||||
]);
|
||||
]));
|
||||
|
||||
$response->assertRedirect(route('platform.prices.index'));
|
||||
|
||||
@@ -139,12 +128,13 @@ class PlatformPriceCrudTest extends TestCase
|
||||
|
||||
$this->actingAs($admin)
|
||||
->from(route('platform.prices.create'))
|
||||
->post(route('platform.prices.store'), [
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->post(route('platform.prices.store'), $this->csrfPayload([
|
||||
'name' => '',
|
||||
'monthly_amount' => '-1',
|
||||
'currency' => 'GB',
|
||||
'sort_order' => -5,
|
||||
])
|
||||
]))
|
||||
->assertRedirect(route('platform.prices.create'))
|
||||
->assertSessionHasErrors(['name', 'monthly_amount', 'currency', 'sort_order']);
|
||||
}
|
||||
@@ -174,7 +164,7 @@ class PlatformPriceCrudTest extends TestCase
|
||||
PlatformPrice::query()->delete();
|
||||
$price = PlatformPrice::create($this->pricePayload(['code' => 'basic', 'name' => 'Basic']));
|
||||
|
||||
$this->actingAs($admin)->put(route('platform.prices.update', $price), [
|
||||
$this->actingAs($admin)->withSession(['_token' => 'test-token'])->put(route('platform.prices.update', $price), $this->csrfPayload([
|
||||
'code' => 'basic',
|
||||
'name' => 'Basic Updated',
|
||||
'strapline' => 'Entry level stock control',
|
||||
@@ -186,7 +176,7 @@ class PlatformPriceCrudTest extends TestCase
|
||||
'features' => "2 warehouses\n10 users",
|
||||
'sort_order' => 5,
|
||||
'is_active' => '1',
|
||||
])->assertRedirect(route('platform.prices.index'));
|
||||
]))->assertRedirect(route('platform.prices.index'));
|
||||
|
||||
$this->assertDatabaseHas('platform_prices', [
|
||||
'id' => $price->id,
|
||||
@@ -212,11 +202,12 @@ class PlatformPriceCrudTest extends TestCase
|
||||
|
||||
$this->actingAs($admin)
|
||||
->from(route('platform.prices.edit', $price))
|
||||
->put(route('platform.prices.update', $price), [
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->put(route('platform.prices.update', $price), $this->csrfPayload([
|
||||
...$this->requestPayload(),
|
||||
'code' => 'basic',
|
||||
'name' => 'Advanced',
|
||||
])
|
||||
]))
|
||||
->assertRedirect(route('platform.prices.edit', $price))
|
||||
->assertSessionHasErrors(['code']);
|
||||
}
|
||||
@@ -231,7 +222,8 @@ class PlatformPriceCrudTest extends TestCase
|
||||
$price = PlatformPrice::create($this->pricePayload(['code' => 'legacy', 'name' => 'Legacy']));
|
||||
|
||||
$this->actingAs($admin)
|
||||
->delete(route('platform.prices.destroy', $price))
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->delete(route('platform.prices.destroy', $price), ['_token' => 'test-token'])
|
||||
->assertRedirect(route('platform.prices.index'));
|
||||
|
||||
$this->assertDatabaseMissing('platform_prices', ['id' => $price->id]);
|
||||
@@ -306,4 +298,25 @@ class PlatformPriceCrudTest extends TestCase
|
||||
'is_active' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return payload with a valid test CSRF token.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function csrfPayload(array $payload = []): array
|
||||
{
|
||||
return ['_token' => 'test-token'] + $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the test session with the matching CSRF token.
|
||||
*/
|
||||
private function withCsrfSession(): self
|
||||
{
|
||||
$this->withSession(['_token' => 'test-token']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Platform;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Platform\app\Models\PlatformPrice;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Covers the legacy PricingController route by rendering the shared Platform price cards.
|
||||
*/
|
||||
class SharedPricingRouteTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Ensure the Pricing module delegates to the shared Platform pricing display.
|
||||
*/
|
||||
public function test_legacy_pricing_module_route_renders_active_platform_prices(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
PlatformPrice::query()->delete();
|
||||
|
||||
PlatformPrice::create([
|
||||
'code' => 'basic',
|
||||
'name' => 'Basic',
|
||||
'strapline' => 'Starter access',
|
||||
'description' => 'Core WMS functions.',
|
||||
'monthly_amount' => 4900,
|
||||
'annual_amount' => 49000,
|
||||
'currency' => 'gbp',
|
||||
'badge_label' => 'Starter',
|
||||
'features' => ['2 warehouses', '10 users'],
|
||||
'sort_order' => 10,
|
||||
'is_featured' => false,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('pricing.index'))
|
||||
->assertOk()
|
||||
->assertSee('Pricing', false)
|
||||
->assertSee('Basic', false);
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,6 @@ class WarehouseCrudTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Disable only CSRF for controller behaviour tests; auth/tenant/licence middleware still runs.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class);
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure licensed tenant users can see the warehouse dashboard for their company.
|
||||
*/
|
||||
@@ -92,11 +81,11 @@ class WarehouseCrudTest extends TestCase
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->post(route('warehouses.store'), [
|
||||
$this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('warehouses.store'), $this->csrfPayload([
|
||||
'code' => 'EAST',
|
||||
'name' => 'East Midlands DC',
|
||||
'address' => 'Main Road, Nottingham',
|
||||
])->assertRedirect();
|
||||
]))->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('warehouses', [
|
||||
'company_id' => $user->company_id,
|
||||
@@ -116,10 +105,11 @@ class WarehouseCrudTest extends TestCase
|
||||
|
||||
$this->actingAs($user)
|
||||
->from(route('warehouses.index'))
|
||||
->post(route('warehouses.store'), [
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->post(route('warehouses.store'), $this->csrfPayload([
|
||||
'code' => '',
|
||||
'name' => '',
|
||||
])
|
||||
]))
|
||||
->assertRedirect(route('warehouses.index'))
|
||||
->assertSessionHasErrors(['code', 'name']);
|
||||
}
|
||||
@@ -154,13 +144,13 @@ class WarehouseCrudTest extends TestCase
|
||||
$user = User::factory()->create();
|
||||
$warehouse = Warehouse::create(['company_id' => $user->company_id, 'code' => 'MAIN', 'name' => 'Main Warehouse']);
|
||||
|
||||
$this->actingAs($user)->post(route('warehouse-rules.store'), [
|
||||
$this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('warehouse-rules.store'), $this->csrfPayload([
|
||||
'warehouse_id' => $warehouse->id,
|
||||
'name' => 'Require batch number',
|
||||
'rule_key' => 'batch-required',
|
||||
'severity' => 'blocker',
|
||||
'configuration' => json_encode(['field' => 'batch_no']),
|
||||
])->assertRedirect();
|
||||
]))->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('warehouse_validation_rules', [
|
||||
'warehouse_id' => $warehouse->id,
|
||||
@@ -182,13 +172,13 @@ class WarehouseCrudTest extends TestCase
|
||||
$otherCompany = Company::create(['name' => 'Other Company', 'slug' => 'other-company']);
|
||||
$otherWarehouse = Warehouse::create(['company_id' => $otherCompany->id, 'code' => 'OTHER', 'name' => 'Other Warehouse']);
|
||||
|
||||
$this->actingAs($user)->post(route('warehouse-rules.store'), [
|
||||
$this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('warehouse-rules.store'), $this->csrfPayload([
|
||||
'warehouse_id' => $otherWarehouse->id,
|
||||
'name' => 'Invalid Rule',
|
||||
'rule_key' => 'invalid-rule',
|
||||
'severity' => 'error',
|
||||
'configuration' => '{}',
|
||||
])->assertNotFound();
|
||||
]))->assertNotFound();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,14 +190,36 @@ class WarehouseCrudTest extends TestCase
|
||||
|
||||
$this->actingAs($user)
|
||||
->from(route('warehouse-rules.index'))
|
||||
->post(route('warehouse-rules.store'), [
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->post(route('warehouse-rules.store'), $this->csrfPayload([
|
||||
'warehouse_id' => '',
|
||||
'name' => '',
|
||||
'rule_key' => '',
|
||||
'severity' => 'critical',
|
||||
'configuration' => '{bad json',
|
||||
])
|
||||
]))
|
||||
->assertRedirect(route('warehouse-rules.index'))
|
||||
->assertSessionHasErrors(['warehouse_id', 'name', 'rule_key', 'severity', 'configuration']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return payload with a valid test CSRF token.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function csrfPayload(array $payload = []): array
|
||||
{
|
||||
return ['_token' => 'test-token'] + $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the test session with the matching CSRF token.
|
||||
*/
|
||||
private function withCsrfSession(): self
|
||||
{
|
||||
$this->withSession(['_token' => 'test-token']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Architecture;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ProjectCoverageStructureTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Ensure every controller has at least one matching feature test area.
|
||||
*/
|
||||
public function test_controllers_are_accounted_for_by_feature_tests(): void
|
||||
{
|
||||
$controllers = $this->filesMatching(['/../../../app', '/../../../Modules'], '/Controller\.php$/');
|
||||
$this->assertNotEmpty($controllers);
|
||||
|
||||
$testCorpus = '';
|
||||
foreach ($this->filesMatching(['/../../Feature', '/../../../Modules'], '/Test\.php$/') as $testFile) {
|
||||
$testCorpus .= file_get_contents($testFile) ?: '';
|
||||
}
|
||||
|
||||
foreach ($controllers as $controller) {
|
||||
$controllerName = pathinfo($controller, PATHINFO_FILENAME);
|
||||
$resourceName = str_replace(['Controller', 'Dashboard'], '', $controllerName);
|
||||
|
||||
$this->assertTrue(
|
||||
str_contains($testCorpus, $controllerName) || str_contains($testCorpus, $resourceName) || $controllerName === 'Controller',
|
||||
"Controller [{$controllerName}] does not appear to be covered by the feature test corpus.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the WMS baseline component is used without duplicate headers across module views.
|
||||
*/
|
||||
public function test_module_views_do_not_contain_duplicate_wms_page_hero_components(): void
|
||||
{
|
||||
$views = $this->filesMatching(['/../../../Modules'], '/\.blade\.php$/');
|
||||
$this->assertNotEmpty($views);
|
||||
|
||||
foreach ($views as $view) {
|
||||
if (str_ends_with($view, 'partials.blade.php') || str_ends_with($view, '_form.blade.php')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($view) ?: '';
|
||||
|
||||
$this->assertLessThanOrEqual(
|
||||
1,
|
||||
substr_count($contents, '<x-wms.page-hero'),
|
||||
"View [{$view}] should not contain duplicate page headers.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure browser tests exist for desktop and mobile navigation flows.
|
||||
*/
|
||||
public function test_browser_navigation_tests_exist_for_mobile_and_desktop(): void
|
||||
{
|
||||
$browserTests = $this->filesMatching(['/../../Browser'], '/Test\.php$/');
|
||||
$contents = implode("\n", array_map(static fn (string $file): string => file_get_contents($file) ?: '', $browserTests));
|
||||
|
||||
$this->assertStringContainsString('desktop sidebar', strtolower($contents));
|
||||
$this->assertStringContainsString('mobile menu', strtolower($contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $relativeDirectories
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function filesMatching(array $relativeDirectories, string $pattern): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach ($relativeDirectories as $relativeDirectory) {
|
||||
$directory = realpath(__DIR__ . $relativeDirectory);
|
||||
|
||||
if (! is_string($directory) || ! is_dir($directory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if (! $file instanceof SplFileInfo || ! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match($pattern, $file->getFilename()) === 1) {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($files));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user