From 691e1e48e31bddeac894833a82dfd60343962a43 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Fri, 8 Nov 2024 14:41:43 -0500 Subject: [PATCH 01/10] First initial tests for Laravel API backend --- .github/workflows/backend_test.yml | 43 +++++++ .../factories/ShoppingListFactory.php | 34 +++++ API/database/factories/UserFactory.php | 37 ++---- API/phpunit.xml | 2 +- API/tests/Feature/ExampleTest.php | 19 --- .../Feature/ShoppingListControllerTest.php | 118 ++++++++++++++++++ API/tests/Unit/ExampleTest.php | 16 --- 7 files changed, 203 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/backend_test.yml create mode 100644 API/database/factories/ShoppingListFactory.php delete mode 100644 API/tests/Feature/ExampleTest.php create mode 100644 API/tests/Feature/ShoppingListControllerTest.php delete mode 100644 API/tests/Unit/ExampleTest.php diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml new file mode 100644 index 0000000..45fb400 --- /dev/null +++ b/.github/workflows/backend_test.yml @@ -0,0 +1,43 @@ +name: Run Laravel API Unit Tests + +on: + push: + paths: + - 'API/**' + pull_request: + paths: + - 'API/**' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' # the PHP version this project requires + extensions: mbstring, pdo_sqlite + ini-values: | + memory_limit=-1 + coverage: none + + - name: Install dependencies + run: | + composer install --prefer-dist --no-interaction + cp .env.example .env + + - name: Configure environment for SQLite in-memory database + run: | + echo "DB_CONNECTION=sqlite" >> .env + echo "DB_DATABASE=:memory:" >> .env + php artisan key:generate + + - name: Run migrations + run: php artisan migrate --force + + - name: Run tests + run: php artisan test --env=testing diff --git a/API/database/factories/ShoppingListFactory.php b/API/database/factories/ShoppingListFactory.php new file mode 100644 index 0000000..8e43c17 --- /dev/null +++ b/API/database/factories/ShoppingListFactory.php @@ -0,0 +1,34 @@ +first(); + + return [ + 'name' => $this->faker->word(), // Random name for the shopping list + 'user_id' => $user ? $user->user_id : null, // Assign a user_id from an existing user + 'route_id' => null, // Set to null if you don't want to assign a route by default + ]; + } +} diff --git a/API/database/factories/UserFactory.php b/API/database/factories/UserFactory.php index 584104c..6ce9df8 100644 --- a/API/database/factories/UserFactory.php +++ b/API/database/factories/UserFactory.php @@ -3,42 +3,19 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; -/** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> - */ +use App\Models\User; + class UserFactory extends Factory { - /** - * The current password being used by the factory. - */ - protected static ?string $password; + protected $model = User::class; - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array + public function definition() { return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), + 'user_id' => $this->faker->uuid(), + 'username' => $this->faker->userName(), + // Add any other necessary fields here ]; } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } } diff --git a/API/phpunit.xml b/API/phpunit.xml index 506b9a3..60b9b16 100644 --- a/API/phpunit.xml +++ b/API/phpunit.xml @@ -23,7 +23,7 @@ - + diff --git a/API/tests/Feature/ExampleTest.php b/API/tests/Feature/ExampleTest.php deleted file mode 100644 index 8364a84..0000000 --- a/API/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} diff --git a/API/tests/Feature/ShoppingListControllerTest.php b/API/tests/Feature/ShoppingListControllerTest.php new file mode 100644 index 0000000..1ec7969 --- /dev/null +++ b/API/tests/Feature/ShoppingListControllerTest.php @@ -0,0 +1,118 @@ +create(); + + // Mock the login by setting a cookie + $response = $this->actingAs($user); + + // Make the post request to create a shopping list + $response = $this->postJson('/shopping-lists', [ + 'name' => 'Test Shopping List', + ]); + + // Assert the response is successful (HTTP 201 created) + $response->assertStatus(201); + + // Assert the response contains the shopping list name + $response->assertJsonFragment([ + 'name' => 'Test Shopping List', + ]); + + // Ensure the shopping list is stored in the database + $this->assertDatabaseHas('shopping_lists', [ + 'name' => 'Test Shopping List', + 'user_id' => $user->user_id, // Make sure the user_id matches + ]); + } + + + public function testGetUserShoppingLists() + { + // Create an authenticated user and a shopping list + $user = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + ]); + + // Mock the login by setting a cookie + $response = $this->actingAs($user); + + // Make the get request to fetch shopping lists + $response = $this->getJson('/shopping-lists'); + + // Assert the response is successful (HTTP 200 OK) + $response->assertStatus(200); + + // Assert the shopping list is in the response + $response->assertJsonFragment([ + 'name' => $list->name, + ]); + } + + public function testGetSpecificShoppingList() + { + // Create an authenticated user and a shopping list + $user = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + ]); + + // Mock the login by setting a cookie + $response = $this->actingAs($user); + + // Make the get request to fetch a specific shopping list by ID + $response = $this->getJson('/shopping-lists/' . $list->list_id); + + // Assert the response is successful (HTTP 200 OK) + $response->assertStatus(200); + + // Assert the specific shopping list is in the response + $response->assertJsonFragment([ + 'name' => $list->name, + ]); + } + + + + public function testDeleteShoppingList() + { + // Create an authenticated user and a shopping list + $user = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + ]); + + // Mock the login by setting a cookie + $response = $this->actingAs($user); + + // Make the delete request to remove the shopping list + $response = $this->deleteJson('/shopping-lists/' . $list->list_id); + + // Assert the response is successful (HTTP 200 OK) + $response->assertStatus(200); + + // Ensure the shopping list is deleted from the database + $this->assertDatabaseMissing('shopping_lists', [ + 'id' => $list->list_id, + ]); + } + +} diff --git a/API/tests/Unit/ExampleTest.php b/API/tests/Unit/ExampleTest.php deleted file mode 100644 index 5773b0c..0000000 --- a/API/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(true); - } -} From 19be9e473a8261d292ae37556bf38442dad510f8 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Sun, 10 Nov 2024 20:21:29 -0500 Subject: [PATCH 02/10] Make initial working Controller tests and adjust setup for proper automated testing on GitHub Actions --- .github/workflows/backend_test.yml | 1 + API/{.env.example => .env.test} | 2 +- .../Controllers/Api/GroceryItemController.php | 31 ++-- .../Api/ShoppingListController.php | 17 +- API/database/factories/GroceryItemFactory.php | 35 ++++ .../Feature/GroceryItemControllerTest.php | 158 ++++++++++++++++++ .../Feature/ShoppingListControllerTest.php | 51 ++++++ 7 files changed, 265 insertions(+), 30 deletions(-) rename API/{.env.example => .env.test} (97%) create mode 100644 API/database/factories/GroceryItemFactory.php create mode 100644 API/tests/Feature/GroceryItemControllerTest.php diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index 45fb400..9657613 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -27,6 +27,7 @@ jobs: - name: Install dependencies run: | + cd API composer install --prefer-dist --no-interaction cp .env.example .env diff --git a/API/.env.example b/API/.env.test similarity index 97% rename from API/.env.example rename to API/.env.test index 7b49625..902505c 100644 --- a/API/.env.example +++ b/API/.env.test @@ -19,7 +19,7 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite +# DB_CONNECTION=sqlite # DB_HOST=127.0.0.1 # DB_PORT=3306 # DB_DATABASE=laravel diff --git a/API/app/Http/Controllers/Api/GroceryItemController.php b/API/app/Http/Controllers/Api/GroceryItemController.php index 5516a1f..a12f769 100644 --- a/API/app/Http/Controllers/Api/GroceryItemController.php +++ b/API/app/Http/Controllers/Api/GroceryItemController.php @@ -99,27 +99,22 @@ public function update(Request $request, $id) public function destroy(Request $request, $id) { - try { - $groceryItem = GroceryItem::findOrFail($id); - - $shoppingList = ShoppingList::findOrFail($groceryItem->shopping_list_id); + $groceryItem = GroceryItem::findOrFail($id); + + $shoppingList = ShoppingList::findOrFail($groceryItem->shopping_list_id); - // This will automatically call the `update` method in the ShoppingListPolicy - $this->authorize('update', $shoppingList); // Throws a 403 if not authorized + // This will automatically call the `update` method in the ShoppingListPolicy + $this->authorize('update', $shoppingList); // Throws a 403 if not authorized - Log::info("Deleting item: " . print_r($groceryItem, true)); - $groceryItem->delete(); + Log::info("Deleting item: " . print_r($groceryItem, true)); + $groceryItem->delete(); - // Also update shopping list name "updated_at" field (users should know the shopping list has been - // modified without having to see the individual items; this is done via this functionality and shown - // for the Dashboard front end component list items) - $shoppingList->updated_at = now(); // Update the timestamp - $shoppingList->save(); + // Also update shopping list name "updated_at" field (users should know the shopping list has been + // modified without having to see the individual items; this is done via this functionality and shown + // for the Dashboard front end component list items) + $shoppingList->updated_at = now(); // Update the timestamp + $shoppingList->save(); - return response()->json(['message' => 'Grocery item deleted successfully'], 200); - } catch (\Exception $e) { - Log::error('Error deleting grocery item: ' . $e->getMessage()); - return response()->json(['error' => 'Could not delete grocery item'], 500); - } + return response()->json(['message' => 'Grocery item deleted successfully'], 200); } } diff --git a/API/app/Http/Controllers/Api/ShoppingListController.php b/API/app/Http/Controllers/Api/ShoppingListController.php index f976849..f704dad 100644 --- a/API/app/Http/Controllers/Api/ShoppingListController.php +++ b/API/app/Http/Controllers/Api/ShoppingListController.php @@ -157,18 +157,13 @@ public function update(Request $request, $id) public function destroy(Request $request, $id) { - try { - $shoppingList = ShoppingList::findOrFail($id); + $shoppingList = ShoppingList::findOrFail($id); - // This will automatically call the `delete` method in the ShoppingListPolicy - $this->authorize('delete', $shoppingList); // Throws a 403 if not authorized - - $shoppingList->delete(); + // This will automatically call the `delete` method in the ShoppingListPolicy + $this->authorize('delete', $shoppingList); // Throws a 403 if not authorized + + $shoppingList->delete(); - return response()->json(['message' => 'Shopping list deleted successfully'], 200); - } catch (\Exception $e) { - Log::error('Error deleting shopping list: ' . $e->getMessage()); - return response()->json(['error' => 'Could not delete shopping list'], 500); - } + return response()->json(['message' => 'Shopping list deleted successfully'], 200); } } diff --git a/API/database/factories/GroceryItemFactory.php b/API/database/factories/GroceryItemFactory.php new file mode 100644 index 0000000..cdcf432 --- /dev/null +++ b/API/database/factories/GroceryItemFactory.php @@ -0,0 +1,35 @@ +first(); + $list = ShoppingList::inRandomOrder()->first(); + return [ + 'name' => $this->faker->word(), // Random name for the shopping list + 'is_food' => false, + 'shopping_list_id' => $list->list_id, // Set to null if you don't want to assign a route by default + ]; + } +} diff --git a/API/tests/Feature/GroceryItemControllerTest.php b/API/tests/Feature/GroceryItemControllerTest.php new file mode 100644 index 0000000..ccac5d4 --- /dev/null +++ b/API/tests/Feature/GroceryItemControllerTest.php @@ -0,0 +1,158 @@ +create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $user->user_id]); + + $this->actingAs($user) + ->postJson('/grocery-items', [ + 'name' => 'Apples', + 'quantity' => 5, + 'is_food' => true, + 'shopping_list_id' => $shoppingList->list_id, + ]) + ->assertStatus(201) + ->assertJsonFragment(['name' => 'Apples', 'quantity' => 5, 'is_food' => true]); + + $this->assertDatabaseHas('grocery_items', [ + 'name' => 'Apples', + 'quantity' => 5, + 'is_food' => true, + 'shopping_list_id' => $shoppingList->list_id, + ]); + } + + public function testStoreGroceryItemUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $otherUser->user_id]); + + $this->actingAs($user) + ->postJson('/grocery-items', [ + 'name' => 'Bananas', + 'quantity' => 10, + 'is_food' => true, + 'shopping_list_id' => $shoppingList->list_id, + ]) + ->assertStatus(403); + } + + public function testShowGroceryItems() + { + $user = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $user->user_id]); + $groceryItem = GroceryItem::factory()->create([ + 'name' => 'Oranges', + 'quantity' => 3, + 'is_food' => true, + 'shopping_list_id' => $shoppingList->list_id, + ]); + + $response = $this->actingAs($user)->getJson('/grocery-items/' . $shoppingList->list_id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'name' => 'Oranges', + 'quantity' => 3, + 'is_food' => 1, + ]); + } + + public function testShowGroceryItemsUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $otherUser->user_id]); + + $this->actingAs($user) + ->getJson('/grocery-items/' . $shoppingList->list_id) + ->assertStatus(403); + } + + public function testUpdateGroceryItem() + { + $user = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $user->user_id]); + $groceryItem = GroceryItem::factory()->create([ + 'name' => 'Milk', + 'quantity' => 1, + 'is_food' => true, + 'shopping_list_id' => $shoppingList->list_id, + ]); + + $this->actingAs($user) + ->putJson('/grocery-items/' . $groceryItem->item_id, [ + 'name' => 'Almond Milk', + 'quantity' => 2, + 'is_food' => true, + ]) + ->assertStatus(200) + ->assertJsonFragment(['name' => 'Almond Milk', 'quantity' => 2, 'is_food' => true]); + + $this->assertDatabaseHas('grocery_items', [ + 'item_id' => $groceryItem->item_id, + 'name' => 'Almond Milk', + 'quantity' => 2, + ]); + } + + public function testUpdateGroceryItemUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $otherUser->user_id]); + $groceryItem = GroceryItem::factory()->create([ + 'shopping_list_id' => $shoppingList->list_id, + 'name' => 'Juice', + ]); + + $this->actingAs($user) + ->putJson('/grocery-items/' . $groceryItem->item_id, [ + 'name' => 'Orange Juice', + 'quantity' => 2, + 'is_food' => true, + ]) + ->assertStatus(403); + } + + public function testDeleteGroceryItem() + { + $user = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $user->user_id]); + $groceryItem = GroceryItem::factory()->create(['shopping_list_id' => $shoppingList->list_id]); + + $this->actingAs($user) + ->deleteJson('/grocery-items/' . $groceryItem->item_id) + ->assertStatus(200) + ->assertJsonFragment(['message' => 'Grocery item deleted successfully']); + + $this->assertDatabaseMissing('grocery_items', ['item_id' => $groceryItem->item_id]); + } + + public function testDeleteGroceryItemUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $shoppingList = ShoppingList::factory()->create(['user_id' => $otherUser->user_id, 'name' => 'foo']); + $groceryItem = GroceryItem::factory()->create(['shopping_list_id' => $shoppingList->list_id]); + + $this->actingAs($user) + ->deleteJson('/grocery-items/' . $groceryItem->item_id) + ->assertStatus(403); + } +} diff --git a/API/tests/Feature/ShoppingListControllerTest.php b/API/tests/Feature/ShoppingListControllerTest.php index 1ec7969..ad1a00c 100644 --- a/API/tests/Feature/ShoppingListControllerTest.php +++ b/API/tests/Feature/ShoppingListControllerTest.php @@ -90,7 +90,47 @@ public function testGetSpecificShoppingList() ]); } + public function testGetSpecificShoppingListUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create(['user_id' => $otherUser->user_id]); + + $this->actingAs($user) + ->getJson('/shopping-lists/' . $list->list_id) + ->assertStatus(403); + } + + public function testUpdateShoppingList() + { + $user = User::factory()->create(); + $list = ShoppingList::factory()->create(['user_id' => $user->user_id, 'name' => 'foo']); + + $this->actingAs($user) + ->putJson('/shopping-lists/' . $list->list_id, [ + 'name' => 'foobar', + ]) + ->assertStatus(200) + ->assertJsonFragment(['name' => 'foobar']); + + $this->assertDatabaseHas('shopping_lists', [ + 'list_id' => $list->list_id, + 'name' => 'foobar' + ]); + } + public function testUpdateShoppingListUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create(['user_id' => $otherUser->user_id, 'name' => 'foo']); + + $this->actingAs($user) + ->putJson('/shopping-lists/' . $list->list_id, [ + 'name' => 'foo2', + ]) + ->assertStatus(403); + } public function testDeleteShoppingList() { @@ -115,4 +155,15 @@ public function testDeleteShoppingList() ]); } + public function testDeleteShoppingListUnauthorized() + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create(['user_id' => $otherUser->user_id, 'name' => 'foo']); + + $this->actingAs($user) + ->deleteJson('/shopping-lists/' . $list->list_id) + ->assertStatus(403); + } + } From 5933762b4df5b500cf5abad6199fc53cd2af8e97 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Sun, 10 Nov 2024 20:31:40 -0500 Subject: [PATCH 03/10] Modify event trigger for Issue #66 implementation --- .github/workflows/backend_test.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index 9657613..8f0c34f 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -1,10 +1,9 @@ -name: Run Laravel API Unit Tests +name: Run Laravel API Feature Tests on: - push: - paths: - - 'API/**' pull_request: + branches: + - development paths: - 'API/**' From 7470bda1ce4bd773735109f9a6cc76f30b15c570 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Sun, 10 Nov 2024 20:34:47 -0500 Subject: [PATCH 04/10] Fix name change of env test file --- .github/workflows/backend_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index 8f0c34f..e9a8912 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -28,7 +28,7 @@ jobs: run: | cd API composer install --prefer-dist --no-interaction - cp .env.example .env + cp .env.test .env - name: Configure environment for SQLite in-memory database run: | From beebc07c264fa3d1d8fa9b3de880f2e854744128 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Sun, 10 Nov 2024 20:36:57 -0500 Subject: [PATCH 05/10] Try bug fix for workflow file test cd command --- .github/workflows/backend_test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index e9a8912..b277faf 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -32,12 +32,17 @@ jobs: - name: Configure environment for SQLite in-memory database run: | + cd API echo "DB_CONNECTION=sqlite" >> .env echo "DB_DATABASE=:memory:" >> .env php artisan key:generate - name: Run migrations - run: php artisan migrate --force + run: | + cd API + php artisan migrate --force - name: Run tests - run: php artisan test --env=testing + run: | + cd API + php artisan test --env=testing From f137411a3cb7a68029a5eea2c910dca770276245 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Sun, 10 Nov 2024 20:38:27 -0500 Subject: [PATCH 06/10] Remove unnecessary flag for php artisan test command in Laravel testing workflow file --- .github/workflows/backend_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index b277faf..de4bea3 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -45,4 +45,4 @@ jobs: - name: Run tests run: | cd API - php artisan test --env=testing + php artisan test From 50671fa855d5d947212460ee64106d2bc65ac7ec Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Sun, 10 Nov 2024 20:40:49 -0500 Subject: [PATCH 07/10] Make backend tests workflow file only execute feature tests --- .github/workflows/backend_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index de4bea3..e38875b 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -45,4 +45,4 @@ jobs: - name: Run tests run: | cd API - php artisan test + php artisan test --testsuite=Feature From ed9b980dfdf834d067344a7a3aa64a0ff14e50ec Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Tue, 12 Nov 2024 11:24:33 -0500 Subject: [PATCH 08/10] Make ListPermissionsController tests and fix security vulnerability in createShareLink endpoint --- .../Api/ShoppingListController.php | 2 - .../GoogleAuthenticationController.php | 5 -- .../Controllers/ListPermissionsController.php | 9 ++- API/app/Providers/AppServiceProvider.php | 7 ++ API/database/factories/SharedLinkFactory.php | 26 ++++++++ .../Feature/ListPermissionsControllerTest.php | 64 +++++++++++++++++++ 6 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 API/database/factories/SharedLinkFactory.php create mode 100644 API/tests/Feature/ListPermissionsControllerTest.php diff --git a/API/app/Http/Controllers/Api/ShoppingListController.php b/API/app/Http/Controllers/Api/ShoppingListController.php index f704dad..63a7d30 100644 --- a/API/app/Http/Controllers/Api/ShoppingListController.php +++ b/API/app/Http/Controllers/Api/ShoppingListController.php @@ -17,8 +17,6 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -define('DEBUG_MODE', 0); - class ShoppingListController extends Controller { diff --git a/API/app/Http/Controllers/GoogleAuthenticationController.php b/API/app/Http/Controllers/GoogleAuthenticationController.php index 4949d54..d915691 100644 --- a/API/app/Http/Controllers/GoogleAuthenticationController.php +++ b/API/app/Http/Controllers/GoogleAuthenticationController.php @@ -14,11 +14,6 @@ use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; - -define('DEBUG_MODE', 0); -// Fetch the client ID from the environment variable -define('GOOGLE_CLIENT_ID', env('GOOGLE_CLIENT_ID')); - class GoogleAuthenticationController extends Controller { /** diff --git a/API/app/Http/Controllers/ListPermissionsController.php b/API/app/Http/Controllers/ListPermissionsController.php index 45c91e3..2ce97b9 100644 --- a/API/app/Http/Controllers/ListPermissionsController.php +++ b/API/app/Http/Controllers/ListPermissionsController.php @@ -12,8 +12,6 @@ use Illuminate\Support\Str; use Illuminate\Support\Facades\Auth; -define('DEBUG_MODE', 0); - class ListPermissionsController extends BaseController { @@ -38,6 +36,13 @@ public function createShareLink($id, Request $request) { $shoppingList = ShoppingList::findOrFail($id); Log::info("Shopping list id: " . $shoppingList->list_id . " and we started with id: " . $id); + + // Grab user from sanctum + $user = Auth::user(); + + if ($shoppingList->user_id != $user->user_id) { + abort(403); + } $maxRetries = 10; // Maximum number of retries $retryCount = 0; diff --git a/API/app/Providers/AppServiceProvider.php b/API/app/Providers/AppServiceProvider.php index 3ec0f61..ec4fdc6 100644 --- a/API/app/Providers/AppServiceProvider.php +++ b/API/app/Providers/AppServiceProvider.php @@ -28,5 +28,12 @@ public function register(): void public function boot(): void { // + if (!defined('DEBUG_MODE')) { + define('DEBUG_MODE', 1); + } + if (!defined('GOOGLE_CLIENT_ID')) { + // Fetch the client ID from the environment variable + define('GOOGLE_CLIENT_ID', env('GOOGLE_CLIENT_ID')); + } } } diff --git a/API/database/factories/SharedLinkFactory.php b/API/database/factories/SharedLinkFactory.php new file mode 100644 index 0000000..6bed18e --- /dev/null +++ b/API/database/factories/SharedLinkFactory.php @@ -0,0 +1,26 @@ + Str::random(32), + 'expires_at' => Carbon::now()->addDays(7), + 'can_update' => false, + 'can_delete' => false, + 'shopping_list_id' => function () { + return \App\Models\ShoppingList::factory()->create()->list_id; + } + ]; + } +} \ No newline at end of file diff --git a/API/tests/Feature/ListPermissionsControllerTest.php b/API/tests/Feature/ListPermissionsControllerTest.php new file mode 100644 index 0000000..119e327 --- /dev/null +++ b/API/tests/Feature/ListPermissionsControllerTest.php @@ -0,0 +1,64 @@ +create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + 'name' => 'Test Shopping List' + ]); + + $this->actingAs($user); + + $response = $this->postJson('/share/' . $list->list_id); + + $response->assertStatus(200); + + // Verify the database entry with all required fields + $this->assertDatabaseHas('shared_links', [ + 'shopping_list_id' => $list->list_id, + ]); + + // Verify that all required fields are present + $sharedLink = \DB::table('shared_links') + ->where('shopping_list_id', $list->list_id) + ->first(); + + $this->assertNotNull($sharedLink); + $this->assertNotNull($sharedLink->token); + $this->assertNotNull($sharedLink->expires_at); + } + + public function testUnauthorizedUserCannotCreateShareLink(): void + { + $owner = User::factory()->create(); + $unauthorizedUser = User::factory()->create(); + + $list = ShoppingList::factory()->create([ + 'user_id' => $owner->user_id + ]); + + $this->actingAs($unauthorizedUser); + + $response = $this->postJson('/share/' . $list->list_id); + $response->assertStatus(403); + } +} \ No newline at end of file From 607c176f0d61b33d6979a42f878f1dc9957b8a5f Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Tue, 12 Nov 2024 20:43:52 -0500 Subject: [PATCH 09/10] Make comment changes for automated tests PR --- API/database/factories/GroceryItemFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/API/database/factories/GroceryItemFactory.php b/API/database/factories/GroceryItemFactory.php index cdcf432..4741bda 100644 --- a/API/database/factories/GroceryItemFactory.php +++ b/API/database/factories/GroceryItemFactory.php @@ -27,9 +27,9 @@ public function definition() $user = User::inRandomOrder()->first(); $list = ShoppingList::inRandomOrder()->first(); return [ - 'name' => $this->faker->word(), // Random name for the shopping list + 'name' => $this->faker->word(), // Random name for the grocery item 'is_food' => false, - 'shopping_list_id' => $list->list_id, // Set to null if you don't want to assign a route by default + 'shopping_list_id' => $list->list_id, ]; } } From ebefb2d820564f9200fc77a0b9fc8598dc447419 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Fri, 15 Nov 2024 13:33:42 -0500 Subject: [PATCH 10/10] Add remaining test changes for PR review --- API/.env.test | 1 + API/app/Models/SharedLink.php | 3 + API/database/factories/SharedLinkFactory.php | 2 +- .../Feature/ListPermissionsControllerTest.php | 215 +++++++++++++++++- 4 files changed, 219 insertions(+), 2 deletions(-) diff --git a/API/.env.test b/API/.env.test index 902505c..0152d6d 100644 --- a/API/.env.test +++ b/API/.env.test @@ -4,6 +4,7 @@ APP_KEY= APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost +FRONTEND_BASE_URL=www.speedcartapp.com APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/API/app/Models/SharedLink.php b/API/app/Models/SharedLink.php index f037dc1..96055d0 100644 --- a/API/app/Models/SharedLink.php +++ b/API/app/Models/SharedLink.php @@ -2,9 +2,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Factories\HasFactory; class SharedLink extends Model { + use HasFactory; + protected $table = 'shared_links'; // Fillable properties, allowing mass assignment diff --git a/API/database/factories/SharedLinkFactory.php b/API/database/factories/SharedLinkFactory.php index 6bed18e..fd78630 100644 --- a/API/database/factories/SharedLinkFactory.php +++ b/API/database/factories/SharedLinkFactory.php @@ -14,7 +14,7 @@ class SharedLinkFactory extends Factory public function definition() { return [ - 'token' => Str::random(32), + 'token' => (string) Str::uuid(), 'expires_at' => Carbon::now()->addDays(7), 'can_update' => false, 'can_delete' => false, diff --git a/API/tests/Feature/ListPermissionsControllerTest.php b/API/tests/Feature/ListPermissionsControllerTest.php index 119e327..c3daec3 100644 --- a/API/tests/Feature/ListPermissionsControllerTest.php +++ b/API/tests/Feature/ListPermissionsControllerTest.php @@ -4,6 +4,8 @@ use App\Models\User; use App\Models\ShoppingList; +use App\Models\GroceryItem; +use App\Models\SharedLink; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use Illuminate\Support\Str; @@ -47,6 +49,209 @@ public function testCreateShareLink(): void $this->assertNotNull($sharedLink->expires_at); } + public function testVerifyShareLinkValid(): void + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + 'name' => 'Test Shopping List' + ]); + $shareLink = SharedLink::factory()->create([ + 'shopping_list_id' => $list->list_id + ]); + + $response = $this->actingAs($otherUser); + + $response = $this->getJson('/share/' . $shareLink->token); + + $response->assertStatus(201); + + // Ensure chosen permissions (default from factory) were properly set + $this->assertDatabaseHas('shared_shopping_list_perms', [ + 'shopping_list_id' => $list->list_id, + 'user_id' => $otherUser->user_id, + 'can_update' => 0, + 'can_delete' => 0 + ]); + } + + public function testCreateShareLinkChainUnauthorized() { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + 'name' => 'Test Shopping List' + ]); + $shareLink = SharedLink::factory()->create([ + 'shopping_list_id' => $list->list_id + ]); + + $response = $this->actingAs($otherUser); + + // Verify the first link with otherUser to give them permissions + $response = $this->getJson('/share/' . $shareLink->token); + + // Now try creating another share link as otherUser + + $secondLinkCreationResponse = $this->actingAs($otherUser); + + $secondLinkCreationResponse = $this->postJson('/share/' . $list->list_id); + + $secondLinkCreationResponse->assertStatus(403); + + // Ensure chosen permissions (default from factory) were NOT properly set + $this->assertDatabaseMissing('shared_links', [ + 'shopping_list_id' => $list->list_id, + 'can_update' => 0, + 'can_delete' => 0 + ]); + } + + public function testVerifyShareLinkUpdateAllowedButNotDelete() { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + 'name' => 'Test Shopping List' + ]); + $shareLink = SharedLink::factory()->create([ + 'shopping_list_id' => $list->list_id, + 'can_update' => 1 + ]); + + $response = $this->actingAs($otherUser); + + // Verify the first link with otherUser to give them permissions + $response = $this->getJson('/share/' . $shareLink->token); + + // Now try CRUD operations with Controllers but DELETE should fail + + // Title update + $titleUpdateResponse = $this->actingAs($otherUser); + $titleUpdateResponse = $this->putJson('/shopping-lists/' . $list->list_id, [ + 'name' => 'foo2', + ]); + $titleUpdateResponse->assertStatus(200); + + // Item creation + $listItemCreationResponse = $this->actingAs($otherUser); + $listItemCreationResponse = $this->postJson('/grocery-items', [ + 'name' => 'Bananas', + 'quantity' => 10, + 'is_food' => true, + 'shopping_list_id' => $list->list_id, + ]); + + $listItemCreationResponse->assertStatus(201); + $listItemCreationResponseJson = $listItemCreationResponse->json(); + + // Delete an item (fabricate via Factory) + // TBD = To Be Deleted + $listItemTBD = GroceryItem::factory()->create([ + 'shopping_list_id' => $list->list_id + ]); + + $listItemTBDResponse = $this->actingAs($otherUser); + $listItemTBDResponse = $this->deleteJson('/grocery-items/' . $listItemTBD->item_id); + $listItemTBDResponse->assertStatus(200); + $this->assertDatabaseMissing('grocery_items', [ + 'shopping_list_id', $list->list_id, + 'item_id' => $listItemTBD->item_id + ]); + + // Deletion which should fail + $listDeleteResponse = $this->actingAs($otherUser); + $listDeleteResponse = $this->deleteJson('/shopping-lists/' . $list->list_id); + + $listDeleteResponse->assertStatus(403); + + // Ensure content was preserved in all tables involved + $this->assertDatabaseHas('shopping_lists', [ + 'list_id' => $list->list_id, + 'user_id' => $user->user_id + ]); + + $this->assertDatabaseHas('grocery_items', [ + 'shopping_list_id' => $list->list_id, + 'item_id' => $listItemCreationResponseJson['item_id'] + ]); + } + + public function testVerifyShareLinkDeleteAllowedButNotUpdate() { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $list = ShoppingList::factory()->create([ + 'user_id' => $user->user_id, + 'name' => 'Test Shopping List' + ]); + $shareLink = SharedLink::factory()->create([ + 'shopping_list_id' => $list->list_id, + 'can_delete' => 1 + ]); + + $response = $this->actingAs($otherUser); + + // Verify the first link with otherUser to give them permissions + $response = $this->getJson('/share/' . $shareLink->token); + + // Now try CRUD operations with Controllers but UPDATE should fail + + // Title update + $titleUpdateResponse = $this->actingAs($otherUser); + $titleUpdateResponse = $this->putJson('/shopping-lists/' . $list->list_id, [ + 'name' => 'foo2', + ]); + $titleUpdateResponse->assertStatus(403); + $this->assertDatabaseMissing('grocery_items', [ + 'name' => 'foo2', + ]); + + // Item creation + $listItemCreationResponse = $this->actingAs($otherUser); + $listItemCreationResponse = $this->postJson('/grocery-items', [ + 'name' => 'Bananas', + 'quantity' => 10, + 'is_food' => true, + 'shopping_list_id' => $list->list_id, + ]); + + $listItemCreationResponse->assertStatus(403); + + // Try to delete an item (fabricate via Factory); THIS REQUEST SHOULD FAIL! + // TBD = To Be Deleted + $listItemTBD = GroceryItem::factory()->create([ + 'shopping_list_id' => $list->list_id, + 'name' => 'stick around' + ]); + + $listItemTBDResponse = $this->actingAs($otherUser); + $listItemTBDResponse = $this->deleteJson('/grocery-items/' . $listItemTBD->item_id); + $listItemTBDResponse->assertStatus(403); + $this->assertDatabaseHas('grocery_items', [ + 'shopping_list_id' => $list->list_id, + 'name' => 'stick around' + ]); + + + // Deletion which should succeed (since we're deleting the list) + $listDeleteResponse = $this->actingAs($otherUser); + $listDeleteResponse = $this->deleteJson('/shopping-lists/' . $list->list_id); + + $listDeleteResponse->assertStatus(200); + + // Ensure content was deleted in all tables involved + $this->assertDatabaseMissing('shopping_lists', [ + 'list_id' => $list->list_id, + 'user_id' => $user->user_id + ]); + + // There should be NO ITEMS REMAINING since the list was deleted + $this->assertDatabaseMissing('grocery_items', [ + 'shopping_list_id' => $list->list_id, + ]); + } + public function testUnauthorizedUserCannotCreateShareLink(): void { $owner = User::factory()->create(); @@ -60,5 +265,13 @@ public function testUnauthorizedUserCannotCreateShareLink(): void $response = $this->postJson('/share/' . $list->list_id); $response->assertStatus(403); + + // Ensure chosen permissions (default from factory) were NOT properly set + $this->assertDatabaseMissing('shared_shopping_list_perms', [ + 'shopping_list_id' => $list->list_id, + 'user_id' => $unauthorizedUser->user_id, + 'can_update' => 0, + 'can_delete' => 0 + ]); } -} \ No newline at end of file +}