'API Company A', 'contact_name' => 'API Admin A', 'email' => 'admin@api-comp-a.com', 'desired_subdomain' => 'api-comp-a', 'email_verified_at' => now(), 'status' => 'approved', ]); (new ProvisionTenantJob($inquiryA))->handle(); // 2. Create Tenant B $inquiryB = Inquiry::create([ 'company_name' => 'API Company B', 'contact_name' => 'API Admin B', 'email' => 'admin@api-comp-b.com', 'desired_subdomain' => 'api-comp-b', 'email_verified_at' => now(), 'status' => 'approved', ]); (new ProvisionTenantJob($inquiryB))->handle(); // 3. Login to Tenant A via API with X-Tenant header using Tenant A credentials -> SUCCESS if (function_exists('tenancy') && tenancy()->initialized) { tenancy()->end(); } $responseA = $this->withHeaders(['X-Tenant' => 'api-comp-a']) ->postJson('/api/auth/login', [ 'email' => 'admin@api-comp-a.com', 'password' => '12345678', ]); $responseA->assertStatus(200) ->assertJsonPath('success', true); // 4. Try logging into Tenant A via API using actual Tenant B credentials -> FAILS 401 if (function_exists('tenancy') && tenancy()->initialized) { tenancy()->end(); } $responseCross = $this->withHeaders(['X-Tenant' => 'api-comp-a']) ->postJson('/api/auth/login', [ 'email' => 'admin@api-comp-b.com', 'password' => '12345678', ]); $responseCross->assertStatus(401) ->assertJsonPath('success', false); // 5. Try logging in without tenant context (no header, no subdomain, no domain host) -> FAILS 422 if (function_exists('tenancy') && tenancy()->initialized) { tenancy()->end(); } $responseNoTenant = $this->withHeaders(['X-Tenant' => ''])->postJson('/api/auth/login', [ 'email' => 'admin@api-comp-a.com', 'password' => '12345678', ]); $responseNoTenant->assertStatus(422) ->assertJsonPath('message', 'Tenant context is required.'); // 6. Verify API token is created inside Tenant A database $tenantA = Tenant::find('api-comp-a'); $tenantA->run(function () { $userA = \App\Models\User::where('email', 'admin@api-comp-a.com')->first(); $this->assertNotNull($userA); $this->assertGreaterThan(0, $userA->tokens()->count()); }); } public function test_host_based_mobile_login_and_authenticated_request_are_tenant_isolated(): void { $inquiryA = Inquiry::create([ 'company_name' => 'Mobile Host Company A', 'contact_name' => 'Mobile Admin A', 'email' => 'admin@mobile-host-a.com', 'desired_subdomain' => 'mobile-host-a', 'email_verified_at' => now(), 'status' => 'approved', ]); (new ProvisionTenantJob($inquiryA))->handle(); $inquiryB = Inquiry::create([ 'company_name' => 'Mobile Host Company B', 'contact_name' => 'Mobile Admin B', 'email' => 'admin@mobile-host-b.com', 'desired_subdomain' => 'mobile-host-b', 'email_verified_at' => now(), 'status' => 'approved', ]); (new ProvisionTenantJob($inquiryB))->handle(); tenancy()->end(); $domainHost = config('app.url_domain', parse_url(config('app.url'), PHP_URL_HOST) ?: 'localhost'); $hostA = "mobile-host-a.{$domainHost}"; $loginA = $this->withServerVariables(['HTTP_HOST' => $hostA]) ->postJson("http://{$hostA}/api/auth/login", [ 'email' => 'admin@mobile-host-a.com', 'password' => '12345678', 'device_name' => 'mobile-a', ]); $loginA->assertStatus(200)->assertJsonPath('success', true); $tokenA = $loginA->json('token'); $this->assertNotEmpty($tokenA); $userA = $this->withServerVariables(['HTTP_HOST' => $hostA]) ->withHeader('Authorization', "Bearer {$tokenA}") ->getJson("http://{$hostA}/api/user"); $userA->assertStatus(200)->assertJsonPath('user.email', 'admin@mobile-host-a.com'); tenancy()->end(); $crossLogin = $this->withServerVariables(['HTTP_HOST' => $hostA]) ->postJson("http://{$hostA}/api/auth/login", [ 'email' => 'admin@mobile-host-b.com', 'password' => '12345678', 'device_name' => 'mobile-cross-tenant', ]); $crossLogin->assertStatus(401)->assertJsonPath('success', false); } }