insert([ 'id' => 7, 'name' => 'General Helper', 'created_at' => now(), 'updated_at' => now(), ]); $this->category = (object) ['id' => 7]; } /** * Test S1 Onboard: Get Config and Supported Languages. */ public function test_s1_get_languages_config() { $response = $this->getJson('/api/workers/config'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'languages' => [ ['code' => 'HI', 'name' => 'Hindi (हिन्दी)'], ['code' => 'SW', 'name' => 'Swahili (Kiswahili)'], ['code' => 'TL', 'name' => 'Tagalog (Wikang Tagalog)'], ['code' => 'TA', 'name' => 'Tamil (தமிழ்)'], ] ]); } /** * Test Get Skills Master API. */ public function test_get_skills_master_api() { // Seed some master skills \Illuminate\Support\Facades\DB::table('skills')->insert([ ['name' => 'Cooking', 'created_at' => now(), 'updated_at' => now()], ['name' => 'Cleaning', 'created_at' => now(), 'updated_at' => now()], ]); $response = $this->getJson('/api/workers/skills'); $response->assertStatus(200) ->assertJson([ 'success' => true, ]) ->assertJsonStructure([ 'success', 'skills' => [ '*' => ['id', 'name'] ] ]); } /** * Test S1 Onboard: Send and Verify OTP. */ public function test_s1_otp_flow() { $phone = '+971501234567'; // 1. Send OTP $sendResponse = $this->postJson('/api/workers/send-otp', [ 'phone' => $phone, ]); $sendResponse->assertStatus(200) ->assertJson([ 'success' => true, 'identifier' => $phone, ]); // Verify OTP is stored in cache $cached = Cache::get('worker_otp_' . $phone); $this->assertNotNull($cached); $this->assertEquals('111111', $cached['code']); // Dev default OTP // 2. Verify OTP $verifyResponse = $this->postJson('/api/workers/verify-otp', [ 'phone' => $phone, 'otp' => '111111', ]); $verifyResponse->assertStatus(200) ->assertJson([ 'success' => true, 'identifier' => $phone, ]); // Verify the gate cache is present $this->assertTrue(Cache::get('worker_otp_verified_' . $phone)); } /** * Test S1 Onboard: Complete Basic Profile Setup. */ public function test_s1_complete_basic_profile_setup() { $phone = '+971501234567'; // Bypass OTP by seeding the cache gate Cache::put('worker_otp_verified_' . $phone, true, 3600); $response = $this->postJson('/api/workers/setup-profile', [ 'phone' => $phone, 'name' => 'Rahul Sharma', 'nationality' => 'India', 'language' => 'HI', 'gender' => 'male', 'skills' => [], ]); $response->assertStatus(201) ->assertJsonStructure([ 'success', 'message', 'data' => [ 'worker' => [ 'id', 'name', 'email', 'phone', 'language', 'verified', 'status', ], 'token', ] ]); $this->assertDatabaseHas('workers', [ 'name' => 'Rahul Sharma', 'phone' => $phone, 'language' => 'HI', 'gender' => 'male', 'verified' => false, 'status' => 'active', ]); // Gate cache should be purged $this->assertNull(Cache::get('worker_otp_verified_' . $phone)); } /** * Test S2 Profile: Add Experience & Preferences. */ public function test_s2_update_experience_and_preferences() { $worker = Worker::create([ 'name' => 'John Swahili', 'email' => 'john@example.com', 'phone' => '+971509999999', 'language' => 'SW', 'password' => bcrypt('password'), 'nationality' => 'Kenya', 'age' => 22, 'salary' => 1200, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'New helper', 'category_id' => $this->category->id, 'verified' => false, 'status' => 'active', 'api_token' => 'test-bearer-token-123', ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer test-bearer-token-123', ])->postJson('/api/workers/profile/update', [ 'experience' => '3 Years Cooking & Childcare', 'salary' => 1800, 'bio' => 'Extremely experienced in Arabic and Western cooking.', ]); $response->assertStatus(200); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'experience' => '3 Years Cooking & Childcare', 'salary' => 1800, ]); } /** * Test S2 Profile: Upload Emirates ID & PDPL Security Purge & Verified Badge Awarded. */ public function test_s2_upload_emirates_id_and_pdpl_secure_purge() { Storage::fake('local'); $worker = Worker::create([ 'name' => 'Juan Dela Cruz', 'email' => 'juan@example.com', 'phone' => '+971507777777', 'language' => 'TL', 'password' => bcrypt('password'), 'nationality' => 'Philippines', 'age' => 28, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Looking for helper job', 'category_id' => $this->category->id, 'verified' => false, 'status' => 'active', 'api_token' => 'test-bearer-token-456', ]); $fakeIdFile = UploadedFile::fake()->create('emirates_id.jpg', 800, 'image/jpeg'); $response = $this->withHeaders([ 'Authorization' => 'Bearer test-bearer-token-456', ])->postJson('/api/workers/profile/upload-emirates-id', [ 'emirates_id_file' => $fakeIdFile, ]); $response->assertStatus(200) ->assertJsonStructure([ 'success', 'message', 'ocr_extracted_data' => [ 'name', 'emirates_id_number', 'expiry_date', ], 'data' => [ 'worker', 'document', ] ]); // Asserts database states $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'verified' => true, // Badge awarded! ]); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $worker->id, 'type' => 'emirates_id', 'file_path' => '[DELETED_FOR_PDPL_COMPLIANCE]', // Image file removed for PDPL ]); // Ensure physical file is deleted and not stored in storage disk Storage::disk('local')->assertDirectoryEmpty('temp/documents'); } /** * Test S3 Go Live: Status set to Active. */ public function test_s3_go_live() { $worker = Worker::create([ 'name' => 'Selvi Tamil', 'email' => 'selvi@example.com', 'phone' => '+971508888888', 'language' => 'TA', 'password' => bcrypt('password'), 'nationality' => 'Sri Lanka', 'age' => 32, 'salary' => 1400, 'availability' => 'Not Available', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Ready to work', 'category_id' => $this->category->id, 'verified' => true, 'status' => 'inactive', 'api_token' => 'test-bearer-token-789', ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer test-bearer-token-789', ])->postJson('/api/workers/go-live'); $response->assertStatus(200) ->assertJsonPath('data.worker.status', 'active') ->assertJsonPath('data.worker.availability', 'Immediate'); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'status' => 'active', 'availability' => 'Immediate', ]); } /** * Test S5 Avail?: Toggle availability (Still looking?). */ public function test_s5_toggle_availability_still_looking() { $worker = Worker::create([ 'name' => 'Rahul Sharma', 'email' => 'rahul@example.com', 'phone' => '+971501234567', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'India', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'test-bearer-token-abc', ]); // Toggle still_looking = false (NO -> Hidden) $response1 = $this->withHeaders([ 'Authorization' => 'Bearer test-bearer-token-abc', ])->postJson('/api/workers/profile/toggle-availability', [ 'still_looking' => false, ]); $response1->assertStatus(200) ->assertJson([ 'success' => true, 'still_looking' => false, 'status' => 'hidden', ]); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'status' => 'hidden', ]); // Toggle still_looking = true (YES -> Visible/Active) $response2 = $this->withHeaders([ 'Authorization' => 'Bearer test-bearer-token-abc', ])->postJson('/api/workers/profile/toggle-availability', [ 'still_looking' => true, ]); $response2->assertStatus(200) ->assertJson([ 'success' => true, 'still_looking' => true, 'status' => 'active', ]); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'status' => 'active', ]); } /** * Test S6 Outcome: Marked as Hired (Profile removed from search - auto-hidden). */ public function test_s6_outcome_mark_hired() { $worker = Worker::create([ 'name' => 'Rahul Sharma', 'email' => 'rahul@example.com', 'phone' => '+971501234567', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'India', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'test-bearer-token-abc', ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer test-bearer-token-abc', ])->postJson('/api/workers/profile/mark-hired'); $response->assertStatus(200) ->assertJsonPath('data.worker.status', 'Hired'); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'status' => 'Hired', ]); } /** * Test registration with new API fields. */ public function test_register_with_new_api_fields() { $fakePassport = UploadedFile::fake()->create('passport.pdf', 500, 'application/pdf'); $response = $this->postJson('/api/workers/register', [ 'name' => 'John New Worker', 'phone' => '+971501111112', 'salary' => 2200.50, 'password' => 'secret123', 'passport_file' => $fakePassport, 'language' => 'HI', 'nationality' => 'Kenya', 'age' => 30, 'experience' => '5 Years', 'in_country' => 'true', 'visa_status' => 'Tourist Visa', 'preferred_job_type' => 'full-time', 'gender' => 'male', 'live_in_out' => 'live_out', 'preferred_location' => 'Dubai Marina', ]); $response->assertStatus(201); $this->assertDatabaseHas('workers', [ 'name' => 'John New Worker', 'phone' => '+971501111112', 'in_country' => true, 'visa_status' => 'Tourist Visa', 'preferred_job_type' => 'full-time', 'gender' => 'male', 'live_in_out' => 'live_out', 'preferred_location' => 'Dubai Marina', ]); } /** * Test updating profile with new API fields. */ public function test_update_profile_with_new_api_fields() { $worker = Worker::create([ 'name' => 'Original Worker', 'email' => 'orig@example.com', 'phone' => '+971502222223', 'language' => 'SW', 'password' => bcrypt('password'), 'nationality' => 'Kenya', 'age' => 22, 'salary' => 1200, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'New helper', 'category_id' => $this->category->id, 'verified' => false, 'status' => 'active', 'api_token' => 'bearer-update-token', ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer bearer-update-token', ])->postJson('/api/workers/profile/update', [ 'in_country' => false, 'visa_status' => null, 'preferred_job_type' => 'part-time', 'gender' => 'female', 'live_in_out' => 'live_in', 'preferred_location' => 'Abu Dhabi', 'country' => 'UAE', 'city' => 'Abu Dhabi', 'area' => 'Khalifa City', ]); $response->assertStatus(200); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'in_country' => false, 'visa_status' => null, 'preferred_job_type' => 'part-time', 'gender' => 'female', 'live_in_out' => 'live_in', 'preferred_location' => 'Abu Dhabi', 'country' => 'UAE', 'city' => 'Abu Dhabi', 'area' => 'Khalifa City', ]); } /** * Test nationalities list API with translations. */ public function test_nationalities_list_api() { // 1. Default (English) $responseEn = $this->getJson('/api/nationalities?per_page=200'); $responseEn->assertStatus(200) ->assertJson([ 'success' => true, ]) ->assertJsonFragment([ 'code' => 'IN', 'name' => 'Indian', ]) ->assertJsonFragment([ 'code' => 'PH', 'name' => 'Filipino', ]); // 2. Hindi $responseHi = $this->getJson('/api/nationalities?lang=hindi&per_page=200'); $responseHi->assertStatus(200) ->assertJsonFragment([ 'code' => 'IN', 'name' => 'भारतीय', ]); // 3. Swahili via header $responseSw = $this->withHeaders([ 'Accept-Language' => 'sw', ])->getJson('/api/nationalities?per_page=200'); $responseSw->assertStatus(200) ->assertJsonFragment([ 'code' => 'IN', 'name' => 'Kihindi', ]); // 4. Tagalog (taglo) $responseTl = $this->getJson('/api/nationalities?lang=taglo&per_page=200'); $responseTl->assertStatus(200) ->assertJsonFragment([ 'code' => 'IN', 'name' => 'Kano/Indian', ]); // 5. Tamil $responseTa = $this->getJson('/api/nationalities?lang=tamil&per_page=200'); $responseTa->assertStatus(200) ->assertJsonFragment([ 'code' => 'IN', 'name' => 'இந்தியன்', ]); // 6. Test Pagination structure $this->flushHeaders(); $responsePaginated = $this->getJson('/api/nationalities?page=2&per_page=5'); $responsePaginated->assertStatus(200) ->assertJsonStructure([ 'success', 'data' => [ 'nationalities', 'pagination' => [ 'total', 'per_page', 'current_page', 'last_page' ] ] ]); $this->assertCount(5, $responsePaginated->json('data.nationalities')); $this->assertEquals(2, $responsePaginated->json('data.pagination.current_page')); // 7. Test Search functionality $this->flushHeaders(); $responseSearch = $this->getJson('/api/nationalities?search=Indian'); $responseSearch->assertStatus(200); $this->assertGreaterThanOrEqual(1, count($responseSearch->json('data.nationalities'))); $responseSearch->assertJsonFragment([ 'code' => 'IN', 'name' => 'Indian' ]); } /** * Test worker dashboard stats including employer_contacted and profile_viewed unique counts. */ public function test_worker_dashboard_stats() { // 1. Create a worker $worker = Worker::create([ 'name' => 'Rahul Sharma', 'email' => 'rahul@example.com', 'phone' => '+971501234567', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'India', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'worker-test-token-dashboard', ]); // Create passport document to prevent "Passport Pending" 404 WorkerDocument::create([ 'worker_id' => $worker->id, 'type' => 'passport', 'number' => '123456', 'file_path' => 'passports/test.pdf', ]); // 2. Create two employers $employer1 = \App\Models\User::create([ 'name' => 'Employer One', 'email' => 'emp1@example.com', 'password' => bcrypt('password'), 'role' => 'employer', 'api_token' => 'emp1-token', ]); $employer2 = \App\Models\User::create([ 'name' => 'Employer Two', 'email' => 'emp2@example.com', 'password' => bcrypt('password'), 'role' => 'employer', 'api_token' => 'emp2-token', ]); // 3. Employer 1 views worker\'s profile twice (should count as 1) $response = $this->withHeaders([ 'Authorization' => 'Bearer emp1-token', ])->getJson('/api/employers/workers/' . $worker->id); $response->assertStatus(200); $response = $this->withHeaders([ 'Authorization' => 'Bearer emp1-token', ])->getJson('/api/employers/workers/' . $worker->id); $response->assertStatus(200); // 4. Employer 2 views worker\'s profile once $response = $this->withHeaders([ 'Authorization' => 'Bearer emp2-token', ])->getJson('/api/employers/workers/' . $worker->id); $response->assertStatus(200); // Assert profile_views database has 2 unique views $this->assertEquals(2, \App\Models\ProfileView::where('worker_id', $worker->id)->count()); // 5. Create a conversation (contact) from Employer 1 \App\Models\Conversation::create([ 'employer_id' => $employer1->id, 'worker_id' => $worker->id, ]); // 6. Hit the worker dashboard API $dashboardResponse = $this->withHeaders([ 'Authorization' => 'Bearer worker-test-token-dashboard', ])->getJson('/api/workers/dashboard'); $dashboardResponse->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'active_status' => 'active', 'profile_viewed' => 2, 'employer_contacted' => 1, ] ]); } }