1. 實作自訂品牌樣式設定,支援上傳 Logo、登入大背景圖與左側卡片背景圖。 2. 整合 ImageHandler 處理上傳圖片,自動壓縮並轉為優質 webp 格式以提升載入速度。 3. 將品牌設定自 Modal 拆分,新增獨立的 branding.blade.php 設定頁面以優化操作體驗。 4. 於 TenantLoginController 新增登入頁預覽 (preview) 與專屬首頁重導向 (indexRedirect) 路由邏輯。 5. 調整 auth/login.blade.php 以支援動態呈現自訂背景圖、Logo、主副標題,且在預覽模式下阻擋登入並顯示頂部橫條。 6. 修正租戶登入安全性比對,加強 (int) 型態校驗並豐富 Log 警示資訊。 7. 更新並對齊 zh_TW.json、en.json 與 ja.json 三語系檔之品牌客製化相關辭彙。 8. 補充完整 Feature Test (CompanySettingsTest),全面涵蓋物理刪除、上傳限制、預覽渲染、跳轉與登入安全性限制之測試。
478 lines
17 KiB
PHP
478 lines
17 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Admin;
|
||
|
||
use Tests\TestCase;
|
||
use App\Models\System\Company;
|
||
use App\Models\System\User;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Spatie\Permission\Models\Role;
|
||
use Spatie\Permission\Models\Permission;
|
||
|
||
class CompanySettingsTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
protected $admin;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
// Setup a system admin with permissions
|
||
$this->admin = User::factory()->create(['company_id' => null]);
|
||
$this->admin->givePermissionTo(Permission::create(['name' => 'menu.permissions.companies']));
|
||
}
|
||
|
||
public function test_can_update_company_settings()
|
||
{
|
||
$company = Company::factory()->create(['original_type' => 'lease', 'current_type' => 'lease']);
|
||
|
||
$settings = [
|
||
'enable_material_code' => true,
|
||
'enable_points' => true,
|
||
];
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->put(route('admin.permission.companies.update', $company->id), [
|
||
'name' => 'Updated Name',
|
||
'code' => $company->code,
|
||
'status' => 1,
|
||
'current_type' => 'lease',
|
||
'settings' => $settings,
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
$this->assertDatabaseHas('companies', [
|
||
'id' => $company->id,
|
||
'name' => 'Updated Name',
|
||
]);
|
||
|
||
$updatedCompany = Company::find($company->id);
|
||
$this->assertEquals(array_merge($updatedCompany->settings ?? [], $settings), $updatedCompany->settings);
|
||
}
|
||
|
||
public function test_can_create_company_with_settings()
|
||
{
|
||
$settings = [
|
||
'enable_material_code' => false,
|
||
'enable_points' => true,
|
||
];
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.store'), [
|
||
'name' => 'New Company',
|
||
'code' => 'NEW001',
|
||
'original_type' => 'lease',
|
||
'status' => 1,
|
||
'settings' => $settings,
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
$company = Company::where('code', 'NEW001')->first();
|
||
$this->assertNotNull($company);
|
||
$this->assertEquals(array_merge($company->settings ?? [], $settings), $company->settings);
|
||
}
|
||
|
||
public function test_can_reuse_code_after_deletion()
|
||
{
|
||
$code = 'REUSE001';
|
||
$company = Company::factory()->create(['code' => $code]);
|
||
|
||
// Delete the company
|
||
$response = $this->actingAs($this->admin)
|
||
->delete(route('admin.permission.companies.destroy', $company->id));
|
||
|
||
$response->assertRedirect();
|
||
$this->assertSoftDeleted('companies', ['id' => $company->id]);
|
||
|
||
// The original code should be freed (renamed in the DB)
|
||
$this->assertDatabaseMissing('companies', [
|
||
'id' => $company->id,
|
||
'code' => $code,
|
||
'deleted_at' => null
|
||
]);
|
||
|
||
// Should be able to create a new company with the same code
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.store'), [
|
||
'name' => 'Brand New Company',
|
||
'code' => $code,
|
||
'original_type' => 'lease',
|
||
'status' => 1,
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
$this->assertDatabaseHas('companies', [
|
||
'name' => 'Brand New Company',
|
||
'code' => $code,
|
||
'deleted_at' => null
|
||
]);
|
||
}
|
||
|
||
public function test_can_update_branding_settings()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'login_title' => 'Custom Welcome Title',
|
||
'login_main_title' => 'Custom Left Main Title',
|
||
'login_sub_title' => 'Custom Left Subtitle',
|
||
]);
|
||
|
||
$response->assertRedirect(route('admin.permission.companies.branding.edit', $company->id));
|
||
|
||
$company->refresh();
|
||
$this->assertEquals('Custom Welcome Title', $company->settings['login_title']);
|
||
$this->assertEquals('Custom Left Main Title', $company->settings['login_main_title']);
|
||
$this->assertEquals('Custom Left Subtitle', $company->settings['login_sub_title']);
|
||
}
|
||
|
||
public function test_cannot_update_branding_if_not_enabled()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => false]
|
||
]);
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'login_title' => 'Custom Welcome Title',
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
$response->assertSessionHas('error');
|
||
|
||
$company->refresh();
|
||
$this->assertArrayNotHasKey('login_title', $company->settings ?? []);
|
||
}
|
||
|
||
public function test_companies_index_page_can_be_rendered()
|
||
{
|
||
$response = $this->actingAs($this->admin)
|
||
->get(route('admin.permission.companies.index'));
|
||
|
||
$response->assertOk();
|
||
$response->assertViewIs('admin.companies.index');
|
||
}
|
||
|
||
public function test_can_upload_branding_logo()
|
||
{
|
||
\Illuminate\Support\Facades\Storage::fake('public');
|
||
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$file = \Illuminate\Http\UploadedFile::fake()->image('logo.png');
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'logo_file' => $file,
|
||
'login_title' => 'Custom Title',
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
|
||
$company->refresh();
|
||
$this->assertNotNull($company->settings['logo_path'] ?? null);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['logo_path']);
|
||
}
|
||
|
||
public function test_branding_validation_fails_redirects_with_errors_and_session()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
// 上傳大於 10MB 的圖片來觸發驗證失敗 (10241 KB)
|
||
$file = \Illuminate\Http\UploadedFile::fake()->image('logo.png')->size(10241);
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'logo_file' => $file,
|
||
'login_title' => str_repeat('a', 101), // 超過 100 字元限制
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
$response->assertSessionHasErrors(['logo_file', 'login_title']);
|
||
}
|
||
|
||
public function test_can_upload_all_branding_images()
|
||
{
|
||
\Illuminate\Support\Facades\Storage::fake('public');
|
||
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$logoFile = \Illuminate\Http\UploadedFile::fake()->image('logo.png');
|
||
$bgFile = \Illuminate\Http\UploadedFile::fake()->image('bg.jpg');
|
||
$cardBgFile = \Illuminate\Http\UploadedFile::fake()->image('card_bg.png');
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'logo_file' => $logoFile,
|
||
'login_bg_file' => $bgFile,
|
||
'login_card_bg_file' => $cardBgFile,
|
||
'login_title' => 'Branding Title',
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
|
||
$company->refresh();
|
||
$this->assertNotNull($company->settings['logo_path'] ?? null);
|
||
$this->assertNotNull($company->settings['login_bg_path'] ?? null);
|
||
$this->assertNotNull($company->settings['login_card_bg_path'] ?? null);
|
||
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['logo_path']);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['login_bg_path']);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['login_card_bg_path']);
|
||
}
|
||
|
||
public function test_can_clear_all_branding_images()
|
||
{
|
||
\Illuminate\Support\Facades\Storage::fake('public');
|
||
|
||
// 先把檔案上傳並建立
|
||
$logoPath = \Illuminate\Support\Facades\Storage::disk('public')->putFile('company_logos', \Illuminate\Http\UploadedFile::fake()->image('logo.png'));
|
||
$bgPath = \Illuminate\Support\Facades\Storage::disk('public')->putFile('company_backgrounds', \Illuminate\Http\UploadedFile::fake()->image('bg.jpg'));
|
||
$cardBgPath = \Illuminate\Support\Facades\Storage::disk('public')->putFile('company_backgrounds', \Illuminate\Http\UploadedFile::fake()->image('card_bg.png'));
|
||
|
||
$company = Company::factory()->create([
|
||
'settings' => [
|
||
'enable_custom_branding' => true,
|
||
'logo_path' => $logoPath,
|
||
'login_bg_path' => $bgPath,
|
||
'login_card_bg_path' => $cardBgPath,
|
||
'login_title' => 'Old Title'
|
||
]
|
||
]);
|
||
|
||
// 確保檔案原本存在
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($logoPath);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($bgPath);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertExists($cardBgPath);
|
||
|
||
// 呼叫清除
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'clear_logo' => '1',
|
||
'clear_login_bg' => '1',
|
||
'clear_login_card_bg' => '1',
|
||
'login_title' => 'New Title'
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
|
||
$company->refresh();
|
||
$this->assertNull($company->settings['logo_path'] ?? null);
|
||
$this->assertNull($company->settings['login_bg_path'] ?? null);
|
||
$this->assertNull($company->settings['login_card_bg_path'] ?? null);
|
||
|
||
// 檔案應該已經被物理刪除
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertMissing($logoPath);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertMissing($bgPath);
|
||
\Illuminate\Support\Facades\Storage::disk('public')->assertMissing($cardBgPath);
|
||
}
|
||
|
||
public function test_branding_validation_fails_for_all_files()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
// 上傳大於 10MB 的大背景圖與卡片背景圖 (10241 KB)
|
||
$bgFile = \Illuminate\Http\UploadedFile::fake()->image('bg.jpg')->size(10241);
|
||
$cardBgFile = \Illuminate\Http\UploadedFile::fake()->image('card_bg.png')->size(10241);
|
||
|
||
$response = $this->actingAs($this->admin)
|
||
->post(route('admin.permission.companies.branding.update', $company->id), [
|
||
'login_bg_file' => $bgFile,
|
||
'login_card_bg_file' => $cardBgFile,
|
||
]);
|
||
|
||
$response->assertRedirect();
|
||
$response->assertSessionHasErrors(['login_bg_file', 'login_card_bg_file']);
|
||
}
|
||
|
||
public function test_tenant_index_redirects_correctly_based_on_auth_status()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'code' => 'test_redirect_comp',
|
||
'status' => 1,
|
||
]);
|
||
|
||
// 未登入狀態:重導向至登入頁面
|
||
$response = $this->get('/c/test_redirect_comp');
|
||
$response->assertRedirect(route('tenant.login', 'test_redirect_comp'));
|
||
|
||
// 已登入狀態:重導向至後台 dashboard
|
||
$response = $this->actingAs($this->admin)->get('/c/test_redirect_comp');
|
||
$response->assertRedirect(route('admin.dashboard'));
|
||
}
|
||
|
||
public function test_tenant_preview_page_can_be_rendered()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'code' => 'test_preview_comp',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
// 不論是否登入,preview 頁面都應該能正常渲染
|
||
$response = $this->actingAs($this->admin)->get('/c/test_preview_comp/preview');
|
||
$response->assertOk();
|
||
$response->assertViewIs('auth.login');
|
||
$response->assertViewHas('company');
|
||
$response->assertViewHas('isPreview', true);
|
||
}
|
||
|
||
public function test_tenant_preview_page_redirects_if_not_enabled()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'code' => 'test_disabled_preview',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => false]
|
||
]);
|
||
|
||
// 未開啟客製化品牌功能,導回一般登入
|
||
$response = $this->actingAs($this->admin)->get('/c/test_disabled_preview/preview');
|
||
$response->assertRedirect(route('login'));
|
||
}
|
||
|
||
public function test_branding_edit_page_can_be_rendered()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$response = $this->actingAs($this->admin)->get(route('admin.permission.companies.branding.edit', $company->id));
|
||
$response->assertOk();
|
||
$response->assertViewIs('admin.companies.branding');
|
||
$response->assertViewHas('company');
|
||
}
|
||
|
||
public function test_branding_edit_page_redirects_if_not_enabled()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'settings' => ['enable_custom_branding' => false]
|
||
]);
|
||
|
||
$response = $this->actingAs($this->admin)->get(route('admin.permission.companies.branding.edit', $company->id));
|
||
$response->assertRedirect(route('admin.permission.companies.index'));
|
||
$response->assertSessionHas('error');
|
||
}
|
||
|
||
public function test_tenant_can_login_successfully()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'code' => 'tenant_a',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$user = User::factory()->create([
|
||
'company_id' => $company->id,
|
||
'username' => 'tenant_user_a',
|
||
'password' => bcrypt('password123'),
|
||
'status' => 1
|
||
]);
|
||
|
||
$response = $this->post('/c/tenant_a/login', [
|
||
'username' => 'tenant_user_a',
|
||
'password' => 'password123',
|
||
]);
|
||
|
||
$response->assertRedirect(route('admin.dashboard'));
|
||
$this->assertAuthenticatedAs($user);
|
||
}
|
||
|
||
public function test_tenant_cannot_login_if_user_belongs_to_another_company()
|
||
{
|
||
$companyA = Company::factory()->create([
|
||
'code' => 'tenant_a',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$companyB = Company::factory()->create([
|
||
'code' => 'tenant_b',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
// User B belongs to Company B
|
||
$userB = User::factory()->create([
|
||
'company_id' => $companyB->id,
|
||
'username' => 'user_b',
|
||
'password' => bcrypt('password123'),
|
||
'status' => 1
|
||
]);
|
||
|
||
// Attempting to login User B at Company A's portal
|
||
$response = $this->from('/c/tenant_a/login')->post('/c/tenant_a/login', [
|
||
'username' => 'user_b',
|
||
'password' => 'password123',
|
||
]);
|
||
|
||
$response->assertRedirect('/c/tenant_a/login');
|
||
$response->assertSessionHasErrors(['username']);
|
||
$this->assertGuest();
|
||
}
|
||
|
||
public function test_tenant_cannot_login_if_account_disabled()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'code' => 'tenant_a',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
// User is disabled (status = 0)
|
||
$user = User::factory()->create([
|
||
'company_id' => $company->id,
|
||
'username' => 'disabled_user',
|
||
'password' => bcrypt('password123'),
|
||
'status' => 0
|
||
]);
|
||
|
||
$response = $this->from('/c/tenant_a/login')->post('/c/tenant_a/login', [
|
||
'username' => 'disabled_user',
|
||
'password' => 'password123',
|
||
]);
|
||
|
||
$response->assertRedirect('/c/tenant_a/login');
|
||
$response->assertSessionHasErrors(['username']);
|
||
$this->assertGuest();
|
||
}
|
||
|
||
public function test_tenant_cannot_login_with_incorrect_password()
|
||
{
|
||
$company = Company::factory()->create([
|
||
'code' => 'tenant_a',
|
||
'status' => 1,
|
||
'settings' => ['enable_custom_branding' => true]
|
||
]);
|
||
|
||
$user = User::factory()->create([
|
||
'company_id' => $company->id,
|
||
'username' => 'tenant_user',
|
||
'password' => bcrypt('password123'),
|
||
'status' => 1
|
||
]);
|
||
|
||
$response = $this->from('/c/tenant_a/login')->post('/c/tenant_a/login', [
|
||
'username' => 'tenant_user',
|
||
'password' => 'wrongpassword',
|
||
]);
|
||
|
||
$response->assertRedirect('/c/tenant_a/login');
|
||
$response->assertSessionHasErrors(['password']);
|
||
$this->assertGuest();
|
||
}
|
||
}
|
||
|