From c91806077adf8970c5ce194b4cfdefb671d82792 Mon Sep 17 00:00:00 2001 From: Nick Sorrell Date: Wed, 19 Feb 2025 20:16:32 -0500 Subject: [PATCH 1/2] Added tests for carriers --- .env.example | 68 --------- .env.testing | 1 + app/Actions/Carriers/UpdateCarrierGeneral.php | 10 +- app/Rules/ValidPhoneNumber.php | 18 +++ config/phones.php | 14 ++ .../Feature/Carriers/CarrierSecurityTest.php | 138 ++++++++++++++++++ tests/Unit/Actions/Carriers/CarrierTest.php | 129 ++++++++++++++++ 7 files changed, 307 insertions(+), 71 deletions(-) delete mode 100644 .env.example create mode 100644 app/Rules/ValidPhoneNumber.php create mode 100644 config/phones.php create mode 100644 tests/Feature/Carriers/CarrierSecurityTest.php create mode 100644 tests/Unit/Actions/Carriers/CarrierTest.php diff --git a/.env.example b/.env.example deleted file mode 100644 index 38b291a..0000000 --- a/.env.example +++ /dev/null @@ -1,68 +0,0 @@ -APP_NAME="LoadPartner TMS" -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_TIMEZONE=UTC -APP_URL=http://localhost - -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US - -APP_MAINTENANCE_DRIVER=file -# APP_MAINTENANCE_STORE=database - -PHP_CLI_SERVER_WORKERS=4 - -BCRYPT_ROUNDS=12 - -LOG_CHANNEL=stack -LOG_STACK=single -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= - -SESSION_DRIVER=database -SESSION_LIFETIME=120 -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null - -BROADCAST_CONNECTION=log -FILESYSTEM_DISK=local -QUEUE_CONNECTION=database - -CACHE_STORE=database -CACHE_PREFIX= - -MEMCACHED_HOST=127.0.0.1 - -REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=log -MAIL_SCHEME=null -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -VITE_APP_NAME="${APP_NAME}" - -FMCSA_API_KEY= \ No newline at end of file diff --git a/.env.testing b/.env.testing index 20f36c6..d9477a7 100644 --- a/.env.testing +++ b/.env.testing @@ -4,6 +4,7 @@ APP_KEY=base64:h2AKCk6aAqx3Vt4NsgnZe8b0jSXLwrq2WO8/OucLqLw= APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost +APP_PORT=8007 APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/app/Actions/Carriers/UpdateCarrierGeneral.php b/app/Actions/Carriers/UpdateCarrierGeneral.php index 311832c..da040d1 100644 --- a/app/Actions/Carriers/UpdateCarrierGeneral.php +++ b/app/Actions/Carriers/UpdateCarrierGeneral.php @@ -4,9 +4,10 @@ use App\Http\Resources\CarrierResource; use App\Models\Carrier; +use App\Rules\ValidPhoneNumber; use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\Concerns\AsAction; - +use Propaganistas\LaravelPhone\PhoneNumber; class UpdateCarrierGeneral { use AsAction; @@ -35,7 +36,10 @@ public function handle( public function asController(ActionRequest $request, Carrier $carrier): Carrier { - + if ($request->has('contact_phone') && $request['contact_phone']) { + $phone = new PhoneNumber($request['contact_phone'], 'US'); + $request->merge(['contact_phone' => $phone->formatE164()]); + } $carrier = $this->handle( carrier: $carrier, name: $request->validated('name'), @@ -67,7 +71,7 @@ public function rules(): array 'dot_number' => ['nullable', 'string'], 'physical_location_id' => ['nullable', 'integer', 'exists:locations,id'], 'contact_email' => ['nullable', 'email', 'max:255'], - 'contact_phone' => ['nullable', 'string', 'max:255', 'phone:US'], + 'contact_phone' => ['nullable', 'string', new ValidPhoneNumber], ]; } } diff --git a/app/Rules/ValidPhoneNumber.php b/app/Rules/ValidPhoneNumber.php new file mode 100644 index 0000000..65fd385 --- /dev/null +++ b/app/Rules/ValidPhoneNumber.php @@ -0,0 +1,18 @@ +countries = explode(',', trim(config('phones.valid_countries'))); + } + + public function message() + { + return 'The :attribute field must be a valid phone number within a supported country (' . implode(', ', $this->countries) .')'; + } +} \ No newline at end of file diff --git a/config/phones.php b/config/phones.php new file mode 100644 index 0000000..76b487c --- /dev/null +++ b/config/phones.php @@ -0,0 +1,14 @@ + env('VALID_PHONE_COUNTRIES', 'US,CA'), +]; \ No newline at end of file diff --git a/tests/Feature/Carriers/CarrierSecurityTest.php b/tests/Feature/Carriers/CarrierSecurityTest.php new file mode 100644 index 0000000..f1d1428 --- /dev/null +++ b/tests/Feature/Carriers/CarrierSecurityTest.php @@ -0,0 +1,138 @@ +setUpOrganization(); + + $this->location = Location::factory()->create(); + + // Set up second organization and user + $this->secondUser = User::factory()->create(); + $this->secondOrg = Organization::create([ + 'name' => 'Second Test Organization', + 'owner_id' => $this->secondUser->id, + ]); + $this->secondUser->organizations()->attach($this->secondOrg); + $this->secondUser->current_organization_id = $this->secondOrg->id; + $this->secondUser->save(); + + // Store organization IDs and initial active user + $this->firstOrgId = $this->organization->id; + $this->secondOrgId = $this->secondOrg->id; + $this->activeUser = $this->user; + + // Helper functions to make tests more readable + $this->switchToFirstOrg = function () { + Context::addHidden('current_organization_id', $this->firstOrgId); + $this->activeUser = $this->user; + }; + + $this->switchToSecondOrg = function () { + Context::addHidden('current_organization_id', $this->secondOrgId); + $this->activeUser = $this->secondUser; + }; + + $this->createTestCarrier = function ($name = 'Test Carrier') { + return CreateCarrier::run(name: $name); + }; +}); + +test('it prevents access to carriers from other organizations', function () { + $carrier = ($this->createTestCarrier)(); + + // Verify first organization access + $response = $this->actingAs($this->activeUser)->get(route('carriers.show', $carrier)); + expect($response->status())->toBe(200); + + // Verify second organization cannot access + ($this->switchToSecondOrg)(); + $response = $this->actingAs($this->activeUser)->get(route('carriers.show', $carrier)); + expect($response->status())->toBe(404); + + // Verify first organization still has access + ($this->switchToFirstOrg)(); + $response = $this->actingAs($this->activeUser)->get(route('carriers.show', $carrier)); + expect($response->status())->toBe(200); +}); + +test('it prevents updating carriers from other organizations', function () { + // Create carrier in first organization + $response = $this->actingAs($this->activeUser) + ->withHeaders(['Accept' => 'application/json']) + ->post(route('carriers.store'), [ + 'name' => 'Test Carrier', + ]); + expect($response->status())->toBe(201); + + $carrier = Carrier::findOrFail($response->json('id')); + $updateData = [ + 'name' => 'Updated Name', + 'mc_number' => 'MC123456', + 'dot_number' => 'DOT789012', + 'physical_location_id' => $this->location->id, + 'contact_email' => 'contact@carrier.com', + 'contact_phone' => '(513) 731-4567', + ]; + + // Test update with second organization + ($this->switchToSecondOrg)(); + $response = $this->actingAs($this->activeUser)->put(route('carriers.update', $carrier), $updateData); + expect($response->status())->toBe(404); + + // Verify first organization can still update + ($this->switchToFirstOrg)(); + $response = $this->actingAs($this->activeUser)->put(route('carriers.update', $carrier), $updateData); + expect($response->status())->toBe(200); +}); + +test('it prevents deleting carriers from other organizations', function () { + $carrier = ($this->createTestCarrier)(); + + // Attempt delete from second organization + ($this->switchToSecondOrg)(); + $response = $this->actingAs($this->activeUser)->delete(route('carriers.destroy', $carrier)); + expect($response->status())->toBe(404); + + // Verify carrier still exists + ($this->switchToFirstOrg)(); + expect(Carrier::find($carrier->id))->not->toBeNull(); + + // Verify first organization can delete + $response = $this->actingAs($this->activeUser)->delete(route('carriers.destroy', $carrier)); + expect($response->status())->toBe(200); +}); + +test('it prevents listing carriers from other organizations', function () { + // Create carriers in first organization + ($this->createTestCarrier)('First Org Carrier 1'); + ($this->createTestCarrier)('First Org Carrier 2'); + + // Create carrier in second organization + ($this->switchToSecondOrg)(); + ($this->createTestCarrier)('Second Org Carrier'); + + // Verify second organization only sees its carrier + $response = $this->actingAs($this->activeUser)->get(route('carriers.search')); + $carriers = $response->json(); + expect(count($carriers))->toBe(1); + expect($carriers[0]['name'])->toBe('Second Org Carrier'); + + // Verify first organization only sees its carriers + ($this->switchToFirstOrg)(); + $response = $this->actingAs($this->activeUser)->get(route('carriers.search')); + $carriers = $response->json(); + expect(count($carriers))->toBe(2); + $names = array_column($carriers, 'name'); + expect($names)->toContain('First Org Carrier 1'); + expect($names)->toContain('First Org Carrier 2'); +}); + diff --git a/tests/Unit/Actions/Carriers/CarrierTest.php b/tests/Unit/Actions/Carriers/CarrierTest.php new file mode 100644 index 0000000..183c5b4 --- /dev/null +++ b/tests/Unit/Actions/Carriers/CarrierTest.php @@ -0,0 +1,129 @@ +location = Location::factory()->create(); +}); + +test('it creates basic carrier with required fields', function () { + // Act + $carrier = CreateCarrier::run( + name: 'Test Carrier', + ); + + // Assert + expect($carrier)->toBeInstanceOf(Carrier::class); + + expect(DB::table('carriers')->where([ + 'id' => $carrier->id, + 'name' => 'Test Carrier', + ])->exists())->toBeTrue(); +}); + +test('it creates carrier with all optional fields', function () { + // Act + $carrier = CreateCarrier::run( + name: 'Full Details Carrier', + ); + + // Act + $updatedCarrier = UpdateCarrierGeneral::run( + carrier: $carrier, + name: 'Updated Carrier Name', + mc_number: 'MC123456', + dot_number: 'DOT789012', + physical_location_id: $this->location->id, + contact_email: 'contact@carrier.com', + contact_phone: '(555) 123-4567', + ); + + // Assert + expect($updatedCarrier)->toBeInstanceOf(Carrier::class); + + expect(DB::table('carriers')->where([ + 'id' => $updatedCarrier->id, + 'name' => 'Updated Carrier Name', + 'mc_number' => 'MC123456', + 'dot_number' => 'DOT789012', + 'physical_location_id' => $this->location->id, + 'contact_email' => 'contact@carrier.com', + 'contact_phone' => '(555) 123-4567', + ])->exists())->toBeTrue(); +}); + +test('it updates carrier general details partially', function () { + // Create initial carrier + $carrier = CreateCarrier::run( + name: 'Initial Carrier', + ); + + // Act - update only some fields + $updatedCarrier = UpdateCarrierGeneral::run( + carrier: $carrier, + name: 'New Name', + mc_number: 'MC999999', + ); + + // Assert + expect(DB::table('carriers')->where([ + 'id' => $carrier->id, + 'name' => 'New Name', + 'mc_number' => 'MC999999', + ])->exists())->toBeTrue(); + + // Verify other fields remain null + expect($updatedCarrier->dot_number)->toBeNull(); + expect($updatedCarrier->physical_location_id)->toBeNull(); + expect($updatedCarrier->contact_email)->toBeNull(); + expect($updatedCarrier->contact_phone)->toBeNull(); +}); + +test('it updates carrier contact information', function () { + // Create initial carrier + $carrier = CreateCarrier::run( + name: 'Contact Test Carrier', + ); + + // Act + $updatedCarrier = UpdateCarrierGeneral::run( + carrier: $carrier, + contact_email: 'new@carrier.com', + contact_phone: '(555) 999-8888', + ); + + // Assert + expect(DB::table('carriers')->where([ + 'id' => $carrier->id, + 'contact_email' => 'new@carrier.com', + 'contact_phone' => '(555) 999-8888', + ])->exists())->toBeTrue(); +}); + +test('it updates carrier location', function () { + // Create initial carrier + $carrier = CreateCarrier::run( + name: 'Location Test Carrier', + ); + + $newLocation = Location::factory()->create(); + + // Act + $updatedCarrier = UpdateCarrierGeneral::run( + carrier: $carrier, + physical_location_id: $newLocation->id, + ); + + // Assert + expect($updatedCarrier->physical_location_id)->toBe($newLocation->id); + expect(DB::table('carriers')->where([ + 'id' => $carrier->id, + 'physical_location_id' => $newLocation->id, + ])->exists())->toBeTrue(); +}); \ No newline at end of file From 4e828d72029f480721de53269b6027c34ba4e48a Mon Sep 17 00:00:00 2001 From: Nick Sorrell Date: Wed, 19 Feb 2025 20:18:40 -0500 Subject: [PATCH 2/2] Adding back env.example --- .env.example | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ .env.testing | 1 - 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4cca8d5 --- /dev/null +++ b/.env.example @@ -0,0 +1,68 @@ +APP_NAME="LoadPartner TMS" +APP_ENV=local +APP_KEY=base64:amHoMcOUBHwj3/2KKnwNev0hU3EDy22r5+rv57O37KI= +APP_DEBUG=true +APP_TIMEZONE=UTC +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" + +FMCSA_API_KEY= \ No newline at end of file diff --git a/.env.testing b/.env.testing index d9477a7..20f36c6 100644 --- a/.env.testing +++ b/.env.testing @@ -4,7 +4,6 @@ APP_KEY=base64:h2AKCk6aAqx3Vt4NsgnZe8b0jSXLwrq2WO8/OucLqLw= APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost -APP_PORT=8007 APP_LOCALE=en APP_FALLBACK_LOCALE=en