seed(\Database\Seeders\NationalitySeeder::class); } /** * 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: Send OTP fails if mobile number already exists. */ public function test_s1_otp_flow_fails_if_phone_already_exists() { Worker::create([ 'name' => 'Existing Worker', 'email' => 'existing@example.com', 'phone' => '+971501234567', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'status' => 'active', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'Test worker', ]); $response = $this->postJson('/api/workers/send-otp', [ 'phone' => '+971501234567', ]); $response->assertStatus(422) ->assertJson([ 'success' => false, 'message' => 'Validation error.', ]) ->assertJsonValidationErrors(['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' => 'Indian', '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' => 'Kenyan', 'age' => 22, 'salary' => 1200, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'New helper', '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, ]); $response->assertStatus(200); $this->assertDatabaseHas('workers', [ 'id' => $worker->id, 'experience' => '3 Years Cooking & Childcare', 'salary' => 1800, ]); } /** /** * 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 Lankan', 'age' => 32, 'salary' => 1400, 'availability' => 'Not Available', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Ready to work', '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' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', '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' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', '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' => 'Kenyan', '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 worker registration utilizing OCR extracted fields. */ public function test_register_with_ocr_extraction() { // Consolidated registration with inline passport payload $response = $this->postJson('/api/workers/register', [ 'name' => 'Temp Name', 'phone' => '+971509999999', 'salary' => 2500, 'password' => 'secret123', 'language' => 'HI', 'passport' => [ 'date_of_birth' => '07/11/1990', 'date_of_expiry' => '06/09/2030', 'date_of_issue' => '06/09/2020', 'given_names' => 'MATAR ALI KHARBASH', 'surname' => 'ALSAEDI', 'nationality' => 'ARE', 'passport_number' => 'SQ0000151', ] ]); $response->assertStatus(201); $workerId = $response->json('data.worker.id'); $expectedAge = now()->diff(new \DateTime('1990-11-07'))->y; $this->assertDatabaseHas('workers', [ 'id' => $workerId, 'name' => 'MATAR ALI KHARBASH ALSAEDI', 'phone' => '+971509999999', 'nationality' => 'Emirati', 'age' => $expectedAge, 'verified' => true, ]); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'passport', 'number' => 'SQ0000151', 'expiry_date' => '2030-09-06', 'issue_date' => '2020-09-06', ]); } /** * Test worker passport and visa registration with direct JSON payload from mobile app. */ public function test_register_passport_and_visa_with_direct_json_payload() { $response = $this->postJson('/api/workers/register', [ 'name' => 'Temp Name', 'phone' => '+971508888888', 'salary' => 2000, 'password' => 'secret123', 'language' => 'HI', 'passport' => [ 'authority' => 'Issuing Authority', 'date_of_birth' => '14/05/1990', 'date_of_expiry' => '05/07/2028', 'date_of_issue' => '05/07/2018', 'document_type' => 'P', 'given_names' => 'Jane', 'issuing_country' => 'PHL', 'nationality' => 'PHL', 'passport_number' => 'Y34B6790', 'personal_number' => '28', 'place_of_birth' => 'Manila', 'sex' => 'F', 'surname' => 'Doe', ], 'visa' => [ 'accompanied_by' => '9', 'expiry_date' => '2027-02-15', 'file_number' => '201/2021/2840233', 'id_number' => '784198839607839', 'issue_date' => '2025-02-16', 'name' => 'Jane Doe', 'passport_number' => 'Y34B6790', 'place_of_issue' => 'Dubai', 'profession' => 'CLEANER', 'sponsor' => 'Sponsor Name', ] ]); $response->assertStatus(201); $workerId = $response->json('data.worker.id'); $expectedAge = now()->diff(new \DateTime('1990-05-14'))->y; $this->assertDatabaseHas('workers', [ 'id' => $workerId, 'name' => 'Jane Doe', 'phone' => '+971508888888', 'nationality' => 'Filipino', 'age' => $expectedAge, 'verified' => true, 'visa_status' => 'CLEANER', ]); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'passport', 'number' => 'Y34B6790', 'expiry_date' => '2028-07-05', 'issue_date' => '2018-07-05', ]); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'visa', 'number' => '201/2021/2840233', 'expiry_date' => '2027-02-15', 'issue_date' => '2025-02-16', ]); $passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first(); $this->assertNotNull($passportDoc->ocr_data); $this->assertEquals('PHL', $passportDoc->ocr_data['nationality']); $this->assertEquals('Jane', $passportDoc->ocr_data['given_names']); $this->assertEquals('Doe', $passportDoc->ocr_data['surname']); $this->assertEquals('Issuing Authority', $passportDoc->ocr_data['authority']); $visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first(); $this->assertNotNull($visaDoc->ocr_data); $this->assertEquals('784198839607839', $visaDoc->ocr_data['id_number']); $this->assertEquals('CLEANER', $visaDoc->ocr_data['profession']); $this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']); } /** * Test worker registration with the specific passport fields and ocr_accuracy. */ public function test_register_passport_with_full_ocr_fields() { $response = $this->postJson('/api/workers/register', [ 'name' => 'Temp Name', 'phone' => '+971507777777', 'salary' => 2000, 'password' => 'secret123', 'language' => 'HI', 'passport' => [ 'document_type' => 'P', 'issuing_country' => 'HAS', 'surname' => 'KHALED', 'given_names' => 'KHALED AL', 'passport_number' => 'Y34B67890', 'nationality' => 'RE1', 'date_of_birth' => '1990-05-14', 'date_of_expiry' => '2022-05-07', 'place_of_birth' => 'SHARJAH', 'authority' => 'MINISTRY OF INTERIOR U.S. 21 is ,,,, is', 'sex' => 'M', 'date_of_issue' => '2017-07-05', 'ocr_accuracy' => 98.7, ] ]); $response->assertStatus(201); $workerId = $response->json('data.worker.id'); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'passport', 'number' => 'Y34B67890', 'expiry_date' => '2022-05-07', 'issue_date' => '2017-07-05', 'ocr_accuracy' => 98.7, ]); $passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first(); $this->assertNotNull($passportDoc->ocr_data); $this->assertEquals('KHALED AL', $passportDoc->ocr_data['given_names']); $this->assertEquals('KHALED', $passportDoc->ocr_data['surname']); $this->assertEquals('MINISTRY OF INTERIOR U.S. 21 is ,,,, is', $passportDoc->ocr_data['authority']); $this->assertEquals(98.7, $passportDoc->ocr_data['ocr_accuracy']); } /** * Test worker registration with the specific visa fields and ocr_accuracy. */ public function test_register_visa_with_full_ocr_fields() { $response = $this->postJson('/api/workers/register', [ 'name' => 'Temp Name', 'phone' => '+971507777778', 'salary' => 2000, 'password' => 'secret123', 'language' => 'HI', 'visa' => [ 'document_type' => 'uae_visa', 'country' => 'United Arab Emirates', 'visa_type' => 'ENTRY PERMIT', 'entry_permit_no' => '77003098 / 2019 / 204', 'issue_date' => '', 'valid_until' => '04-MAR-2019', 'uid_no' => '207404887', 'full_name' => 'Mr.MUHAMMAD NADEEM RASHEED', 'nationality' => 'PAKISTAN', 'place_of_birth' => 'SIALKOT PAK', 'date_of_birth' => '25-JUN-1999', 'passport_no' => 'EN9458281', 'profession' => 'SALES REPRESENTATIVE', 'sponsor_name' => '', 'ocr_accuracy' => 97.4, ] ]); $response->assertStatus(201); $workerId = $response->json('data.worker.id'); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'visa', 'number' => '77003098 / 2019 / 204', 'expiry_date' => '2019-03-04', 'ocr_accuracy' => 97.4, ]); $visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first(); $this->assertNotNull($visaDoc->ocr_data); $this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']); $this->assertEquals('ENTRY PERMIT', $visaDoc->ocr_data['visa_type']); $this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']); $this->assertEquals(97.4, $visaDoc->ocr_data['ocr_accuracy']); } /** * Test worker passport and visa registration when they are passed as JSON strings. */ public function test_register_passport_and_visa_with_json_encoded_strings() { $response = $this->postJson('/api/workers/register', [ 'name' => 'JSON String Worker', 'phone' => '+971509999999', 'salary' => 3000, 'password' => 'secret123', 'language' => 'EN', 'passport' => json_encode([ 'authority' => 'Authority', 'date_of_birth' => '14/05/1990', 'date_of_expiry' => '05/07/2028', 'date_of_issue' => '05/07/2018', 'document_type' => 'P', 'given_names' => 'Johnny', 'issuing_country' => 'PHL', 'nationality' => 'PHL', 'passport_number' => 'Z99B1234', 'personal_number' => '28', 'place_of_birth' => 'Manila', 'sex' => 'M', 'surname' => 'Test', ]), 'visa' => json_encode([ 'accompanied_by' => '0', 'expiry_date' => '2027-02-15', 'file_number' => '202/2022/2840234', 'id_number' => '784198839607840', 'issue_date' => '2025-02-16', 'name' => 'Johnny Test', 'passport_number' => 'Z99B1234', 'place_of_issue' => 'Abu Dhabi', 'profession' => 'HELPER', 'sponsor' => 'Sponsor Name', ]) ]); $response->assertStatus(201); $workerId = $response->json('data.worker.id'); $this->assertDatabaseHas('workers', [ 'id' => $workerId, 'name' => 'Johnny Test', 'phone' => '+971509999999', 'verified' => true, 'visa_status' => 'HELPER', ]); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'passport', 'number' => 'Z99B1234', ]); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $workerId, 'type' => 'visa', 'number' => '202/2022/2840234', ]); } /** * 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' => 'Kenyan', 'age' => 22, 'salary' => 1200, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'New helper', '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', ]); $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', ]); } /** * 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', 'country_code' => 'IN', 'phone_code' => '+91', 'dial_code' => '+91', 'calling_code' => '+91', 'name' => 'Indian', ]) ->assertJsonFragment([ 'code' => 'PH', 'country_code' => 'PH', 'phone_code' => '+63', 'dial_code' => '+63', 'calling_code' => '+63', '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', 'country_code' => 'IN', 'phone_code' => '+91', 'dial_code' => '+91', 'calling_code' => '+91', 'name' => 'Indian' ]); } /** * Test languages list API with translations. */ public function test_languages_list_api() { // 1. Default (English) $responseEn = $this->getJson('/api/languages?per_page=200'); $responseEn->assertStatus(200) ->assertJson([ 'success' => true, ]) ->assertJsonFragment([ 'code' => 'en', 'name' => 'English', ]) ->assertJsonFragment([ 'code' => 'hi', 'name' => 'Hindi', ]); // 2. Hindi $responseHi = $this->getJson('/api/languages?lang=hindi&per_page=200'); $responseHi->assertStatus(200) ->assertJsonFragment([ 'code' => 'hi', 'name' => 'हिन्दी', ]); // 3. Swahili via header $responseSw = $this->withHeaders([ 'Accept-Language' => 'sw', ])->getJson('/api/languages?per_page=200'); $responseSw->assertStatus(200) ->assertJsonFragment([ 'code' => 'sw', 'name' => 'Kiswahili', ]); // 4. Tagalog (taglo) $responseTl = $this->getJson('/api/languages?lang=taglo&per_page=200'); $responseTl->assertStatus(200) ->assertJsonFragment([ 'code' => 'tl', 'name' => 'Tagalog', ]); // 5. Tamil $responseTa = $this->getJson('/api/languages?lang=tamil&per_page=200'); $responseTa->assertStatus(200) ->assertJsonFragment([ 'code' => 'ta', 'name' => 'தமிழ்', ]); // 6. Test Pagination structure $this->flushHeaders(); $responsePaginated = $this->getJson('/api/languages?page=2&per_page=5'); $responsePaginated->assertStatus(200) ->assertJsonStructure([ 'success', 'data' => [ 'languages', 'pagination' => [ 'total', 'per_page', 'current_page', 'last_page' ] ] ]); $this->assertCount(5, $responsePaginated->json('data.languages')); $this->assertEquals(2, $responsePaginated->json('data.pagination.current_page')); // 7. Test Search functionality $this->flushHeaders(); $responseSearch = $this->getJson('/api/languages?search=Hindi'); $responseSearch->assertStatus(200); $this->assertGreaterThanOrEqual(1, count($responseSearch->json('data.languages'))); $responseSearch->assertJsonFragment([ 'code' => 'hi', 'name' => 'Hindi' ]); } /** * 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' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', '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, ] ]); } /** * Test getting new unviewed announcements and marking them viewed. */ public function test_worker_new_announcements_flow() { // 1. Create a worker $worker = Worker::create([ 'name' => 'Rahul Sharma', 'email' => 'rahul@example.com', 'phone' => '+971501234567', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', 'verified' => true, 'status' => 'active', 'api_token' => 'worker-test-token-announcements', ]); // Create passport document to prevent 404/any checks WorkerDocument::create([ 'worker_id' => $worker->id, 'type' => 'passport', 'number' => '123456', 'file_path' => 'passports/test.pdf', ]); // 2. Create 4 approved announcements $ann1 = \App\Models\Announcement::create([ 'title' => 'Event 1', 'body' => 'Details 1', 'type' => 'charity', 'status' => 'approved', ]); $ann2 = \App\Models\Announcement::create([ 'title' => 'Event 2', 'body' => 'Details 2', 'type' => 'charity', 'status' => 'approved', ]); $ann3 = \App\Models\Announcement::create([ 'title' => 'Event 3', 'body' => 'Details 3', 'type' => 'charity', 'status' => 'approved', ]); $ann4 = \App\Models\Announcement::create([ 'title' => 'Event 4', 'body' => 'Details 4', 'type' => 'charity', 'status' => 'approved', ]); // 3. Get new announcements (should return max 3 events, latest first: Event 4, Event 3, Event 2) $response = $this->withHeaders([ 'Authorization' => 'Bearer worker-test-token-announcements', ])->getJson('/api/workers/announcements/new'); $response->assertStatus(200); $data = $response->json('data.announcements'); $this->assertCount(3, $data); $this->assertEquals('Event 4', $data[0]['title']); $this->assertEquals('Event 3', $data[1]['title']); $this->assertEquals('Event 2', $data[2]['title']); // Assert that they are now marked as viewed in the DB $this->assertDatabaseHas('announcement_views', ['worker_id' => $worker->id, 'announcement_id' => $ann4->id]); $this->assertDatabaseHas('announcement_views', ['worker_id' => $worker->id, 'announcement_id' => $ann3->id]); $this->assertDatabaseHas('announcement_views', ['worker_id' => $worker->id, 'announcement_id' => $ann2->id]); // 4. Get new announcements again (should return the remaining 1: Event 1) $response = $this->withHeaders([ 'Authorization' => 'Bearer worker-test-token-announcements', ])->getJson('/api/workers/announcements/new'); $response->assertStatus(200); $data = $response->json('data.announcements'); $this->assertCount(1, $data); $this->assertEquals('Event 1', $data[0]['title']); // 5. Get new announcements again (should return 0) $response = $this->withHeaders([ 'Authorization' => 'Bearer worker-test-token-announcements', ])->getJson('/api/workers/announcements/new'); $response->assertStatus(200); $this->assertCount(0, $response->json('data.announcements')); // 6. Create a 5th approved announcement $ann5 = \App\Models\Announcement::create([ 'title' => 'Event 5', 'body' => 'Details 5', 'type' => 'charity', 'status' => 'approved', ]); // 7. Manually mark Event 5 as viewed $response = $this->withHeaders([ 'Authorization' => 'Bearer worker-test-token-announcements', ])->postJson("/api/workers/announcements/{$ann5->id}/view"); $response->assertStatus(200); // 8. Get new announcements (should return 0 because Event 5 is already viewed) $response = $this->withHeaders([ 'Authorization' => 'Bearer worker-test-token-announcements', ])->getJson('/api/workers/announcements/new'); $response->assertStatus(200); $this->assertCount(0, $response->json('data.announcements')); } /** * Test localized preferred locations endpoint. */ public function test_get_preferred_locations_localized() { // 1. Get English locations $response = $this->getJson('/api/preferred-locations?lang=en'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'preferred_locations' => [ ['code' => 'dubai', 'name' => 'Dubai'], ['code' => 'abu dhabi', 'name' => 'Abu Dhabi'], ['code' => 'oman', 'name' => 'Oman'], ] ] ]); // 2. Get Hindi locations $response = $this->getJson('/api/preferred-locations?lang=hi'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'preferred_locations' => [ ['code' => 'dubai', 'name' => 'दुबई'], ['code' => 'abu dhabi', 'name' => 'अबू धाबी'], ['code' => 'oman', 'name' => 'ओमान'], ] ] ]); // 3. Test pagination $response = $this->getJson('/api/preferred-locations?per_page=1&page=2'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'preferred_locations' => [ ['code' => 'abu dhabi', 'name' => 'Abu Dhabi'], ], 'pagination' => [ 'total' => 3, 'per_page' => 1, 'current_page' => 2, 'last_page' => 3 ] ] ]); } /** * Test localized preferred location areas endpoint. */ public function test_get_preferred_location_areas_localized() { // 1. Get English areas for Dubai (using path parameter) $response = $this->getJson('/api/preferred-locations/dubai/areas?lang=en'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'location' => 'dubai', ] ]); $this->assertGreaterThan(5, count($response->json('data.areas'))); $this->assertEquals('Dubai Marina', $response->json('data.areas.1.name')); // 2. Get Hindi areas for Dubai (using query parameter) $response = $this->getJson('/api/preferred-locations/areas?city=dubai&lang=hi'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'location' => 'dubai', ] ]); $this->assertEquals('दुबई मरीना', $response->json('data.areas.1.name')); // 3. Test pagination $response = $this->getJson('/api/preferred-locations/oman/areas?per_page=2&page=2'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'location' => 'oman', 'pagination' => [ 'total' => 8, 'per_page' => 2, 'current_page' => 2, 'last_page' => 4 ] ] ]); $this->assertCount(2, $response->json('data.areas')); // 4. Test not found location $response = $this->getJson('/api/preferred-locations/sharjah/areas'); $response->assertStatus(404) ->assertJson([ 'success' => false, ]); } /** * Test worker registration fails validation if the phone number already exists. */ public function test_register_validation_fails_if_phone_number_already_exists() { // 1. Create a worker with a specific phone number Worker::create([ 'name' => 'Existing Worker', 'email' => 'existing@example.com', 'phone' => '+971501234567', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'status' => 'active', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'Test worker', ]); // 2. Attempt to register another worker with the same phone number $response = $this->postJson('/api/workers/register', [ 'name' => 'New Worker', 'phone' => '+971501234567', 'salary' => 2000, 'password' => 'secret123', 'language' => 'HI', ]); $response->assertStatus(422) ->assertJson([ 'success' => false, 'message' => 'Validation error.', ]) ->assertJsonValidationErrors(['phone']); $this->assertEquals( 'This mobile number is already registered.', $response->json('errors.phone.0') ); } /** * Test worker registration fails validation if the passport number already exists. */ public function test_register_validation_fails_if_passport_number_already_exists() { // 1. Create a worker and their passport document $worker = Worker::create([ 'name' => 'First Worker', 'email' => 'first@example.com', 'phone' => '+971501111111', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'status' => 'active', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'Test worker', ]); $worker->documents()->create([ 'type' => 'passport', 'number' => 'SQ1234567', ]); // 2. Attempt to register another worker with the same passport number $response = $this->postJson('/api/workers/register', [ 'name' => 'Second Worker', 'phone' => '+971502222222', 'salary' => 2000, 'password' => 'secret123', 'language' => 'HI', 'passport' => [ 'passport_number' => 'SQ1234567', ] ]); $response->assertStatus(422) ->assertJson([ 'success' => false, 'message' => 'Validation error.', ]) ->assertJsonValidationErrors(['passport.passport_number']); $this->assertEquals( 'This passport number is already registered.', $response->json('errors')['passport.passport_number'][0] ); } /** * Test profile update fails if updating a passport document with a different passport number. */ public function test_update_profile_fails_if_passport_number_does_not_match_existing() { $worker = Worker::create([ 'name' => 'Test Worker', 'email' => 'test@example.com', 'phone' => '+971503333333', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'New helper', 'verified' => false, 'status' => 'active', 'api_token' => 'bearer-test-token-passport-mismatch', ]); $worker->documents()->create([ 'type' => 'passport', 'number' => 'ABC123456', 'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01', 'ocr_accuracy' => 95.0, ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer bearer-test-token-passport-mismatch', ])->postJson('/api/workers/profile/update', [ 'passport' => [ 'passport_number' => 'XYZ987654', // Different passport number ] ]); $response->assertStatus(422) ->assertJsonStructure([ 'success', 'message', 'errors' => [ 'passport.passport_number' ] ]); $this->assertEquals( 'The passport number cannot be changed. It must match the previously verified passport number.', $response->json('errors')['passport.passport_number'][0] ); } /** * Test profile update succeeds if passport number matches existing. */ public function test_update_profile_succeeds_if_passport_number_matches_existing() { $worker = Worker::create([ 'name' => 'Test Worker', 'email' => 'test2@example.com', 'phone' => '+971503333334', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'New helper', 'verified' => false, 'status' => 'active', 'api_token' => 'bearer-test-token-passport-match', ]); $worker->documents()->create([ 'type' => 'passport', 'number' => 'ABC123456', 'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01', 'ocr_accuracy' => 95.0, ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer bearer-test-token-passport-match', ])->postJson('/api/workers/profile/update', [ 'passport' => [ 'passport_number' => 'ABC123456', // Same passport number 'date_of_expiry' => '2032-01-01', // Updating expiry date ] ]); $response->assertStatus(200); $this->assertDatabaseHas('worker_documents', [ 'worker_id' => $worker->id, 'type' => 'passport', 'number' => 'ABC123456', 'expiry_date' => '2032-01-01', ]); } /** * Test unique mobile number validation on profile update. */ public function test_update_profile_fails_if_phone_already_exists() { // 1. Create original worker with phone +971504444444 Worker::create([ 'name' => 'Other Worker', 'email' => 'other@example.com', 'phone' => '+971504444444', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'New helper', 'verified' => false, 'status' => 'active', ]); // 2. Create current worker with phone +971505555555 $worker = Worker::create([ 'name' => 'Current Worker', 'email' => 'current@example.com', 'phone' => '+971505555555', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Not Specified', 'bio' => 'New helper', 'verified' => false, 'status' => 'active', 'api_token' => 'bearer-test-token-phone-unique', ]); // 3. Attempt to update current worker's phone to the other worker's phone $response = $this->withHeaders([ 'Authorization' => 'Bearer bearer-test-token-phone-unique', ])->postJson('/api/workers/profile/update', [ 'phone' => '+971504444444', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['phone']); $this->assertEquals( 'This mobile number is already registered.', $response->json('errors')['phone'][0] ); } /** * Test deprecated fields are hidden from JSON response. */ public function test_profile_response_does_not_contain_deprecated_fields() { $worker = Worker::create([ 'name' => 'Test Worker', 'email' => 'test_dep@example.com', 'phone' => '+971503333335', 'language' => 'HI', 'password' => bcrypt('password'), 'nationality' => 'Indian', 'age' => 25, 'salary' => 1500, 'availability' => 'Immediate', 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'Some Bio', 'country' => 'UAE', 'city' => 'Dubai', 'area' => 'JLT', 'verified' => false, 'status' => 'active', 'api_token' => 'bearer-test-token-deprecated-fields', ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer bearer-test-token-deprecated-fields', ])->getJson('/api/workers/profile'); $response->assertStatus(200); $json = $response->json('data.worker'); $this->assertArrayNotHasKey('email', $json); $this->assertArrayNotHasKey('religion', $json); $this->assertArrayNotHasKey('availability', $json); $this->assertArrayNotHasKey('bio', $json); $this->assertArrayNotHasKey('country', $json); $this->assertArrayNotHasKey('city', $json); $this->assertArrayNotHasKey('area', $json); } }