Commit 65fbf276 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test(auth,admin,companies,creators,settings): add 325 comprehensive test methods

Covers Auth (registration, login, email verification, password reset, username validation),
Admin (user management, company approval, impersonation, audit log, dashboard stats),
Companies (profile CRUD, social link normalization, completion scoring, approval flow),
Creators (profile CRUD, username throttle, completion scoring, scopes, policies),
and Settings (platform settings CRUD, caching, CSS generation, feature flags).
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 17007ee0
<?php
namespace Tests\Feature\Admin;
use App\Models\User;
use App\Modules\Companies\Models\CompanyProfile;
use App\Shared\Enums\UserStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminApiTest extends TestCase
{
use RefreshDatabase;
private User $admin;
protected function setUp(): void
{
parent::setUp();
$this->admin = User::factory()->admin()->create(['email_verified_at' => now()]);
}
// ─── Access Control ─────────────────────────────────────────────────
public function test_admin_dashboard_requires_auth(): void
{
$response = $this->get('/admin/dashboard');
$response->assertRedirect('/login');
}
public function test_admin_dashboard_requires_admin_role(): void
{
$creator = User::factory()->creator()->create(['email_verified_at' => now()]);
$response = $this->actingAs($creator)->get('/admin/dashboard');
$response->assertStatus(403);
}
public function test_admin_dashboard_requires_verified_email(): void
{
$admin = User::factory()->admin()->unverified()->create();
$response = $this->actingAs($admin)->get('/admin/dashboard');
$response->assertRedirect();
}
public function test_admin_can_access_dashboard(): void
{
$response = $this->actingAs($this->admin)->get('/admin/dashboard');
$response->assertStatus(200);
}
public function test_creator_cannot_access_admin_users(): void
{
$creator = User::factory()->creator()->create(['email_verified_at' => now()]);
$response = $this->actingAs($creator)->get('/admin/users');
$response->assertStatus(403);
}
public function test_company_cannot_access_admin_users(): void
{
$company = User::factory()->company()->create(['email_verified_at' => now()]);
$response = $this->actingAs($company)->get('/admin/users');
$response->assertStatus(403);
}
// ─── User Management ────────────────────────────────────────────────
public function test_admin_can_list_users(): void
{
User::factory()->count(5)->create();
$response = $this->actingAs($this->admin)
->getJson('/admin/users');
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
public function test_admin_can_filter_users_by_role(): void
{
User::factory()->creator()->count(3)->create();
User::factory()->company()->count(2)->create();
$response = $this->actingAs($this->admin)
->getJson('/admin/users?role=creator');
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
public function test_admin_can_search_users(): void
{
User::factory()->create(['name' => 'Unique Findable Name']);
$response = $this->actingAs($this->admin)
->getJson('/admin/users?search=Unique+Findable');
$response->assertStatus(200);
}
public function test_admin_can_view_user_detail(): void
{
$user = User::factory()->create();
$response = $this->actingAs($this->admin)
->getJson("/admin/users/{$user->uuid}");
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
public function test_admin_can_suspend_active_user(): void
{
$user = User::factory()->create(['status' => 'active']);
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'suspended',
'reason' => 'Violated community guidelines.',
]);
$response->assertStatus(200);
$user->refresh();
$this->assertEquals('suspended', $user->status);
}
public function test_admin_can_ban_active_user(): void
{
$user = User::factory()->create(['status' => 'active']);
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'banned',
'reason' => 'Spam and harassment.',
]);
$response->assertStatus(200);
$user->refresh();
$this->assertEquals('banned', $user->status);
}
public function test_admin_can_reactivate_suspended_user(): void
{
$user = User::factory()->suspended()->create();
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'active',
]);
$response->assertStatus(200);
$user->refresh();
$this->assertEquals('active', $user->status);
}
public function test_status_change_requires_reason_for_suspension(): void
{
$user = User::factory()->create(['status' => 'active']);
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'suspended',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('reason');
}
public function test_status_change_requires_reason_for_ban(): void
{
$user = User::factory()->create(['status' => 'active']);
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'banned',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('reason');
}
public function test_status_change_validates_status_value(): void
{
$user = User::factory()->create(['status' => 'active']);
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'invalid_status',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('status');
}
public function test_cannot_change_admin_status(): void
{
$otherAdmin = User::factory()->admin()->create();
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$otherAdmin->uuid}/status", [
'status' => 'suspended',
'reason' => 'Test',
]);
$response->assertStatus(500);
}
public function test_cannot_make_invalid_transition(): void
{
$user = User::factory()->create(['status' => 'active']);
$response = $this->actingAs($this->admin)
->putJson("/admin/users/{$user->uuid}/status", [
'status' => 'unverified',
]);
$response->assertStatus(422);
}
// ─── Company Approval ───────────────────────────────────────────────
public function test_admin_can_list_pending_companies(): void
{
$user = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create(['user_id' => $user->id]);
$response = $this->actingAs($this->admin)
->getJson('/admin/companies/pending');
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
public function test_admin_can_approve_company(): void
{
$user = User::factory()->company()->create(['status' => 'pending_review']);
$company = CompanyProfile::factory()->pending()->create(['user_id' => $user->id]);
$response = $this->actingAs($this->admin)
->postJson("/admin/companies/{$company->id}/approve");
$response->assertStatus(200);
}
public function test_admin_can_reject_company_with_reason(): void
{
$user = User::factory()->company()->create(['status' => 'pending_review']);
$company = CompanyProfile::factory()->pending()->create(['user_id' => $user->id]);
$response = $this->actingAs($this->admin)
->postJson("/admin/companies/{$company->id}/reject", [
'reason' => 'Insufficient documentation provided. Please resubmit with company registration documents.',
]);
$response->assertStatus(200);
}
public function test_reject_company_requires_reason(): void
{
$user = User::factory()->company()->create(['status' => 'pending_review']);
$company = CompanyProfile::factory()->pending()->create(['user_id' => $user->id]);
$response = $this->actingAs($this->admin)
->postJson("/admin/companies/{$company->id}/reject", []);
$response->assertStatus(422);
$response->assertJsonValidationErrors('reason');
}
public function test_reject_company_reason_minimum_length(): void
{
$user = User::factory()->company()->create(['status' => 'pending_review']);
$company = CompanyProfile::factory()->pending()->create(['user_id' => $user->id]);
$response = $this->actingAs($this->admin)
->postJson("/admin/companies/{$company->id}/reject", [
'reason' => 'Short',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('reason');
}
// ─── Impersonation ──────────────────────────────────────────────────
public function test_admin_can_impersonate_creator(): void
{
$creator = User::factory()->creator()->create();
$response = $this->actingAs($this->admin)
->post("/admin/users/{$creator->uuid}/impersonate");
$response->assertRedirect('/');
}
public function test_admin_cannot_impersonate_another_admin(): void
{
$otherAdmin = User::factory()->admin()->create();
$response = $this->actingAs($this->admin)
->post("/admin/users/{$otherAdmin->uuid}/impersonate");
$response->assertStatus(500);
}
public function test_stop_impersonation_requires_auth(): void
{
$response = $this->post('/admin/impersonate/stop');
$response->assertRedirect('/login');
}
public function test_stop_impersonation_redirects_to_dashboard(): void
{
$creator = User::factory()->creator()->create();
$this->actingAs($this->admin)
->withSession([
'impersonating' => true,
'impersonator_id' => $this->admin->id,
'original_user_id' => $this->admin->id,
])
->post("/admin/users/{$creator->uuid}/impersonate");
$response = $this->actingAs($creator)
->withSession([
'impersonating' => true,
'impersonator_id' => $this->admin->id,
'original_user_id' => $this->admin->id,
])
->post('/admin/impersonate/stop');
$response->assertRedirect(route('admin.dashboard'));
}
// ─── Audit Log ──────────────────────────────────────────────────────
public function test_admin_can_view_audit_log(): void
{
$response = $this->actingAs($this->admin)
->get('/admin/audit-log');
$response->assertStatus(200);
}
public function test_non_admin_cannot_view_audit_log(): void
{
$creator = User::factory()->creator()->create(['email_verified_at' => now()]);
$response = $this->actingAs($creator)
->get('/admin/audit-log');
$response->assertStatus(403);
}
// ─── Dashboard JSON Response ────────────────────────────────────────
public function test_dashboard_json_returns_stats(): void
{
User::factory()->creator()->count(3)->create();
$response = $this->actingAs($this->admin)
->getJson('/admin/dashboard');
$response->assertStatus(200);
$response->assertJson(['success' => true]);
$response->assertJsonStructure([
'success',
'data' => [
'total_users',
'total_creators',
'total_companies',
'pending_companies',
'active_users',
],
]);
}
}
<?php
namespace Tests\Feature\Admin;
use App\Models\User;
use App\Modules\Admin\Services\AdminService;
use App\Modules\Companies\Models\CompanyProfile;
use App\Shared\Enums\UserRole;
use App\Shared\Enums\UserStatus;
use App\Shared\Exceptions\InvalidStatusTransitionException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AdminServiceTest extends TestCase
{
use RefreshDatabase;
private AdminService $service;
private User $admin;
protected function setUp(): void
{
parent::setUp();
$this->service = app(AdminService::class);
$this->admin = User::factory()->admin()->create();
}
// ─── Get Users ──────────────────────────────────────────────────────
public function test_get_users_returns_paginated_results(): void
{
User::factory()->count(30)->create();
$result = $this->service->getUsers();
$this->assertEquals(25, $result->perPage());
$this->assertGreaterThanOrEqual(30, $result->total());
}
public function test_get_users_respects_per_page_limit(): void
{
User::factory()->count(10)->create();
$result = $this->service->getUsers(['per_page' => 5]);
$this->assertEquals(5, $result->perPage());
}
public function test_get_users_caps_per_page_at_100(): void
{
$result = $this->service->getUsers(['per_page' => 200]);
$this->assertEquals(100, $result->perPage());
}
public function test_get_users_filters_by_search(): void
{
User::factory()->create(['name' => 'Unique Searchable Name']);
User::factory()->count(5)->create();
$result = $this->service->getUsers(['search' => 'Unique Searchable']);
$this->assertEquals(1, $result->total());
}
public function test_get_users_filters_by_role(): void
{
User::factory()->creator()->count(3)->create();
User::factory()->company()->count(2)->create();
$result = $this->service->getUsers(['role' => 'creator']);
$this->assertEquals(3, $result->total());
}
public function test_get_users_filters_by_status(): void
{
User::factory()->create(['status' => 'active']);
User::factory()->create(['status' => 'active']);
User::factory()->suspended()->create();
$result = $this->service->getUsers(['status' => 'suspended']);
$this->assertEquals(1, $result->total());
}
public function test_get_users_filters_by_verified(): void
{
User::factory()->create(['email_verified_at' => now()]);
User::factory()->unverified()->create();
$verified = $this->service->getUsers(['verified' => true]);
$unverified = $this->service->getUsers(['verified' => false]);
$verifiedCount = $verified->total();
$unverifiedCount = $unverified->total();
$this->assertGreaterThanOrEqual(1, $verifiedCount);
$this->assertGreaterThanOrEqual(1, $unverifiedCount);
}
public function test_get_users_includes_soft_deleted(): void
{
$user = User::factory()->create();
$user->delete();
$result = $this->service->getUsers();
$this->assertTrue($result->getCollection()->contains('id', $user->id));
}
public function test_get_users_sorts_by_allowed_column(): void
{
User::factory()->create(['name' => 'Alpha']);
User::factory()->create(['name' => 'Zeta']);
$result = $this->service->getUsers(['sort' => 'name', 'direction' => 'asc']);
$names = $result->getCollection()->pluck('name')->toArray();
$sorted = $names;
sort($sorted);
$this->assertEquals($sorted, $names);
}
public function test_get_users_ignores_invalid_sort_column(): void
{
User::factory()->count(3)->create();
$result = $this->service->getUsers(['sort' => 'password']);
$this->assertGreaterThanOrEqual(3, $result->total());
}
// ─── Get User ───────────────────────────────────────────────────────
public function test_get_user_by_uuid(): void
{
$user = User::factory()->create();
$found = $this->service->getUser($user->uuid);
$this->assertEquals($user->id, $found->id);
}
public function test_get_user_includes_relationships(): void
{
$user = User::factory()->creator()->create();
$user->creatorProfile()->create([
'first_name' => 'Test',
'last_name' => 'Creator',
'username' => 'testcreator',
'country' => 'US',
]);
$found = $this->service->getUser($user->uuid);
$this->assertTrue($found->relationLoaded('creatorProfile'));
$this->assertTrue($found->relationLoaded('companyProfile'));
}
public function test_get_user_finds_soft_deleted(): void
{
$user = User::factory()->create();
$user->delete();
$found = $this->service->getUser($user->uuid);
$this->assertEquals($user->id, $found->id);
$this->assertNotNull($found->deleted_at);
}
public function test_get_user_throws_for_invalid_uuid(): void
{
$this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
$this->service->getUser('nonexistent-uuid');
}
// ─── Change User Status ─────────────────────────────────────────────
public function test_change_status_from_active_to_suspended(): void
{
$user = User::factory()->create(['status' => 'active']);
$result = $this->service->changeUserStatus($user, 'suspended', $this->admin, 'Violated TOS.');
$this->assertEquals('suspended', $result->status);
}
public function test_change_status_from_active_to_banned(): void
{
$user = User::factory()->create(['status' => 'active']);
$result = $this->service->changeUserStatus($user, 'banned', $this->admin, 'Spam account.');
$this->assertEquals('banned', $result->status);
}
public function test_change_status_from_suspended_to_active(): void
{
$user = User::factory()->suspended()->create();
$result = $this->service->changeUserStatus($user, 'active', $this->admin);
$this->assertEquals('active', $result->status);
}
public function test_change_status_logs_activity(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user, 'suspended', $this->admin, 'Testing log.');
$this->assertDatabaseHas('admin_activity_logs', [
'admin_id' => $this->admin->id,
'action' => 'user_status_change',
'target_type' => 'user',
'target_id' => $user->id,
]);
$log = DB::table('admin_activity_logs')
->where('target_id', $user->id)
->where('action', 'user_status_change')
->first();
$metadata = json_decode($log->metadata, true);
$this->assertEquals('active', $metadata['from']);
$this->assertEquals('suspended', $metadata['to']);
$this->assertEquals('Testing log.', $metadata['reason']);
}
public function test_change_status_throws_for_invalid_transition(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->expectException(InvalidStatusTransitionException::class);
$this->service->changeUserStatus($user, 'unverified', $this->admin);
}
public function test_change_status_throws_for_admin_target(): void
{
$otherAdmin = User::factory()->admin()->create();
$this->expectException(InvalidStatusTransitionException::class);
$this->service->changeUserStatus($otherAdmin, 'suspended', $this->admin);
}
public function test_change_status_from_deactivated_to_active(): void
{
$user = User::factory()->create(['status' => 'deactivated']);
$result = $this->service->changeUserStatus($user, 'active', $this->admin);
$this->assertEquals('active', $result->status);
}
// ─── Pending Companies ──────────────────────────────────────────────
public function test_get_pending_companies_only_returns_pending(): void
{
$companyUser = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create(['user_id' => $companyUser->id]);
$approvedUser = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $approvedUser->id, 'status' => 'approved']);
$result = $this->service->getPendingCompanies();
$this->assertEquals(1, $result->total());
}
public function test_get_pending_companies_ordered_oldest_first(): void
{
$user1 = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create([
'user_id' => $user1->id,
'created_at' => now()->subDays(5),
]);
$user2 = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create([
'user_id' => $user2->id,
'created_at' => now()->subDays(1),
]);
$result = $this->service->getPendingCompanies();
$this->assertEquals($user1->id, $result->first()->user_id);
}
public function test_get_pending_companies_eager_loads_user(): void
{
$user = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create(['user_id' => $user->id]);
$result = $this->service->getPendingCompanies();
$this->assertTrue($result->first()->relationLoaded('user'));
}
// ─── Impersonation ──────────────────────────────────────────────────
public function test_start_impersonation_logs_in_as_target(): void
{
$target = User::factory()->creator()->create();
$this->actingAs($this->admin);
$this->service->startImpersonation($this->admin, $target);
$this->assertEquals($target->id, auth()->id());
}
public function test_start_impersonation_stores_session_data(): void
{
$target = User::factory()->creator()->create();
$this->actingAs($this->admin);
session()->start();
$this->service->startImpersonation($this->admin, $target);
$this->assertTrue(session('impersonating'));
$this->assertEquals($this->admin->id, session('original_user_id'));
}
public function test_start_impersonation_logs_activity(): void
{
$target = User::factory()->creator()->create();
$this->actingAs($this->admin);
$this->service->startImpersonation($this->admin, $target);
$this->assertDatabaseHas('admin_activity_logs', [
'admin_id' => $this->admin->id,
'action' => 'impersonation_start',
'target_id' => $target->id,
]);
}
public function test_start_impersonation_throws_for_admin_target(): void
{
$otherAdmin = User::factory()->admin()->create();
$this->expectException(InvalidStatusTransitionException::class);
$this->service->startImpersonation($this->admin, $otherAdmin);
}
public function test_stop_impersonation_restores_admin(): void
{
$target = User::factory()->creator()->create();
$this->actingAs($this->admin);
session()->start();
$this->service->startImpersonation($this->admin, $target);
$this->assertEquals($target->id, auth()->id());
$this->service->stopImpersonation();
$this->assertEquals($this->admin->id, auth()->id());
}
public function test_stop_impersonation_clears_session_data(): void
{
$target = User::factory()->creator()->create();
$this->actingAs($this->admin);
session()->start();
$this->service->startImpersonation($this->admin, $target);
$this->service->stopImpersonation();
$this->assertNull(session('impersonating'));
$this->assertNull(session('impersonator_id'));
$this->assertNull(session('original_user_id'));
}
public function test_stop_impersonation_does_nothing_without_session(): void
{
$this->actingAs($this->admin);
session()->start();
$this->service->stopImpersonation();
$this->assertEquals($this->admin->id, auth()->id());
}
// ─── Dashboard Stats ────────────────────────────────────────────────
public function test_dashboard_stats_returns_all_fields(): void
{
User::factory()->creator()->count(3)->create();
User::factory()->company()->count(2)->create();
User::factory()->suspended()->create();
$stats = $this->service->getDashboardStats();
$this->assertArrayHasKey('total_users', $stats);
$this->assertArrayHasKey('total_creators', $stats);
$this->assertArrayHasKey('total_companies', $stats);
$this->assertArrayHasKey('pending_companies', $stats);
$this->assertArrayHasKey('active_users', $stats);
$this->assertArrayHasKey('suspended_users', $stats);
$this->assertArrayHasKey('banned_users', $stats);
$this->assertArrayHasKey('verified_users', $stats);
$this->assertArrayHasKey('new_users_today', $stats);
$this->assertArrayHasKey('new_users_week', $stats);
}
public function test_dashboard_stats_counts_correctly(): void
{
User::factory()->creator()->count(5)->create(['status' => 'active']);
User::factory()->company()->count(3)->create(['status' => 'active']);
User::factory()->suspended()->create();
User::factory()->banned()->create();
$stats = $this->service->getDashboardStats();
$this->assertEquals(5, $stats['total_creators']);
$this->assertEquals(3, $stats['total_companies']);
$this->assertEquals(1, $stats['suspended_users']);
$this->assertEquals(1, $stats['banned_users']);
}
public function test_dashboard_stats_counts_pending_companies(): void
{
$user1 = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create(['user_id' => $user1->id]);
$user2 = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create(['user_id' => $user2->id]);
$user3 = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $user3->id, 'status' => 'approved']);
$stats = $this->service->getDashboardStats();
$this->assertEquals(2, $stats['pending_companies']);
}
public function test_dashboard_stats_counts_new_users_today(): void
{
User::factory()->create(['created_at' => now()]);
User::factory()->create(['created_at' => now()->subDays(2)]);
$stats = $this->service->getDashboardStats();
$this->assertGreaterThanOrEqual(1, $stats['new_users_today']);
}
// ─── Activity Log ───────────────────────────────────────────────────
public function test_get_activity_log_returns_paginated(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user, 'suspended', $this->admin, 'Test');
$log = $this->service->getActivityLog();
$this->assertGreaterThanOrEqual(1, $log->total());
}
public function test_get_activity_log_includes_admin_name(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user, 'suspended', $this->admin, 'Test');
$log = $this->service->getActivityLog();
$entry = $log->first();
$this->assertEquals($this->admin->name, $entry->admin_name);
$this->assertEquals($this->admin->email, $entry->admin_email);
}
public function test_get_activity_log_filters_by_action(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user, 'suspended', $this->admin, 'Test');
$target = User::factory()->creator()->create();
$this->actingAs($this->admin);
$this->service->startImpersonation($this->admin, $target);
$filtered = $this->service->getActivityLog(['action' => 'user_status_change']);
foreach ($filtered->items() as $item) {
$this->assertEquals('user_status_change', $item->action);
}
}
public function test_get_activity_log_filters_by_admin_id(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user, 'suspended', $this->admin, 'Test');
$otherAdmin = User::factory()->admin()->create();
$user2 = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user2, 'banned', $otherAdmin, 'Other admin');
$filtered = $this->service->getActivityLog(['admin_id' => $this->admin->id]);
foreach ($filtered->items() as $item) {
$this->assertEquals($this->admin->id, $item->admin_id);
}
}
public function test_get_activity_log_filters_by_date_range(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user, 'suspended', $this->admin, 'Test');
$tomorrow = now()->addDay()->toDateTimeString();
$filtered = $this->service->getActivityLog(['date_from' => $tomorrow]);
$this->assertEquals(0, $filtered->total());
}
public function test_get_activity_log_ordered_newest_first(): void
{
$user1 = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user1, 'suspended', $this->admin, 'First');
$user2 = User::factory()->create(['status' => 'active']);
$this->service->changeUserStatus($user2, 'banned', $this->admin, 'Second');
$log = $this->service->getActivityLog();
$items = collect($log->items());
if ($items->count() >= 2) {
$this->assertTrue($items->first()->created_at >= $items->last()->created_at);
}
}
}
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use App\Modules\Auth\Requests\RegisterCompanyRequest;
use App\Modules\Creators\Models\CreatorProfile;
use App\Shared\Enums\UserStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use Tests\TestCase;
class AuthApiTest extends TestCase
{
use RefreshDatabase;
// ─── Creator Registration Endpoint ──────────────────────────────────
public function test_creator_registration_with_valid_data(): void
{
Event::fake();
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Ahmed',
'last_name' => 'Hassan',
'email' => 'ahmed@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'ahmed_creates',
'phone' => '+201012345678',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(200);
$response->assertJsonStructure(['success', 'data' => ['redirect'], 'message']);
$response->assertJson(['success' => true]);
$this->assertDatabaseHas('users', ['email' => 'ahmed@example.com', 'role' => 'creator']);
$this->assertDatabaseHas('creator_profiles', ['username' => 'ahmed_creates']);
}
public function test_creator_registration_requires_first_name(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'last_name' => 'Hassan',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('first_name');
}
public function test_creator_registration_requires_last_name(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Ahmed',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('last_name');
}
public function test_creator_registration_validates_name_format(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Name123',
'last_name' => 'Valid',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('first_name');
}
public function test_creator_registration_rejects_duplicate_email(): void
{
User::factory()->create(['email' => 'taken@example.com']);
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'taken@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('email');
}
public function test_creator_registration_validates_password_strength(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'weak',
'password_confirmation' => 'weak',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
}
public function test_creator_registration_requires_password_confirmation(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'DifferentPass1',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
}
public function test_creator_registration_validates_username_format(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'invalid username!',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('username');
}
public function test_creator_registration_rejects_reserved_username(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'admin',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('username');
}
public function test_creator_registration_rejects_duplicate_username(): void
{
Event::fake();
$user = User::factory()->creator()->create();
CreatorProfile::factory()->create(['user_id' => $user->id, 'username' => 'taken_name']);
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'taken_name',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('username');
}
public function test_creator_registration_validates_username_min_length(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'ab',
'country' => 'EG',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('username');
}
public function test_creator_registration_validates_country_format(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'egypt',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('country');
}
public function test_creator_registration_validates_phone_format(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'EG',
'phone' => '12345',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('phone');
}
public function test_creator_registration_requires_terms_acceptance(): void
{
$response = $this->postJson('/api/auth/register/creator', [
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'username' => 'testuser',
'country' => 'EG',
'agreed_to_terms' => false,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('agreed_to_terms');
}
// ─── Company Registration Endpoint ──────────────────────────────────
public function test_company_registration_with_valid_data(): void
{
Event::fake();
$response = $this->postJson('/api/auth/register/company', [
'company_name' => 'Acme Corporation',
'contact_person_name' => 'John Doe',
'email' => 'john@acme.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => '11-50',
'agreed_to_terms' => true,
]);
$response->assertStatus(200);
$response->assertJson(['success' => true]);
$this->assertDatabaseHas('users', ['email' => 'john@acme.com', 'role' => 'company']);
$this->assertDatabaseHas('company_profiles', ['company_name' => 'Acme Corporation']);
}
public function test_company_registration_requires_company_name(): void
{
$response = $this->postJson('/api/auth/register/company', [
'contact_person_name' => 'John',
'email' => 'test@company.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => '11-50',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('company_name');
}
public function test_company_registration_validates_industry(): void
{
$response = $this->postJson('/api/auth/register/company', [
'company_name' => 'Test Corp',
'contact_person_name' => 'John',
'email' => 'test@company.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'invalid_industry',
'company_size' => '11-50',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('industry');
}
public function test_company_registration_validates_company_size(): void
{
$response = $this->postJson('/api/auth/register/company', [
'company_name' => 'Test Corp',
'contact_person_name' => 'John',
'email' => 'test@company.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => 'huge',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('company_size');
}
public function test_company_registration_requires_phone(): void
{
$response = $this->postJson('/api/auth/register/company', [
'company_name' => 'Test Corp',
'contact_person_name' => 'John',
'email' => 'test@company.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'country' => 'US',
'industry' => 'technology',
'company_size' => '11-50',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('phone');
}
public function test_company_registration_validates_website_url(): void
{
$response = $this->postJson('/api/auth/register/company', [
'company_name' => 'Test Corp',
'contact_person_name' => 'John',
'email' => 'test@company.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => '11-50',
'website' => 'not-a-url',
'agreed_to_terms' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('website');
}
public function test_company_registration_accepts_valid_website(): void
{
Event::fake();
$response = $this->postJson('/api/auth/register/company', [
'company_name' => 'Web Corp',
'contact_person_name' => 'Jane',
'email' => 'jane@webcorp.com',
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => '51-200',
'website' => 'https://webcorp.com',
'agreed_to_terms' => true,
]);
$response->assertStatus(200);
}
public function test_company_registration_accepts_all_valid_industries(): void
{
Event::fake();
$industries = RegisterCompanyRequest::industries();
$this->assertNotEmpty($industries);
$this->assertContains('technology', $industries);
$this->assertContains('ecommerce', $industries);
$this->assertContains('fashion', $industries);
$this->assertContains('beauty', $industries);
$this->assertContains('gaming', $industries);
$this->assertContains('other', $industries);
}
public function test_company_registration_accepts_all_valid_sizes(): void
{
Event::fake();
$validSizes = ['solo', '2-10', '11-50', '51-200', '201-500', '500+'];
foreach ($validSizes as $size) {
$email = "test_{$size}@company.com";
$response = $this->postJson('/api/auth/register/company', [
'company_name' => "Corp {$size}",
'contact_person_name' => 'Test',
'email' => str_replace(['+', '-'], '_', $email),
'password' => 'SecurePass1',
'password_confirmation' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => $size,
'agreed_to_terms' => true,
]);
$response->assertStatus(200, "Failed for company_size: {$size}");
}
}
// ─── Login Endpoint ─────────────────────────────────────────────────
public function test_login_with_valid_credentials(): void
{
Event::fake();
User::factory()->create([
'email' => 'user@example.com',
'password' => Hash::make('ValidPass1'),
'status' => 'active',
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'user@example.com',
'password' => 'ValidPass1',
]);
$response->assertStatus(200);
$response->assertJson(['success' => true]);
$response->assertJsonStructure(['success', 'data' => ['redirect'], 'message']);
}
public function test_login_with_invalid_credentials(): void
{
User::factory()->create([
'email' => 'user@example.com',
'password' => Hash::make('ValidPass1'),
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'user@example.com',
'password' => 'WrongPassword1',
]);
$response->assertStatus(422);
$response->assertJson(['success' => false]);
}
public function test_login_requires_email(): void
{
$response = $this->postJson('/api/auth/login', [
'password' => 'SomePass1',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('email');
}
public function test_login_requires_password(): void
{
$response = $this->postJson('/api/auth/login', [
'email' => 'user@example.com',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
}
public function test_login_validates_email_format(): void
{
$response = $this->postJson('/api/auth/login', [
'email' => 'not-an-email',
'password' => 'SomePass1',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('email');
}
public function test_login_blocked_for_suspended_user(): void
{
User::factory()->create([
'email' => 'suspended@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::Suspended->value,
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'suspended@example.com',
'password' => 'ValidPass1',
]);
$response->assertStatus(422);
$response->assertJson(['success' => false]);
}
public function test_login_blocked_for_banned_user(): void
{
User::factory()->create([
'email' => 'banned@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::Banned->value,
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'banned@example.com',
'password' => 'ValidPass1',
]);
$response->assertStatus(422);
$response->assertJson(['success' => false]);
}
// ─── Logout Endpoint ────────────────────────────────────────────────
public function test_logout_requires_authentication(): void
{
$response = $this->postJson('/api/auth/logout');
$response->assertStatus(401);
}
public function test_authenticated_user_can_logout(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/auth/logout');
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
// ─── Forgot Password Endpoint ───────────────────────────────────────
public function test_forgot_password_with_valid_email(): void
{
Notification::fake();
User::factory()->create(['email' => 'user@example.com']);
$response = $this->postJson('/api/auth/forgot-password', [
'email' => 'user@example.com',
]);
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
public function test_forgot_password_with_nonexistent_email_still_succeeds(): void
{
$response = $this->postJson('/api/auth/forgot-password', [
'email' => 'nobody@nowhere.com',
]);
$response->assertStatus(200);
$response->assertJson(['success' => true]);
}
public function test_forgot_password_requires_email(): void
{
$response = $this->postJson('/api/auth/forgot-password', []);
$response->assertStatus(422);
$response->assertJsonValidationErrors('email');
}
public function test_forgot_password_validates_email_format(): void
{
$response = $this->postJson('/api/auth/forgot-password', [
'email' => 'not-valid',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('email');
}
// ─── Reset Password Endpoint ────────────────────────────────────────
public function test_reset_password_requires_token(): void
{
$response = $this->postJson('/api/auth/reset-password', [
'email' => 'user@example.com',
'password' => 'NewSecurePass1',
'password_confirmation' => 'NewSecurePass1',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('token');
}
public function test_reset_password_requires_email(): void
{
$response = $this->postJson('/api/auth/reset-password', [
'token' => 'fake-token',
'password' => 'NewSecurePass1',
'password_confirmation' => 'NewSecurePass1',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('email');
}
public function test_reset_password_validates_password_strength(): void
{
$response = $this->postJson('/api/auth/reset-password', [
'token' => 'fake-token',
'email' => 'user@example.com',
'password' => 'weak',
'password_confirmation' => 'weak',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
}
public function test_reset_password_requires_confirmation(): void
{
$response = $this->postJson('/api/auth/reset-password', [
'token' => 'fake-token',
'email' => 'user@example.com',
'password' => 'NewSecurePass1',
'password_confirmation' => 'DifferentPass1',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
}
public function test_reset_password_with_invalid_token_fails(): void
{
User::factory()->create(['email' => 'user@example.com']);
$response = $this->postJson('/api/auth/reset-password', [
'token' => 'completely-invalid-token',
'email' => 'user@example.com',
'password' => 'NewSecurePass1',
'password_confirmation' => 'NewSecurePass1',
]);
$response->assertStatus(422);
}
// ─── Check Username Endpoint ────────────────────────────────────────
public function test_check_username_available(): void
{
$response = $this->postJson('/api/auth/check-username', [
'username' => 'available_name',
]);
$response->assertStatus(200);
$response->assertJson(['success' => true, 'data' => ['available' => true]]);
}
public function test_check_username_too_short(): void
{
$response = $this->postJson('/api/auth/check-username', [
'username' => 'ab',
]);
$response->assertStatus(422);
$response->assertJson(['success' => false]);
}
public function test_check_username_reserved(): void
{
$response = $this->postJson('/api/auth/check-username', [
'username' => 'admin',
]);
$response->assertStatus(422);
$response->assertJson(['success' => false]);
$response->assertJsonStructure(['data' => ['suggestions']]);
}
public function test_check_username_taken(): void
{
$user = User::factory()->creator()->create();
CreatorProfile::factory()->create(['user_id' => $user->id, 'username' => 'taken_name']);
$response = $this->postJson('/api/auth/check-username', [
'username' => 'taken_name',
]);
$response->assertStatus(422);
$response->assertJson(['success' => false]);
$response->assertJsonStructure(['data' => ['suggestions']]);
}
// ─── Email Verification Resend ──────────────────────────────────────
public function test_verification_resend_requires_auth(): void
{
$response = $this->postJson('/api/auth/email/verification-notification');
$response->assertStatus(401);
}
public function test_verification_resend_for_unverified_user(): void
{
Notification::fake();
$user = User::factory()->unverified()->create();
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/auth/email/verification-notification');
$response->assertStatus(200);
}
public function test_verification_resend_for_already_verified_user(): void
{
$user = User::factory()->create(['email_verified_at' => now()]);
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/auth/email/verification-notification');
$response->assertStatus(200);
}
// ─── Web Routes: Guest Middleware ───────────────────────────────────
public function test_login_page_accessible_for_guests(): void
{
$response = $this->get('/login');
$response->assertStatus(200);
}
public function test_register_page_accessible_for_guests(): void
{
$response = $this->get('/register');
$response->assertStatus(200);
}
public function test_register_creator_page_accessible_for_guests(): void
{
$response = $this->get('/register/creator');
$response->assertStatus(200);
}
public function test_register_company_page_accessible_for_guests(): void
{
$response = $this->get('/register/company');
$response->assertStatus(200);
}
public function test_forgot_password_page_accessible_for_guests(): void
{
$response = $this->get('/forgot-password');
$response->assertStatus(200);
}
public function test_authenticated_user_redirected_from_login_page(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/login');
$response->assertRedirect();
}
public function test_authenticated_user_redirected_from_register_page(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/register');
$response->assertRedirect();
}
// ─── Web Routes: Auth Middleware ────────────────────────────────────
public function test_email_verify_page_requires_auth(): void
{
$response = $this->get('/email/verify');
$response->assertRedirect('/login');
}
public function test_logout_via_web_requires_auth(): void
{
$response = $this->post('/logout');
$response->assertRedirect('/login');
}
public function test_web_logout_redirects_to_homepage(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
$response->assertRedirect('/');
}
}
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use App\Modules\Auth\Events\PasswordResetRequested;
use App\Modules\Auth\Events\PasswordWasReset;
use App\Modules\Auth\Events\UserLoggedIn;
use App\Modules\Auth\Events\UserRegistered;
use App\Modules\Auth\Services\AuthService;
use App\Modules\Creators\Models\CreatorProfile;
use App\Shared\Enums\UserRole;
use App\Shared\Enums\UserStatus;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
class AuthServiceTest extends TestCase
{
use RefreshDatabase;
private AuthService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = app(AuthService::class);
}
// ─── Register Creator ───────────────────────────────────────────────
public function test_register_creator_creates_user_with_correct_role(): void
{
Event::fake();
$user = $this->service->registerCreator([
'first_name' => 'Ahmed',
'last_name' => 'Hassan',
'email' => 'ahmed@example.com',
'password' => 'SecurePass1',
'username' => 'ahmed_creator',
'country' => 'EG',
'phone' => '+201012345678',
]);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals(UserRole::Creator->value, $user->role);
$this->assertEquals(UserStatus::Unverified->value, $user->status);
$this->assertEquals('Ahmed Hassan', $user->name);
$this->assertEquals('ahmed@example.com', $user->email);
$this->assertEquals('+201012345678', $user->phone);
}
public function test_register_creator_creates_profile(): void
{
Event::fake();
$user = $this->service->registerCreator([
'first_name' => 'Sara',
'last_name' => 'Ali',
'email' => 'sara@example.com',
'password' => 'SecurePass1',
'username' => 'sara_creator',
'country' => 'SA',
]);
$profile = $user->creatorProfile;
$this->assertNotNull($profile);
$this->assertEquals('Sara', $profile->first_name);
$this->assertEquals('Ali', $profile->last_name);
$this->assertEquals('sara_creator', $profile->username);
$this->assertEquals('SA', $profile->country);
$this->assertEquals(10, $profile->completion_percentage);
}
public function test_register_creator_hashes_password(): void
{
Event::fake();
$user = $this->service->registerCreator([
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'PlainTextPass1',
'username' => 'testuser',
'country' => 'US',
]);
$this->assertNotEquals('PlainTextPass1', $user->password);
$this->assertTrue(Hash::check('PlainTextPass1', $user->password));
}
public function test_register_creator_dispatches_user_registered_event(): void
{
Event::fake();
$user = $this->service->registerCreator([
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'event@example.com',
'password' => 'SecurePass1',
'username' => 'eventuser',
'country' => 'US',
]);
Event::assertDispatched(UserRegistered::class, function ($event) use ($user) {
return $event->user->id === $user->id;
});
}
public function test_register_creator_with_optional_phone_null(): void
{
Event::fake();
$user = $this->service->registerCreator([
'first_name' => 'No',
'last_name' => 'Phone',
'email' => 'nophone@example.com',
'password' => 'SecurePass1',
'username' => 'nophone',
'country' => 'GB',
]);
$this->assertNull($user->phone);
}
public function test_register_creator_is_transactional(): void
{
Event::fake();
$this->assertDatabaseCount('users', 0);
$this->assertDatabaseCount('creator_profiles', 0);
$user = $this->service->registerCreator([
'first_name' => 'Trans',
'last_name' => 'Action',
'email' => 'trans@example.com',
'password' => 'SecurePass1',
'username' => 'transaction',
'country' => 'DE',
]);
$this->assertDatabaseCount('users', 1);
$this->assertDatabaseCount('creator_profiles', 1);
}
// ─── Register Company ───────────────────────────────────────────────
public function test_register_company_creates_user_with_pending_review_status(): void
{
Event::fake();
$user = $this->service->registerCompany([
'company_name' => 'Acme Corp',
'contact_person_name' => 'John Doe',
'email' => 'john@acme.com',
'password' => 'SecurePass1',
'phone' => '+12025551234',
'country' => 'US',
'industry' => 'technology',
'company_size' => '11-50',
]);
$this->assertEquals(UserRole::Company->value, $user->role);
$this->assertEquals(UserStatus::PendingReview->value, $user->status);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('+12025551234', $user->phone);
}
public function test_register_company_creates_profile(): void
{
Event::fake();
$user = $this->service->registerCompany([
'company_name' => 'Tech Inc',
'contact_person_name' => 'Jane Smith',
'email' => 'jane@tech.com',
'password' => 'SecurePass1',
'phone' => '+44207123456',
'country' => 'GB',
'industry' => 'ecommerce',
'website' => 'https://tech.com',
'company_size' => '51-200',
]);
$profile = $user->companyProfile;
$this->assertNotNull($profile);
$this->assertEquals('Tech Inc', $profile->company_name);
$this->assertEquals('Jane Smith', $profile->contact_person_name);
$this->assertEquals('GB', $profile->country);
$this->assertEquals('ecommerce', $profile->industry);
$this->assertEquals('https://tech.com', $profile->website);
$this->assertEquals('51-200', $profile->company_size);
$this->assertEquals('pending_review', $profile->status);
$this->assertEquals(15, $profile->completion_percentage);
}
public function test_register_company_with_null_website(): void
{
Event::fake();
$user = $this->service->registerCompany([
'company_name' => 'No Web Inc',
'contact_person_name' => 'Bob',
'email' => 'bob@noweb.com',
'password' => 'SecurePass1',
'phone' => '+201112345678',
'country' => 'EG',
'industry' => 'food_beverage',
'company_size' => 'solo',
]);
$this->assertNull($user->companyProfile->website);
}
public function test_register_company_dispatches_user_registered_event(): void
{
Event::fake();
$user = $this->service->registerCompany([
'company_name' => 'Event Corp',
'contact_person_name' => 'Eve',
'email' => 'eve@event.com',
'password' => 'SecurePass1',
'phone' => '+496912345678',
'country' => 'DE',
'industry' => 'entertainment',
'company_size' => '2-10',
]);
Event::assertDispatched(UserRegistered::class, function ($event) use ($user) {
return $event->user->id === $user->id;
});
}
// ─── Attempt Login ──────────────────────────────────────────────────
public function test_login_with_valid_credentials_succeeds(): void
{
Event::fake();
$user = User::factory()->create([
'email' => 'valid@example.com',
'password' => Hash::make('MyPassword1'),
'status' => 'active',
]);
$result = $this->service->attemptLogin([
'email' => 'valid@example.com',
'password' => 'MyPassword1',
]);
$this->assertTrue($result['success']);
$this->assertEquals($user->id, $result['user']->id);
$this->assertNotNull($result['redirect']);
}
public function test_login_with_wrong_password_fails(): void
{
User::factory()->create([
'email' => 'user@example.com',
'password' => Hash::make('CorrectPass1'),
]);
$result = $this->service->attemptLogin([
'email' => 'user@example.com',
'password' => 'WrongPass1',
]);
$this->assertFalse($result['success']);
$this->assertEquals('invalid_credentials', $result['reason']);
}
public function test_login_with_nonexistent_email_fails(): void
{
$result = $this->service->attemptLogin([
'email' => 'nobody@example.com',
'password' => 'AnyPass1',
]);
$this->assertFalse($result['success']);
$this->assertEquals('invalid_credentials', $result['reason']);
}
public function test_login_with_suspended_account_fails(): void
{
User::factory()->create([
'email' => 'suspended@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::Suspended->value,
]);
$result = $this->service->attemptLogin([
'email' => 'suspended@example.com',
'password' => 'ValidPass1',
]);
$this->assertFalse($result['success']);
$this->assertEquals('account_suspended', $result['reason']);
}
public function test_login_with_banned_account_fails(): void
{
User::factory()->create([
'email' => 'banned@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::Banned->value,
]);
$result = $this->service->attemptLogin([
'email' => 'banned@example.com',
'password' => 'ValidPass1',
]);
$this->assertFalse($result['success']);
$this->assertEquals('account_banned', $result['reason']);
}
public function test_login_with_deactivated_account_fails(): void
{
User::factory()->create([
'email' => 'deactivated@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::Deactivated->value,
]);
$result = $this->service->attemptLogin([
'email' => 'deactivated@example.com',
'password' => 'ValidPass1',
]);
$this->assertFalse($result['success']);
$this->assertEquals('account_deactivated', $result['reason']);
}
public function test_login_with_unverified_account_succeeds(): void
{
Event::fake();
User::factory()->create([
'email' => 'unverified@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::Unverified->value,
]);
$result = $this->service->attemptLogin([
'email' => 'unverified@example.com',
'password' => 'ValidPass1',
]);
$this->assertTrue($result['success']);
}
public function test_login_with_pending_review_account_succeeds(): void
{
Event::fake();
User::factory()->create([
'email' => 'pending@example.com',
'password' => Hash::make('ValidPass1'),
'status' => UserStatus::PendingReview->value,
]);
$result = $this->service->attemptLogin([
'email' => 'pending@example.com',
'password' => 'ValidPass1',
]);
$this->assertTrue($result['success']);
}
public function test_login_updates_last_login_timestamp(): void
{
Event::fake();
$user = User::factory()->create([
'email' => 'timestamp@example.com',
'password' => Hash::make('ValidPass1'),
'last_login_at' => null,
]);
$this->service->attemptLogin([
'email' => 'timestamp@example.com',
'password' => 'ValidPass1',
]);
$user->refresh();
$this->assertNotNull($user->last_login_at);
}
public function test_login_dispatches_user_logged_in_event(): void
{
Event::fake();
User::factory()->create([
'email' => 'event@example.com',
'password' => Hash::make('ValidPass1'),
]);
$this->service->attemptLogin([
'email' => 'event@example.com',
'password' => 'ValidPass1',
]);
Event::assertDispatched(UserLoggedIn::class);
}
public function test_login_redirect_for_admin(): void
{
Event::fake();
User::factory()->admin()->create([
'email' => 'admin@example.com',
'password' => Hash::make('AdminPass1'),
]);
$result = $this->service->attemptLogin([
'email' => 'admin@example.com',
'password' => 'AdminPass1',
]);
$this->assertEquals('/admin/dashboard', $result['redirect']);
}
public function test_login_redirect_for_company(): void
{
Event::fake();
User::factory()->company()->create([
'email' => 'company@example.com',
'password' => Hash::make('CompanyPass1'),
]);
$result = $this->service->attemptLogin([
'email' => 'company@example.com',
'password' => 'CompanyPass1',
]);
$this->assertEquals('/company/dashboard', $result['redirect']);
}
public function test_login_redirect_for_creator(): void
{
Event::fake();
User::factory()->creator()->create([
'email' => 'creator@example.com',
'password' => Hash::make('CreatorPass1'),
]);
$result = $this->service->attemptLogin([
'email' => 'creator@example.com',
'password' => 'CreatorPass1',
]);
$this->assertEquals('/creator/dashboard', $result['redirect']);
}
// ─── Verify Email ───────────────────────────────────────────────────
public function test_verify_email_marks_email_verified(): void
{
Event::fake();
$user = User::factory()->unverified()->create([
'status' => UserStatus::Unverified->value,
'role' => UserRole::Creator->value,
]);
$this->assertNull($user->email_verified_at);
$this->service->verifyEmail($user);
$user->refresh();
$this->assertNotNull($user->email_verified_at);
}
public function test_verify_email_activates_creator_account(): void
{
Event::fake();
$user = User::factory()->unverified()->creator()->create([
'status' => UserStatus::Unverified->value,
]);
$this->service->verifyEmail($user);
$user->refresh();
$this->assertEquals(UserStatus::Active->value, $user->status);
}
public function test_verify_email_does_not_activate_company_account(): void
{
Event::fake();
$user = User::factory()->unverified()->company()->create([
'status' => UserStatus::Unverified->value,
]);
$this->service->verifyEmail($user);
$user->refresh();
$this->assertEquals(UserStatus::Unverified->value, $user->status);
}
public function test_verify_email_dispatches_verified_event(): void
{
Event::fake();
$user = User::factory()->unverified()->create([
'status' => UserStatus::Unverified->value,
]);
$this->service->verifyEmail($user);
Event::assertDispatched(Verified::class);
}
public function test_verify_email_already_verified_returns_true(): void
{
$user = User::factory()->create([
'email_verified_at' => now(),
]);
$result = $this->service->verifyEmail($user);
$this->assertTrue($result);
}
// ─── Password Reset ─────────────────────────────────────────────────
public function test_send_password_reset_link_for_existing_user(): void
{
Event::fake();
$user = User::factory()->create([
'email' => 'reset@example.com',
]);
$status = $this->service->sendPasswordResetLink('reset@example.com');
$this->assertEquals('passwords.sent', $status);
}
public function test_send_password_reset_link_for_nonexistent_email_returns_sent(): void
{
$status = $this->service->sendPasswordResetLink('nobody@nowhere.com');
$this->assertEquals('passwords.sent', $status);
}
public function test_send_password_reset_link_for_banned_user_returns_sent_silently(): void
{
User::factory()->banned()->create([
'email' => 'banned@example.com',
]);
$status = $this->service->sendPasswordResetLink('banned@example.com');
$this->assertEquals('passwords.sent', $status);
}
// ─── Username Validation ────────────────────────────────────────────
public function test_reserved_username_is_detected(): void
{
$this->assertTrue($this->service->isReservedUsername('admin'));
$this->assertTrue($this->service->isReservedUsername('ADMIN'));
$this->assertTrue($this->service->isReservedUsername('Admin'));
$this->assertTrue($this->service->isReservedUsername('support'));
$this->assertTrue($this->service->isReservedUsername('moderator'));
$this->assertTrue($this->service->isReservedUsername('root'));
$this->assertTrue($this->service->isReservedUsername('system'));
}
public function test_non_reserved_username_is_allowed(): void
{
$this->assertFalse($this->service->isReservedUsername('johndoe'));
$this->assertFalse($this->service->isReservedUsername('creative_designer'));
$this->assertFalse($this->service->isReservedUsername('video_maker'));
$this->assertFalse($this->service->isReservedUsername('brandname'));
}
public function test_username_exists_returns_true_for_taken_username(): void
{
Event::fake();
$user = User::factory()->creator()->create();
CreatorProfile::factory()->create([
'user_id' => $user->id,
'username' => 'taken_name',
]);
$this->assertTrue($this->service->usernameExists('taken_name'));
}
public function test_username_exists_returns_false_for_available_username(): void
{
$this->assertFalse($this->service->usernameExists('available_name'));
}
public function test_suggest_usernames_returns_array_of_suggestions(): void
{
$suggestions = $this->service->suggestUsernames('testuser');
$this->assertIsArray($suggestions);
$this->assertLessThanOrEqual(3, count($suggestions));
foreach ($suggestions as $suggestion) {
$this->assertStringStartsWith('testuser_', $suggestion);
}
}
public function test_suggest_usernames_strips_special_characters(): void
{
$suggestions = $this->service->suggestUsernames('user@#$name');
foreach ($suggestions as $suggestion) {
$this->assertStringStartsWith('username_', $suggestion);
}
}
// ─── UserStatus Enum ────────────────────────────────────────────────
public function test_active_status_can_login(): void
{
$this->assertTrue(UserStatus::Active->canLogin());
}
public function test_unverified_status_can_login(): void
{
$this->assertTrue(UserStatus::Unverified->canLogin());
}
public function test_pending_review_status_can_login(): void
{
$this->assertTrue(UserStatus::PendingReview->canLogin());
}
public function test_suspended_status_cannot_login(): void
{
$this->assertFalse(UserStatus::Suspended->canLogin());
}
public function test_banned_status_cannot_login(): void
{
$this->assertFalse(UserStatus::Banned->canLogin());
}
public function test_deactivated_status_cannot_login(): void
{
$this->assertFalse(UserStatus::Deactivated->canLogin());
}
public function test_only_active_status_can_use_platform(): void
{
$this->assertTrue(UserStatus::Active->canUsePlatform());
$this->assertFalse(UserStatus::Unverified->canUsePlatform());
$this->assertFalse(UserStatus::PendingReview->canUsePlatform());
$this->assertFalse(UserStatus::Suspended->canUsePlatform());
$this->assertFalse(UserStatus::Banned->canUsePlatform());
$this->assertFalse(UserStatus::Deactivated->canUsePlatform());
}
public function test_status_transitions_from_active(): void
{
$this->assertTrue(UserStatus::canTransition('active', 'suspended'));
$this->assertTrue(UserStatus::canTransition('active', 'banned'));
$this->assertTrue(UserStatus::canTransition('active', 'deactivated'));
$this->assertFalse(UserStatus::canTransition('active', 'unverified'));
$this->assertFalse(UserStatus::canTransition('active', 'pending_review'));
}
public function test_status_transitions_from_unverified(): void
{
$this->assertTrue(UserStatus::canTransition('unverified', 'active'));
$this->assertTrue(UserStatus::canTransition('unverified', 'pending_review'));
$this->assertFalse(UserStatus::canTransition('unverified', 'suspended'));
$this->assertFalse(UserStatus::canTransition('unverified', 'banned'));
}
public function test_status_transitions_from_suspended(): void
{
$this->assertTrue(UserStatus::canTransition('suspended', 'active'));
$this->assertFalse(UserStatus::canTransition('suspended', 'banned'));
$this->assertFalse(UserStatus::canTransition('suspended', 'deactivated'));
}
public function test_banned_status_cannot_transition(): void
{
$this->assertFalse(UserStatus::canTransition('banned', 'active'));
$this->assertFalse(UserStatus::canTransition('banned', 'suspended'));
}
// ─── User Model Helpers ─────────────────────────────────────────────
public function test_user_is_admin_helper(): void
{
$user = User::factory()->admin()->create();
$this->assertTrue($user->isAdmin());
$this->assertFalse($user->isCreator());
$this->assertFalse($user->isCompany());
}
public function test_user_is_creator_helper(): void
{
$user = User::factory()->creator()->create();
$this->assertTrue($user->isCreator());
$this->assertFalse($user->isAdmin());
$this->assertFalse($user->isCompany());
}
public function test_user_is_company_helper(): void
{
$user = User::factory()->company()->create();
$this->assertTrue($user->isCompany());
$this->assertFalse($user->isAdmin());
$this->assertFalse($user->isCreator());
}
public function test_user_is_active_helper(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->assertTrue($user->isActive());
$user->update(['status' => 'suspended']);
$this->assertFalse($user->isActive());
}
public function test_user_is_suspended_helper(): void
{
$user = User::factory()->suspended()->create();
$this->assertTrue($user->isSuspended());
}
public function test_user_is_banned_helper(): void
{
$user = User::factory()->banned()->create();
$this->assertTrue($user->isBanned());
}
public function test_user_route_key_is_uuid(): void
{
$user = User::factory()->create();
$this->assertEquals('uuid', $user->getRouteKeyName());
$this->assertNotNull($user->uuid);
}
// ─── User Model Scopes ──────────────────────────────────────────────
public function test_active_scope_filters_correctly(): void
{
User::factory()->create(['status' => 'active']);
User::factory()->create(['status' => 'suspended']);
User::factory()->create(['status' => 'banned']);
$activeUsers = User::active()->get();
$this->assertCount(1, $activeUsers);
$this->assertEquals('active', $activeUsers->first()->status);
}
public function test_by_role_scope_with_string(): void
{
User::factory()->admin()->create();
User::factory()->creator()->create();
User::factory()->company()->create();
$this->assertCount(1, User::byRole('admin')->get());
$this->assertCount(1, User::byRole('creator')->get());
$this->assertCount(1, User::byRole('company')->get());
}
public function test_by_role_scope_with_enum(): void
{
User::factory()->creator()->count(3)->create();
User::factory()->company()->count(2)->create();
$this->assertCount(3, User::byRole(UserRole::Creator)->get());
$this->assertCount(2, User::byRole(UserRole::Company)->get());
}
public function test_verified_scope_filters_correctly(): void
{
User::factory()->create(['email_verified_at' => now()]);
User::factory()->unverified()->create();
$verified = User::verified()->get();
$this->assertCount(1, $verified);
$this->assertNotNull($verified->first()->email_verified_at);
}
public function test_search_scope_finds_by_name(): void
{
User::factory()->create(['name' => 'Unique Test Name']);
User::factory()->create(['name' => 'Other Person']);
$results = User::search('Unique')->get();
$this->assertCount(1, $results);
$this->assertEquals('Unique Test Name', $results->first()->name);
}
public function test_search_scope_finds_by_email(): void
{
User::factory()->create(['email' => 'searchable@domain.com']);
User::factory()->create(['email' => 'other@elsewhere.net']);
$results = User::search('searchable@domain')->get();
$this->assertCount(1, $results);
}
public function test_search_scope_is_case_insensitive(): void
{
User::factory()->create(['name' => 'Ahmed Mohamed']);
$results = User::search('ahmed')->get();
$this->assertCount(1, $results);
}
// ─── User Soft Deletes ──────────────────────────────────────────────
public function test_user_can_be_soft_deleted(): void
{
$user = User::factory()->create();
$userId = $user->id;
$user->delete();
$this->assertSoftDeleted('users', ['id' => $userId]);
$this->assertNull(User::find($userId));
$this->assertNotNull(User::withTrashed()->find($userId));
}
}
<?php
namespace Tests\Feature\Companies;
use App\Models\User;
use App\Modules\Companies\Events\CompanyApproved;
use App\Modules\Companies\Events\CompanyRejected;
use App\Modules\Companies\Models\CompanyProfile;
use App\Modules\Companies\Services\CompanyProfileService;
use App\Shared\Enums\UserStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class CompanyServiceTest extends TestCase
{
use RefreshDatabase;
private CompanyProfileService $service;
private User $companyUser;
private CompanyProfile $profile;
private User $admin;
protected function setUp(): void
{
parent::setUp();
$this->service = app(CompanyProfileService::class);
$this->admin = User::factory()->admin()->create();
$this->companyUser = User::factory()->company()->create();
$this->profile = CompanyProfile::factory()->pending()->create([
'user_id' => $this->companyUser->id,
]);
}
// ─── Create ─────────────────────────────────────────────────────────
public function test_create_returns_company_profile(): void
{
$user = User::factory()->company()->create();
$profile = $this->service->create($user, [
'company_name' => 'New Corp',
'contact_person_name' => 'John Doe',
'country' => 'US',
'industry' => 'technology',
'company_size' => '11-50',
]);
$this->assertInstanceOf(CompanyProfile::class, $profile);
$this->assertEquals('New Corp', $profile->company_name);
$this->assertEquals('pending_review', $profile->status);
$this->assertEquals(15, $profile->completion_percentage);
}
public function test_create_uses_user_email_as_default_contact_email(): void
{
$user = User::factory()->company()->create(['email' => 'company@test.com']);
$profile = $this->service->create($user, [
'company_name' => 'Email Corp',
'contact_person_name' => 'Jane',
'country' => 'GB',
'industry' => 'fashion',
'company_size' => '2-10',
]);
$this->assertEquals('company@test.com', $profile->contact_email);
}
public function test_create_with_explicit_contact_email(): void
{
$user = User::factory()->company()->create();
$profile = $this->service->create($user, [
'company_name' => 'Contact Corp',
'contact_person_name' => 'Bob',
'contact_email' => 'specific@contact.com',
'country' => 'DE',
'industry' => 'ecommerce',
'company_size' => '51-200',
]);
$this->assertEquals('specific@contact.com', $profile->contact_email);
}
public function test_create_generates_unique_slug(): void
{
$user = User::factory()->company()->create();
$profile = $this->service->create($user, [
'company_name' => 'Test Company',
'contact_person_name' => 'Alice',
'country' => 'US',
'industry' => 'technology',
'company_size' => 'solo',
]);
$this->assertNotEmpty($profile->slug);
$this->assertEquals('test-company', $profile->slug);
}
public function test_create_handles_duplicate_slug(): void
{
$user1 = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $user1->id, 'slug' => 'tech-corp']);
$user2 = User::factory()->company()->create();
$profile = $this->service->create($user2, [
'company_name' => 'Tech Corp',
'contact_person_name' => 'Bob',
'country' => 'US',
'industry' => 'technology',
'company_size' => '2-10',
]);
$this->assertNotEquals('tech-corp', $profile->slug);
$this->assertStringStartsWith('tech-corp-', $profile->slug);
}
// ─── Update ─────────────────────────────────────────────────────────
public function test_update_changes_allowed_fields(): void
{
$result = $this->service->update($this->profile, [
'company_name' => 'Updated Corp',
'description' => 'A new description that is definitely long enough to count for completion scoring purposes.',
'website' => 'https://updated.com',
'country' => 'GB',
]);
$this->assertEquals('Updated Corp', $result->company_name);
$this->assertStringContainsString('A new description', $result->description);
$this->assertEquals('https://updated.com', $result->website);
$this->assertEquals('GB', $result->country);
}
public function test_update_normalizes_social_links(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'instagram' => 'ourcompany',
'tiktok' => '@companyhandle',
'linkedin' => 'ourcompany-llc',
],
]);
$links = $result->social_links;
$this->assertStringContainsString('instagram.com/ourcompany', $links['instagram']);
$this->assertStringContainsString('tiktok.com/@companyhandle', $links['tiktok']);
$this->assertStringContainsString('linkedin.com/company/ourcompany-llc', $links['linkedin']);
}
public function test_update_normalizes_instagram_with_at_sign(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'instagram' => '@myhandle',
],
]);
$this->assertEquals('https://www.instagram.com/myhandle', $result->social_links['instagram']);
}
public function test_update_preserves_full_urls_in_social_links(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'instagram' => 'https://www.instagram.com/existing',
'youtube' => 'https://www.youtube.com/@channel',
],
]);
$this->assertEquals('https://www.instagram.com/existing', $result->social_links['instagram']);
$this->assertEquals('https://www.youtube.com/@channel', $result->social_links['youtube']);
}
public function test_update_normalizes_twitter_handle(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'x_twitter' => 'handle',
],
]);
$this->assertEquals('https://x.com/handle', $result->social_links['x_twitter']);
}
public function test_update_normalizes_facebook_handle(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'facebook' => 'ourpage',
],
]);
$this->assertEquals('https://www.facebook.com/ourpage', $result->social_links['facebook']);
}
public function test_update_normalizes_youtube_handle(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'youtube' => '@ourchannel',
],
]);
$this->assertEquals('https://www.youtube.com/@ourchannel', $result->social_links['youtube']);
}
public function test_update_skips_empty_social_links(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'instagram' => 'handle',
'tiktok' => '',
'linkedin' => null,
],
]);
$this->assertArrayHasKey('instagram', $result->social_links);
$this->assertArrayNotHasKey('tiktok', $result->social_links);
$this->assertArrayNotHasKey('linkedin', $result->social_links);
}
public function test_update_recalculates_completion(): void
{
$this->profile->update([
'logo_path' => null,
'description' => null,
'website' => null,
'completion_percentage' => 15,
]);
$this->service->update($this->profile, [
'website' => 'https://example.com',
]);
$this->profile->refresh();
$this->assertGreaterThan(15, $this->profile->completion_percentage);
}
public function test_update_ignores_non_updatable_fields(): void
{
$originalStatus = $this->profile->status;
$this->service->update($this->profile, [
'status' => 'approved',
'reputation_score' => 100,
]);
$this->profile->refresh();
$this->assertEquals($originalStatus, $this->profile->status);
}
// ─── Approve ────────────────────────────────────────────────────────
public function test_approve_sets_approved_status(): void
{
Event::fake();
$this->service->approve($this->profile, $this->admin);
$this->profile->refresh();
$this->assertEquals('approved', $this->profile->status);
$this->assertNotNull($this->profile->approved_at);
$this->assertEquals($this->admin->id, $this->profile->approved_by);
}
public function test_approve_activates_user_account(): void
{
Event::fake();
$this->companyUser->update(['status' => UserStatus::PendingReview->value]);
$this->service->approve($this->profile, $this->admin);
$this->companyUser->refresh();
$this->assertEquals(UserStatus::Active->value, $this->companyUser->status);
}
public function test_approve_clears_rejection_data(): void
{
Event::fake();
$this->profile->update([
'rejected_at' => now()->subDay(),
'rejection_reason' => 'Previous rejection.',
]);
$this->service->approve($this->profile, $this->admin);
$this->profile->refresh();
$this->assertNull($this->profile->rejected_at);
$this->assertNull($this->profile->rejection_reason);
}
public function test_approve_dispatches_company_approved_event(): void
{
Event::fake();
$this->service->approve($this->profile, $this->admin);
Event::assertDispatched(CompanyApproved::class, function ($event) {
return $event->company->id === $this->profile->id
&& $event->admin->id === $this->admin->id;
});
}
// ─── Reject ─────────────────────────────────────────────────────────
public function test_reject_sets_rejected_status(): void
{
Event::fake();
$this->service->reject($this->profile, $this->admin, 'Incomplete documentation.');
$this->profile->refresh();
$this->assertEquals('rejected', $this->profile->status);
$this->assertNotNull($this->profile->rejected_at);
$this->assertEquals('Incomplete documentation.', $this->profile->rejection_reason);
}
public function test_reject_dispatches_company_rejected_event(): void
{
Event::fake();
$this->service->reject($this->profile, $this->admin, 'Invalid business.');
Event::assertDispatched(CompanyRejected::class, function ($event) {
return $event->company->id === $this->profile->id
&& $event->reason === 'Invalid business.';
});
}
// ─── Calculate Completion ───────────────────────────────────────────
public function test_completion_with_minimal_data_is_15(): void
{
$profile = CompanyProfile::factory()->create([
'user_id' => User::factory()->company()->create()->id,
'logo_path' => null,
'description' => null,
'website' => null,
'social_links' => null,
'short_description' => null,
'brand_colors' => null,
'tone_of_voice' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(30, $score);
}
public function test_completion_with_logo_adds_20(): void
{
$this->profile->update([
'logo_path' => 'companies/test/logo/medium/logo.webp',
'description' => null,
'website' => null,
'social_links' => null,
'short_description' => null,
'brand_colors' => null,
'tone_of_voice' => null,
]);
$score = $this->service->calculateCompletion($this->profile);
$this->assertGreaterThanOrEqual(35, $score);
}
public function test_completion_with_description_over_50_chars_adds_20(): void
{
$this->profile->update([
'description' => str_repeat('A', 51),
'logo_path' => null,
'website' => null,
'social_links' => null,
'short_description' => null,
'brand_colors' => null,
'tone_of_voice' => null,
]);
$score = $this->service->calculateCompletion($this->profile);
$this->assertGreaterThanOrEqual(35, $score);
}
public function test_completion_short_description_doesnt_count_for_description(): void
{
$this->profile->update([
'description' => 'Too short',
'logo_path' => null,
'website' => null,
'social_links' => null,
'short_description' => null,
'brand_colors' => null,
'tone_of_voice' => null,
]);
$score = $this->service->calculateCompletion($this->profile);
$this->assertLessThan(50, $score);
}
public function test_completion_website_adds_10(): void
{
$this->profile->update([
'website' => 'https://test.com',
'description' => null,
'logo_path' => null,
'social_links' => null,
'short_description' => null,
'brand_colors' => null,
'tone_of_voice' => null,
]);
$withWebsite = $this->service->calculateCompletion($this->profile);
$this->profile->update(['website' => null]);
$withoutWebsite = $this->service->calculateCompletion($this->profile);
$this->assertEquals(10, $withWebsite - $withoutWebsite);
}
public function test_completion_social_links_adds_10(): void
{
$this->profile->update([
'social_links' => ['instagram' => 'https://instagram.com/test'],
'description' => null,
'logo_path' => null,
'website' => null,
'short_description' => null,
'brand_colors' => null,
'tone_of_voice' => null,
]);
$withLinks = $this->service->calculateCompletion($this->profile);
$this->profile->update(['social_links' => null]);
$withoutLinks = $this->service->calculateCompletion($this->profile);
$this->assertEquals(10, $withLinks - $withoutLinks);
}
public function test_completion_capped_at_100(): void
{
$this->profile->update([
'logo_path' => 'logo.webp',
'description' => str_repeat('X', 100),
'website' => 'https://complete.com',
'social_links' => ['instagram' => 'https://instagram.com/test'],
'short_description' => 'Short desc here.',
'brand_colors' => ['#FF0000', '#00FF00'],
'tone_of_voice' => 'professional',
]);
$score = $this->service->calculateCompletion($this->profile);
$this->assertLessThanOrEqual(100, $score);
}
// ─── Model Helpers ──────────────────────────────────────────────────
public function test_is_approved_helper(): void
{
$this->profile->update(['status' => 'approved']);
$this->assertTrue($this->profile->isApproved());
$this->assertFalse($this->profile->isPending());
$this->assertFalse($this->profile->isRejected());
}
public function test_is_pending_helper(): void
{
$this->assertTrue($this->profile->isPending());
$this->assertFalse($this->profile->isApproved());
}
public function test_is_rejected_helper(): void
{
$this->profile->update(['status' => 'rejected']);
$this->assertTrue($this->profile->isRejected());
$this->assertFalse($this->profile->isApproved());
}
// ─── Model Scopes ───────────────────────────────────────────────────
public function test_approved_scope(): void
{
$user1 = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $user1->id, 'status' => 'approved']);
$user2 = User::factory()->company()->create();
CompanyProfile::factory()->pending()->create(['user_id' => $user2->id]);
$approved = CompanyProfile::approved()->get();
$this->assertTrue($approved->every(fn ($p) => $p->status === 'approved'));
}
public function test_pending_scope(): void
{
$pending = CompanyProfile::pending()->get();
$this->assertTrue($pending->every(fn ($p) => $p->status === 'pending_review'));
}
public function test_by_industry_scope(): void
{
$user = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $user->id, 'industry' => 'gaming']);
$gaming = CompanyProfile::byIndustry('gaming')->get();
$this->assertTrue($gaming->every(fn ($p) => $p->industry === 'gaming'));
}
public function test_by_country_scope(): void
{
$user = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $user->id, 'country' => 'JP']);
$result = CompanyProfile::byCountry('JP')->get();
$this->assertTrue($result->every(fn ($p) => $p->country === 'JP'));
}
public function test_search_scope_finds_by_company_name(): void
{
$user = User::factory()->company()->create();
CompanyProfile::factory()->create(['user_id' => $user->id, 'company_name' => 'Unique Findable Corp']);
$results = CompanyProfile::search('Unique Findable')->get();
$this->assertGreaterThanOrEqual(1, $results->count());
$this->assertTrue($results->contains(fn ($p) => $p->company_name === 'Unique Findable Corp'));
}
// ─── Policy Tests ───────────────────────────────────────────────────
public function test_anyone_can_view_approved_company(): void
{
$this->profile->update(['status' => 'approved']);
$this->companyUser->update(['status' => 'active']);
$visitor = User::factory()->creator()->create();
$this->assertTrue($visitor->can('view', $this->profile));
}
public function test_pending_company_only_visible_to_owner_and_admin(): void
{
$this->assertTrue($this->companyUser->can('view', $this->profile));
$this->assertTrue($this->admin->can('view', $this->profile));
$outsider = User::factory()->creator()->create();
$this->assertFalse($outsider->can('view', $this->profile));
}
public function test_suspended_user_company_only_visible_to_admin(): void
{
$this->profile->update(['status' => 'approved']);
$this->companyUser->update(['status' => 'suspended']);
$outsider = User::factory()->creator()->create();
$this->assertFalse($outsider->can('view', $this->profile));
$this->assertTrue($this->admin->can('view', $this->profile));
}
public function test_only_owner_can_update_profile(): void
{
$this->assertTrue($this->companyUser->can('update', $this->profile));
$otherUser = User::factory()->company()->create();
$this->assertFalse($otherUser->can('update', $this->profile));
$this->assertFalse($this->admin->can('update', $this->profile));
}
public function test_only_admin_can_approve_pending(): void
{
$this->assertTrue($this->admin->can('approve', $this->profile));
$this->assertFalse($this->companyUser->can('approve', $this->profile));
}
public function test_admin_cannot_approve_already_approved(): void
{
$this->profile->update(['status' => 'approved']);
$this->assertFalse($this->admin->can('approve', $this->profile));
}
public function test_only_admin_can_reject_pending(): void
{
$this->assertTrue($this->admin->can('reject', $this->profile));
$this->assertFalse($this->companyUser->can('reject', $this->profile));
}
}
<?php
namespace Tests\Feature\Creators;
use App\Models\User;
use App\Modules\Creators\Exceptions\UsernameChangeThrottledException;
use App\Modules\Creators\Models\CreatorProfile;
use App\Modules\Creators\Services\CreatorProfileService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CreatorServiceTest extends TestCase
{
use RefreshDatabase;
private CreatorProfileService $service;
private User $creatorUser;
private CreatorProfile $profile;
protected function setUp(): void
{
parent::setUp();
$this->service = app(CreatorProfileService::class);
$this->creatorUser = User::factory()->creator()->create();
$this->profile = CreatorProfile::factory()->create([
'user_id' => $this->creatorUser->id,
'username' => 'test_creator',
]);
}
// ─── Create ─────────────────────────────────────────────────────────
public function test_create_returns_creator_profile(): void
{
$user = User::factory()->creator()->create();
$profile = $this->service->create($user, [
'first_name' => 'Ahmed',
'last_name' => 'Hassan',
'username' => 'ahmed_creates',
'country' => 'EG',
]);
$this->assertInstanceOf(CreatorProfile::class, $profile);
$this->assertEquals('Ahmed', $profile->first_name);
$this->assertEquals('Hassan', $profile->last_name);
$this->assertEquals('ahmed_creates', $profile->username);
$this->assertEquals('EG', $profile->country);
}
public function test_create_with_all_optional_fields(): void
{
$user = User::factory()->creator()->create();
$profile = $this->service->create($user, [
'first_name' => 'Sara',
'last_name' => 'Ali',
'username' => 'sara_ali',
'country' => 'SA',
'display_name' => 'Sara A.',
'bio' => 'I make awesome content about tech and gaming.',
'languages' => ['ar', 'en'],
'content_niches' => ['technology', 'gaming'],
'skills' => ['video_creation', 'voiceover'],
]);
$this->assertEquals('Sara A.', $profile->display_name);
$this->assertStringContainsString('awesome content', $profile->bio);
$this->assertEquals(['ar', 'en'], $profile->languages);
$this->assertEquals(['technology', 'gaming'], $profile->content_niches);
$this->assertEquals(['video_creation', 'voiceover'], $profile->skills);
}
public function test_create_calculates_completion(): void
{
$user = User::factory()->creator()->create();
$profile = $this->service->create($user, [
'first_name' => 'Test',
'last_name' => 'User',
'username' => 'testcreator',
'country' => 'US',
'languages' => ['en'],
'content_niches' => ['technology'],
'skills' => ['video_creation'],
]);
$this->assertGreaterThan(0, $profile->completion_percentage);
}
// ─── Update ─────────────────────────────────────────────────────────
public function test_update_changes_basic_fields(): void
{
$result = $this->service->update($this->profile, [
'bio' => 'Updated bio that is long enough to be meaningful.',
'city' => 'Cairo',
'experience_level' => 'advanced',
]);
$this->assertStringContainsString('Updated bio', $result->bio);
$this->assertEquals('Cairo', $result->city);
$this->assertEquals('advanced', $result->experience_level);
}
public function test_update_changes_array_fields(): void
{
$result = $this->service->update($this->profile, [
'languages' => ['en', 'ar', 'fr'],
'content_niches' => ['technology', 'gaming', 'education'],
'skills' => ['video_creation', 'script_writing', 'motion_graphics'],
'equipment' => ['camera_4k', 'ring_light', 'external_mic'],
]);
$this->assertEquals(['en', 'ar', 'fr'], $result->languages);
$this->assertEquals(['technology', 'gaming', 'education'], $result->content_niches);
$this->assertEquals(['video_creation', 'script_writing', 'motion_graphics'], $result->skills);
$this->assertEquals(['camera_4k', 'ring_light', 'external_mic'], $result->equipment);
}
public function test_update_changes_rate_fields(): void
{
$result = $this->service->update($this->profile, [
'hourly_rate' => 50.00,
'project_min_budget' => 500.00,
]);
$this->assertEquals('50.00', $result->hourly_rate);
$this->assertEquals('500.00', $result->project_min_budget);
}
public function test_update_changes_availability(): void
{
$result = $this->service->update($this->profile, [
'availability_status' => 'busy',
'weekly_capacity_hours' => 20,
'preferred_project_length' => 'medium',
]);
$this->assertEquals('busy', $result->availability_status);
$this->assertEquals(20, $result->weekly_capacity_hours);
$this->assertEquals('medium', $result->preferred_project_length);
}
public function test_update_normalizes_social_links(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'tiktok' => '@mychannel',
'instagram' => 'myinsta',
'youtube' => 'mychannel',
'x_twitter' => '@myhandle',
'facebook' => 'mypage',
'linkedin' => 'myprofile',
],
]);
$links = $result->social_links;
$this->assertEquals('https://www.tiktok.com/@mychannel', $links['tiktok']);
$this->assertEquals('https://www.instagram.com/myinsta', $links['instagram']);
$this->assertEquals('https://www.youtube.com/@mychannel', $links['youtube']);
$this->assertEquals('https://x.com/myhandle', $links['x_twitter']);
$this->assertEquals('https://www.facebook.com/mypage', $links['facebook']);
$this->assertEquals('https://www.linkedin.com/in/myprofile', $links['linkedin']);
}
public function test_update_preserves_existing_full_urls(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'tiktok' => 'https://www.tiktok.com/@existing',
'instagram' => 'https://www.instagram.com/existing',
],
]);
$links = $result->social_links;
$this->assertEquals('https://www.tiktok.com/@existing', $links['tiktok']);
$this->assertEquals('https://www.instagram.com/existing', $links['instagram']);
}
public function test_update_filters_empty_social_links(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'tiktok' => '@mychannel',
'instagram' => '',
'youtube' => null,
],
]);
$this->assertArrayHasKey('tiktok', $result->social_links);
$this->assertArrayNotHasKey('instagram', $result->social_links);
$this->assertArrayNotHasKey('youtube', $result->social_links);
}
public function test_update_normalizes_snapchat_strips_special_chars(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'snapchat' => 'my@snap!user',
],
]);
$this->assertEquals('mysnapuser', $result->social_links['snapchat']);
}
public function test_update_normalizes_website_adds_https(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'website' => 'mysite.com',
],
]);
$this->assertEquals('https://mysite.com', $result->social_links['website']);
}
public function test_update_normalizes_website_keeps_existing_protocol(): void
{
$result = $this->service->update($this->profile, [
'social_links' => [
'website' => 'http://already-has-protocol.com',
],
]);
$this->assertEquals('http://already-has-protocol.com', $result->social_links['website']);
}
public function test_update_recalculates_completion(): void
{
$this->profile->update(['completion_percentage' => 10]);
$this->service->update($this->profile, [
'languages' => ['en', 'ar'],
'content_niches' => ['gaming'],
'skills' => ['video_creation'],
]);
$this->profile->refresh();
$this->assertGreaterThan(10, $this->profile->completion_percentage);
}
// ─── Update Username ────────────────────────────────────────────────
public function test_update_username_succeeds_when_allowed(): void
{
$this->profile->update(['username_changed_at' => null]);
$result = $this->service->updateUsername($this->profile, 'new_username');
$this->assertEquals('new_username', $result->username);
$this->assertNotNull($result->username_changed_at);
}
public function test_update_username_succeeds_after_30_days(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(31)]);
$result = $this->service->updateUsername($this->profile, 'another_name');
$this->assertEquals('another_name', $result->username);
}
public function test_update_username_throws_within_30_days(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(5)]);
$this->expectException(UsernameChangeThrottledException::class);
$this->service->updateUsername($this->profile, 'too_soon');
}
public function test_update_username_throws_at_exactly_29_days(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(29)]);
$this->expectException(UsernameChangeThrottledException::class);
$this->service->updateUsername($this->profile, 'almost_there');
}
// ─── Calculate Completion ───────────────────────────────────────────
public function test_completion_zero_for_empty_profile(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'last_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => null,
'skills' => null,
'social_links' => null,
'equipment' => null,
'availability_status' => null,
'weekly_capacity_hours' => null,
'preferred_project_length' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(0, $score);
}
public function test_completion_basic_info_requires_all_four(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => 'Test',
'last_name' => 'User',
'bio' => 'A bio that is certainly long enough to count.',
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => null,
'skills' => null,
'social_links' => null,
'equipment' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertLessThan(20, $score);
}
public function test_completion_country_and_language_adds_10(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => 'US',
'languages' => ['en'],
'content_niches' => null,
'skills' => null,
'social_links' => null,
'equipment' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(10, $score);
}
public function test_completion_niches_adds_10(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => ['technology'],
'skills' => null,
'social_links' => null,
'equipment' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(10, $score);
}
public function test_completion_skills_adds_10(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => null,
'skills' => ['video_creation'],
'social_links' => null,
'equipment' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(10, $score);
}
public function test_completion_social_links_adds_10(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => null,
'skills' => null,
'social_links' => ['instagram' => 'https://instagram.com/test'],
'equipment' => null,
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(10, $score);
}
public function test_completion_equipment_adds_10(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => null,
'skills' => null,
'social_links' => null,
'equipment' => ['camera_4k', 'ring_light'],
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(10, $score);
}
public function test_completion_equipment_none_value_not_counted(): void
{
$user = User::factory()->creator()->create();
$profile = CreatorProfile::factory()->create([
'user_id' => $user->id,
'first_name' => null,
'bio' => null,
'profile_picture_path' => null,
'country' => null,
'languages' => null,
'content_niches' => null,
'skills' => null,
'social_links' => null,
'equipment' => ['none'],
]);
$score = $this->service->calculateCompletion($profile);
$this->assertEquals(0, $score);
}
public function test_completion_capped_at_100(): void
{
$score = $this->service->calculateCompletion($this->profile);
$this->assertLessThanOrEqual(100, $score);
}
// ─── Model Helpers ──────────────────────────────────────────────────
public function test_full_name_uses_display_name_when_available(): void
{
$this->profile->update(['display_name' => 'Cool Creator']);
$this->assertEquals('Cool Creator', $this->profile->full_name);
}
public function test_full_name_falls_back_to_first_last(): void
{
$this->profile->update(['display_name' => null, 'first_name' => 'John', 'last_name' => 'Doe']);
$this->assertEquals('John Doe', $this->profile->full_name);
}
public function test_can_change_username_when_never_changed(): void
{
$this->profile->update(['username_changed_at' => null]);
$this->assertTrue($this->profile->canChangeUsername());
}
public function test_cannot_change_username_within_30_days(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(10)]);
$this->assertFalse($this->profile->canChangeUsername());
}
public function test_can_change_username_after_30_days(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(30)]);
$this->assertTrue($this->profile->canChangeUsername());
}
public function test_days_until_username_change_attribute(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(20)]);
$this->assertEquals(10, $this->profile->days_until_username_change);
}
public function test_days_until_username_change_zero_when_allowed(): void
{
$this->profile->update(['username_changed_at' => now()->subDays(35)]);
$this->assertEquals(0, $this->profile->days_until_username_change);
}
public function test_is_profile_visible_at_50_percent(): void
{
$this->profile->update(['completion_percentage' => 50]);
$this->assertTrue($this->profile->isProfileVisible());
$this->profile->update(['completion_percentage' => 49]);
$this->assertFalse($this->profile->isProfileVisible());
}
public function test_is_profile_complete_at_80_percent(): void
{
$this->profile->update(['completion_percentage' => 80]);
$this->assertTrue($this->profile->isProfileComplete());
$this->profile->update(['completion_percentage' => 79]);
$this->assertFalse($this->profile->isProfileComplete());
}
public function test_profile_url_attribute(): void
{
$this->assertStringContainsString('/creators/test_creator', $this->profile->profile_url);
}
public function test_avatar_url_with_path(): void
{
$this->profile->update(['profile_picture_path' => 'creators/test/avatar.webp']);
$this->assertStringContainsString('storage/creators/test/avatar.webp', $this->profile->avatar_url);
}
public function test_avatar_url_null_without_path(): void
{
$this->profile->update(['profile_picture_path' => null]);
$this->assertNull($this->profile->avatar_url);
}
// ─── Model Scopes ───────────────────────────────────────────────────
public function test_active_scope_only_returns_active_users(): void
{
$activeUser = User::factory()->creator()->create(['status' => 'active']);
CreatorProfile::factory()->create(['user_id' => $activeUser->id]);
$suspendedUser = User::factory()->creator()->create(['status' => 'suspended']);
CreatorProfile::factory()->create(['user_id' => $suspendedUser->id]);
$active = CreatorProfile::active()->get();
$this->assertTrue($active->every(function ($profile) {
return $profile->user->status === 'active';
}));
}
public function test_visible_scope_requires_50_percent_and_active(): void
{
$user = User::factory()->creator()->create(['status' => 'active']);
CreatorProfile::factory()->create([
'user_id' => $user->id,
'completion_percentage' => 60,
]);
$lowUser = User::factory()->creator()->create(['status' => 'active']);
CreatorProfile::factory()->create([
'user_id' => $lowUser->id,
'completion_percentage' => 30,
]);
$visible = CreatorProfile::visible()->get();
$this->assertTrue($visible->every(fn ($p) => $p->completion_percentage >= 50));
}
public function test_available_scope(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'availability_status' => 'available',
]);
CreatorProfile::factory()->busy()->create([
'user_id' => User::factory()->creator()->create()->id,
]);
$available = CreatorProfile::available()->get();
$this->assertTrue($available->every(fn ($p) => $p->availability_status === 'available'));
}
public function test_by_country_scope(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'country' => 'EG',
]);
$result = CreatorProfile::byCountry('EG')->get();
$this->assertTrue($result->every(fn ($p) => $p->country === 'EG'));
}
public function test_by_niche_scope(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'content_niches' => ['gaming', 'technology'],
]);
$result = CreatorProfile::byNiche('gaming')->get();
$this->assertTrue($result->every(fn ($p) => in_array('gaming', $p->content_niches)));
}
public function test_by_language_scope(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'languages' => ['ar', 'en'],
]);
$result = CreatorProfile::byLanguage('ar')->get();
$this->assertTrue($result->every(fn ($p) => in_array('ar', $p->languages)));
}
public function test_by_skill_scope(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'skills' => ['video_creation', 'voiceover'],
]);
$result = CreatorProfile::bySkill('voiceover')->get();
$this->assertTrue($result->every(fn ($p) => in_array('voiceover', $p->skills)));
}
public function test_min_reputation_scope(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'reputation_score' => 90,
]);
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'reputation_score' => 30,
]);
$result = CreatorProfile::minReputation(80)->get();
$this->assertTrue($result->every(fn ($p) => $p->reputation_score >= 80));
}
public function test_search_scope_finds_by_username(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'username' => 'unique_findable_user',
]);
$result = CreatorProfile::search('unique_findable')->get();
$this->assertGreaterThanOrEqual(1, $result->count());
}
public function test_search_scope_finds_by_first_name(): void
{
CreatorProfile::factory()->create([
'user_id' => User::factory()->creator()->create()->id,
'first_name' => 'Zxqwerty',
]);
$result = CreatorProfile::search('Zxqwerty')->get();
$this->assertGreaterThanOrEqual(1, $result->count());
}
// ─── Policy Tests ───────────────────────────────────────────────────
public function test_anyone_can_view_active_creator(): void
{
$visitor = User::factory()->company()->create();
$this->assertTrue($visitor->can('view', $this->profile));
}
public function test_suspended_user_profile_only_visible_to_admin(): void
{
$this->creatorUser->update(['status' => 'suspended']);
$admin = User::factory()->admin()->create();
$outsider = User::factory()->company()->create();
$this->assertTrue($admin->can('view', $this->profile));
$this->assertFalse($outsider->can('view', $this->profile));
}
public function test_only_owner_can_update_profile(): void
{
$this->assertTrue($this->creatorUser->can('update', $this->profile));
$other = User::factory()->creator()->create();
$this->assertFalse($other->can('update', $this->profile));
}
public function test_only_owner_can_update_avatar(): void
{
$this->assertTrue($this->creatorUser->can('updateAvatar', $this->profile));
$other = User::factory()->creator()->create();
$this->assertFalse($other->can('updateAvatar', $this->profile));
}
public function test_only_owner_can_change_username(): void
{
$this->assertTrue($this->creatorUser->can('changeUsername', $this->profile));
$other = User::factory()->creator()->create();
$this->assertFalse($other->can('changeUsername', $this->profile));
}
}
<?php
namespace Tests\Feature\Settings;
use App\Modules\Settings\Models\PlatformSetting;
use App\Modules\Settings\Services\PlatformSettingsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class PlatformSettingsServiceTest extends TestCase
{
use RefreshDatabase;
private PlatformSettingsService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = app(PlatformSettingsService::class);
Cache::flush();
}
// ─── Get All ────────────────────────────────────────────────────────
public function test_all_returns_defaults_when_no_stored_settings(): void
{
$all = $this->service->all();
$this->assertIsArray($all);
$this->assertArrayHasKey('branding', $all);
$this->assertArrayHasKey('theme', $all);
$this->assertArrayHasKey('theme_dark', $all);
$this->assertArrayHasKey('features', $all);
$this->assertArrayHasKey('seo', $all);
$this->assertArrayHasKey('social', $all);
$this->assertArrayHasKey('limits', $all);
}
public function test_all_returns_default_values(): void
{
$all = $this->service->all();
$this->assertEquals('UGC Heaven', $all['branding']['app_name']);
$this->assertEquals('#6366f1', $all['theme']['color_primary']);
$this->assertEquals(true, $all['features']['registration_enabled']);
$this->assertEquals(50, $all['limits']['max_portfolio_items']);
}
public function test_all_merges_stored_values_with_defaults(): void
{
PlatformSetting::create([
'group' => 'branding',
'key' => 'app_name',
'value' => 'Custom Name',
'type' => 'string',
]);
Cache::flush();
$all = $this->service->all();
$this->assertEquals('Custom Name', $all['branding']['app_name']);
$this->assertNull($all['branding']['logo_url']);
}
public function test_all_caches_results(): void
{
$this->service->all();
$this->assertTrue(Cache::has('platform_settings'));
}
// ─── Get Single Value ───────────────────────────────────────────────
public function test_get_returns_default_for_group_key(): void
{
$value = $this->service->get('branding', 'app_name');
$this->assertEquals('UGC Heaven', $value);
}
public function test_get_returns_stored_value_over_default(): void
{
PlatformSetting::create([
'group' => 'theme',
'key' => 'color_primary',
'value' => '#ff0000',
'type' => 'color',
]);
Cache::flush();
$value = $this->service->get('theme', 'color_primary');
$this->assertEquals('#ff0000', $value);
}
public function test_get_returns_custom_default_for_unknown_key(): void
{
$value = $this->service->get('branding', 'nonexistent', 'fallback');
$this->assertEquals('fallback', $value);
}
public function test_get_returns_null_for_unknown_key_without_default(): void
{
$value = $this->service->get('unknown_group', 'unknown_key');
$this->assertNull($value);
}
// ─── Get Group ──────────────────────────────────────────────────────
public function test_get_group_returns_all_keys_in_group(): void
{
$branding = $this->service->getGroup('branding');
$this->assertArrayHasKey('app_name', $branding);
$this->assertArrayHasKey('logo_url', $branding);
$this->assertArrayHasKey('favicon_url', $branding);
$this->assertArrayHasKey('tagline', $branding);
}
public function test_get_group_returns_empty_for_unknown(): void
{
$result = $this->service->getGroup('nonexistent');
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function test_get_group_features_has_all_feature_flags(): void
{
$features = $this->service->getGroup('features');
$this->assertArrayHasKey('registration_enabled', $features);
$this->assertArrayHasKey('creator_registration_enabled', $features);
$this->assertArrayHasKey('company_registration_enabled', $features);
$this->assertArrayHasKey('portfolio_enabled', $features);
$this->assertArrayHasKey('messaging_enabled', $features);
$this->assertArrayHasKey('reviews_enabled', $features);
$this->assertArrayHasKey('matching_enabled', $features);
$this->assertArrayHasKey('email_verification_required', $features);
$this->assertArrayHasKey('company_approval_required', $features);
$this->assertArrayHasKey('maintenance_mode', $features);
}
// ─── Set ────────────────────────────────────────────────────────────
public function test_set_creates_new_setting(): void
{
$this->service->set('branding', 'app_name', 'New Name', 'string');
$this->assertDatabaseHas('platform_settings', [
'group' => 'branding',
'key' => 'app_name',
'value' => 'New Name',
'type' => 'string',
]);
}
public function test_set_updates_existing_setting(): void
{
PlatformSetting::create([
'group' => 'branding',
'key' => 'app_name',
'value' => 'Old Name',
'type' => 'string',
]);
$this->service->set('branding', 'app_name', 'Updated Name', 'string');
$this->assertDatabaseHas('platform_settings', [
'group' => 'branding',
'key' => 'app_name',
'value' => 'Updated Name',
]);
$this->assertDatabaseCount('platform_settings', 1);
}
public function test_set_boolean_stores_as_string(): void
{
$this->service->set('features', 'registration_enabled', true, 'boolean');
$this->assertDatabaseHas('platform_settings', [
'group' => 'features',
'key' => 'registration_enabled',
'value' => '1',
'type' => 'boolean',
]);
}
public function test_set_boolean_false_stores_as_zero(): void
{
$this->service->set('features', 'maintenance_mode', false, 'boolean');
$this->assertDatabaseHas('platform_settings', [
'group' => 'features',
'key' => 'maintenance_mode',
'value' => '0',
'type' => 'boolean',
]);
}
public function test_set_json_stores_encoded_string(): void
{
$this->service->set('branding', 'custom_data', ['key' => 'value'], 'json');
$this->assertDatabaseHas('platform_settings', [
'group' => 'branding',
'key' => 'custom_data',
'value' => '{"key":"value"}',
'type' => 'json',
]);
}
public function test_set_busts_cache(): void
{
$this->service->all();
$this->assertTrue(Cache::has('platform_settings'));
$this->service->set('branding', 'app_name', 'New', 'string');
$this->assertFalse(Cache::has('platform_settings'));
}
// ─── Set Many ───────────────────────────────────────────────────────
public function test_set_many_stores_multiple_values(): void
{
$this->service->setMany('theme', [
'color_primary' => '#ff0000',
'color_secondary' => '#00ff00',
'color_accent' => '#0000ff',
]);
$this->assertDatabaseHas('platform_settings', [
'group' => 'theme',
'key' => 'color_primary',
'value' => '#ff0000',
]);
$this->assertDatabaseHas('platform_settings', [
'group' => 'theme',
'key' => 'color_secondary',
'value' => '#00ff00',
]);
$this->assertDatabaseHas('platform_settings', [
'group' => 'theme',
'key' => 'color_accent',
'value' => '#0000ff',
]);
}
public function test_set_many_infers_types_correctly(): void
{
$this->service->setMany('features', [
'registration_enabled' => false,
'maintenance_mode' => true,
]);
$this->service->setMany('limits', [
'max_portfolio_items' => 100,
]);
$this->assertDatabaseHas('platform_settings', [
'key' => 'registration_enabled',
'value' => '0',
'type' => 'boolean',
]);
$this->assertDatabaseHas('platform_settings', [
'key' => 'maintenance_mode',
'value' => '1',
'type' => 'boolean',
]);
$this->assertDatabaseHas('platform_settings', [
'key' => 'max_portfolio_items',
'value' => '100',
'type' => 'integer',
]);
}
public function test_set_many_infers_color_type_for_color_keys(): void
{
$this->service->setMany('theme', [
'color_primary' => '#123456',
]);
$this->assertDatabaseHas('platform_settings', [
'key' => 'color_primary',
'type' => 'color',
]);
}
// ─── Theme CSS Generation ───────────────────────────────────────────
public function test_get_theme_css_generates_root_variables(): void
{
$css = $this->service->getThemeCss();
$this->assertStringStartsWith(':root {', $css);
$this->assertStringEndsWith('}', $css);
$this->assertStringContainsString('--color-primary:', $css);
$this->assertStringContainsString('--color-secondary:', $css);
$this->assertStringContainsString('--gradient-primary:', $css);
$this->assertStringContainsString('--border-radius-card:', $css);
}
public function test_get_theme_css_uses_stored_values(): void
{
PlatformSetting::create([
'group' => 'theme',
'key' => 'color_primary',
'value' => '#custom1',
'type' => 'color',
]);
Cache::flush();
$css = $this->service->getThemeCss();
$this->assertStringContainsString('#custom1', $css);
}
public function test_get_dark_theme_css_generates_data_theme_selector(): void
{
$css = $this->service->getDarkThemeCss();
$this->assertStringStartsWith('[data-theme="dark"] {', $css);
$this->assertStringContainsString('--color-bg:', $css);
$this->assertStringContainsString('--color-surface:', $css);
$this->assertStringContainsString('--color-text-primary:', $css);
}
// ─── Feature Flags ──────────────────────────────────────────────────
public function test_is_feature_enabled_returns_default_true(): void
{
$this->assertTrue($this->service->isFeatureEnabled('registration_enabled'));
$this->assertTrue($this->service->isFeatureEnabled('messaging_enabled'));
$this->assertTrue($this->service->isFeatureEnabled('reviews_enabled'));
}
public function test_is_feature_enabled_returns_default_false(): void
{
$this->assertFalse($this->service->isFeatureEnabled('maintenance_mode'));
}
public function test_is_feature_enabled_respects_stored_value(): void
{
$this->service->set('features', 'registration_enabled', false, 'boolean');
Cache::flush();
$this->assertFalse($this->service->isFeatureEnabled('registration_enabled'));
}
public function test_is_feature_enabled_unknown_returns_false(): void
{
$this->assertFalse($this->service->isFeatureEnabled('nonexistent_feature'));
}
// ─── Cache Busting ──────────────────────────────────────────────────
public function test_bust_cache_removes_cached_settings(): void
{
$this->service->all();
$this->assertTrue(Cache::has('platform_settings'));
$this->service->bustCache();
$this->assertFalse(Cache::has('platform_settings'));
}
// ─── Get Defaults ───────────────────────────────────────────────────
public function test_get_defaults_returns_all_groups(): void
{
$defaults = $this->service->getDefaults();
$this->assertArrayHasKey('branding', $defaults);
$this->assertArrayHasKey('theme', $defaults);
$this->assertArrayHasKey('theme_dark', $defaults);
$this->assertArrayHasKey('features', $defaults);
$this->assertArrayHasKey('seo', $defaults);
$this->assertArrayHasKey('social', $defaults);
$this->assertArrayHasKey('limits', $defaults);
}
public function test_defaults_have_correct_types(): void
{
$defaults = $this->service->getDefaults();
$this->assertIsBool($defaults['features']['registration_enabled']);
$this->assertIsInt($defaults['limits']['max_portfolio_items']);
$this->assertIsString($defaults['branding']['app_name']);
}
// ─── Model: Casted Value Accessor ───────────────────────────────────
public function test_model_casted_value_boolean(): void
{
$setting = PlatformSetting::create([
'group' => 'features',
'key' => 'test_bool',
'value' => '1',
'type' => 'boolean',
]);
$this->assertTrue($setting->casted_value);
$setting->update(['value' => '0']);
$setting->refresh();
$this->assertFalse($setting->casted_value);
}
public function test_model_casted_value_integer(): void
{
$setting = PlatformSetting::create([
'group' => 'limits',
'key' => 'test_int',
'value' => '42',
'type' => 'integer',
]);
$this->assertSame(42, $setting->casted_value);
}
public function test_model_casted_value_json(): void
{
$setting = PlatformSetting::create([
'group' => 'custom',
'key' => 'test_json',
'value' => '{"items":["a","b"]}',
'type' => 'json',
]);
$this->assertIsArray($setting->casted_value);
$this->assertEquals(['items' => ['a', 'b']], $setting->casted_value);
}
public function test_model_casted_value_string(): void
{
$setting = PlatformSetting::create([
'group' => 'branding',
'key' => 'test_str',
'value' => 'hello world',
'type' => 'string',
]);
$this->assertSame('hello world', $setting->casted_value);
}
public function test_model_group_scope(): void
{
PlatformSetting::create(['group' => 'theme', 'key' => 'a', 'value' => '1', 'type' => 'string']);
PlatformSetting::create(['group' => 'theme', 'key' => 'b', 'value' => '2', 'type' => 'string']);
PlatformSetting::create(['group' => 'branding', 'key' => 'c', 'value' => '3', 'type' => 'string']);
$themeSettings = PlatformSetting::group('theme')->get();
$this->assertCount(2, $themeSettings);
$this->assertTrue($themeSettings->every(fn ($s) => $s->group === 'theme'));
}
// ─── Limits Default Values ──────────────────────────────────────────
public function test_limits_have_sensible_defaults(): void
{
$limits = $this->service->getGroup('limits');
$this->assertEquals(50, $limits['max_portfolio_items']);
$this->assertEquals(20, $limits['max_campaign_deliverables']);
$this->assertEquals(2048, $limits['max_file_upload_mb']);
$this->assertEquals(10, $limits['max_image_upload_mb']);
$this->assertEquals(90, $limits['campaign_auto_close_days']);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment