Files
HRM-System/tests/Feature/TenantAdminIsolationTest.php

256 lines
9.6 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Inquiry;
use App\Jobs\ProvisionTenantJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\Tenant;
use Tests\TestCase;
class TenantAdminIsolationTest extends TestCase
{
use RefreshDatabase;
public function test_tenant_provisioning_creates_admin_account_with_inquiry_email_and_default_password(): void
{
$inquiryEmail = 'custom.admin@acmecompany.com';
$inquiry = Inquiry::create([
'company_name' => 'Acme Corporation',
'contact_name' => 'Acme Admin',
'email' => $inquiryEmail,
'desired_subdomain' => 'acme-test-domain',
'employee_count' => 25,
'requested_features' => ['payroll', 'geofence'],
'email_verified_at' => now(),
'status' => 'approved',
]);
$job = new ProvisionTenantJob($inquiry);
$job->handle();
$tenant = Tenant::find('acme-test-domain');
$this->assertNotNull($tenant);
// Verify that inside the tenant database context:
// 1. The admin user exists ONLY under $inquiryEmail (the email entered in the inquiry wizard).
// 2. The admin user can authenticate with password '12345678'.
$tenant->run(function () use ($inquiryEmail) {
$user = \App\Models\User::where('email', $inquiryEmail)->first();
$this->assertNotNull($user, "Tenant admin user with email {$inquiryEmail} was not found in tenant database.");
// Assert password verifies against default password '12345678'
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('12345678', $user->password));
// Assert non-registered email does not exist in this tenant's isolated database
$unregisteredUser = \App\Models\User::where('email', 'unregistered.other@domain.com')->first();
$this->assertNull($unregisteredUser);
});
}
public function test_subdomains_have_isolated_databases_and_credentials(): void
{
// Tenant A
$inquiryA = Inquiry::create([
'company_name' => 'Company A',
'contact_name' => 'Admin A',
'email' => 'admin@companya.com',
'desired_subdomain' => 'companya-tenant',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryA))->handle();
// Tenant B
$inquiryB = Inquiry::create([
'company_name' => 'Company B',
'contact_name' => 'Admin B',
'email' => 'admin@companyb.com',
'desired_subdomain' => 'companyb-tenant',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryB))->handle();
// Verify Tenant A DB contains Admin A
$tenantA = Tenant::find('companya-tenant');
$this->assertNotNull($tenantA);
$tenantA->run(function () {
$this->assertNotNull(\App\Models\User::where('email', 'admin@companya.com')->first());
});
// Verify Tenant B DB contains Admin B
$tenantB = Tenant::find('companyb-tenant');
$this->assertNotNull($tenantB);
$tenantB->run(function () {
$this->assertNotNull(\App\Models\User::where('email', 'admin@companyb.com')->first());
});
}
public function test_real_http_domain_cross_tenant_login_rejection(): void
{
// 1. Provision Subdomain A (admin@companya.com / 12345678)
$inquiryA = Inquiry::create([
'company_name' => 'Company A',
'contact_name' => 'Admin A',
'email' => 'admin@companya.com',
'desired_subdomain' => 'subdomain-a',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryA))->handle();
// 2. Provision Subdomain B (admin@companyb.com / 12345678)
$inquiryB = Inquiry::create([
'company_name' => 'Company B',
'contact_name' => 'Admin B',
'email' => 'admin@companyb.com',
'desired_subdomain' => 'subdomain-b',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryB))->handle();
if (function_exists('tenancy') && tenancy()->initialized) {
tenancy()->end();
}
$domainHost = config('app.url_domain', 'localhost');
// 3. Real HTTP POST to Subdomain A (/login) with Subdomain B credentials -> Fails validation / 401 / redirect
$tenantA = \App\Models\Tenant::find('subdomain-a');
tenancy()->initialize($tenantA);
$responseAToB = $this->withServerVariables(['HTTP_HOST' => "subdomain-a.{$domainHost}"])
->post("http://subdomain-a.{$domainHost}/login", [
'email' => 'admin@companyb.com',
'password' => '12345678',
]);
$this->assertTrue(
$responseAToB->isRedirect() || $responseAToB->status() === 422 || $responseAToB->status() === 401
);
$this->assertGuest();
// 4. Real HTTP POST to Subdomain B (/login) with Subdomain A credentials -> Fails validation / 401 / redirect
if (function_exists('tenancy') && tenancy()->initialized) {
tenancy()->end();
}
$tenantB = \App\Models\Tenant::find('subdomain-b');
tenancy()->initialize($tenantB);
$responseBToA = $this->withServerVariables(['HTTP_HOST' => "subdomain-b.{$domainHost}"])
->post("http://subdomain-b.{$domainHost}/login", [
'email' => 'admin@companya.com',
'password' => '12345678',
]);
$this->assertTrue(
$responseBToA->isRedirect() || $responseBToA->status() === 422 || $responseBToA->status() === 401
);
$this->assertGuest();
}
public function test_subdomain_a_rejects_subdomain_b_credentials_without_manual_tenant_initialization(): void
{
$inquiryA = Inquiry::create([
'company_name' => 'Company A',
'contact_name' => 'Admin A',
'email' => 'admin@subdomain-a.test',
'desired_subdomain' => 'strict-subdomain-a',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryA))->handle();
$inquiryB = Inquiry::create([
'company_name' => 'Company B',
'contact_name' => 'Admin B',
'email' => 'admin@subdomain-b.test',
'desired_subdomain' => 'strict-subdomain-b',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryB))->handle();
if (function_exists('tenancy') && tenancy()->initialized) {
tenancy()->end();
}
$domainHost = config('app.url_domain', 'localhost');
$response = $this->withServerVariables([
'HTTP_HOST' => "strict-subdomain-a.{$domainHost}",
])->post("http://strict-subdomain-a.{$domainHost}/login", [
'email' => 'admin@subdomain-b.test',
'password' => '12345678',
]);
$this->assertGuest();
$this->assertNotSame(
'/dashboard',
parse_url($response->headers->get('Location', ''), PHP_URL_PATH),
'Cross-tenant credentials must not redirect to the authenticated dashboard.'
);
}
public function test_real_subdomain_login_accepts_only_the_subdomain_credentials(): void
{
$inquiryA = Inquiry::create([
'company_name' => 'Company A',
'contact_name' => 'Admin A',
'email' => 'admin@real-a.test',
'desired_subdomain' => 'real-subdomain-a',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryA))->handle();
$inquiryB = Inquiry::create([
'company_name' => 'Company B',
'contact_name' => 'Admin B',
'email' => 'admin@real-b.test',
'desired_subdomain' => 'real-subdomain-b',
'email_verified_at' => now(),
'status' => 'approved',
]);
(new ProvisionTenantJob($inquiryB))->handle();
if (function_exists('tenancy') && tenancy()->initialized) {
tenancy()->end();
}
$domainHost = config('app.url_domain', 'localhost');
$request = fn (string $subdomain, string $email) => $this
->withServerVariables(['HTTP_HOST' => "{$subdomain}.{$domainHost}"])
->post("http://{$subdomain}.{$domainHost}/login", [
'email' => $email,
'password' => '12345678',
]);
$aWithA = $request('real-subdomain-a', 'admin@real-a.test');
$this->assertSame(
'/dashboard',
parse_url($aWithA->headers->get('Location', ''), PHP_URL_PATH),
'Subdomain A credentials must authenticate on Subdomain A.'
);
$this->app['auth']->logout();
if (function_exists('tenancy') && tenancy()->initialized) {
tenancy()->end();
}
$aWithB = $request('real-subdomain-a', 'admin@real-b.test');
$this->assertGuest();
$this->assertNotSame('/dashboard', parse_url($aWithB->headers->get('Location', ''), PHP_URL_PATH));
if (function_exists('tenancy') && tenancy()->initialized) {
tenancy()->end();
}
$bWithA = $request('real-subdomain-b', 'admin@real-a.test');
$this->assertGuest();
$this->assertNotSame('/dashboard', parse_url($bWithA->headers->get('Location', ''), PHP_URL_PATH));
}
}