diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 535d56f7..5f68fb24 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -190,7 +190,7 @@ public function show(string $commentableType, int $commentableId, int $index) } } - \Log::debug("Returned comments: " . json_encode($flattenedComments)); + Log::debug("Returned comments: " . json_encode($flattenedComments)); return [ 'comments' => $flattenedComments, 'users' => $users->values(), diff --git a/app/Http/Controllers/ComputerScienceResourceController.php b/app/Http/Controllers/ComputerScienceResourceController.php index d0c5a8c6..b63ea7d1 100644 --- a/app/Http/Controllers/ComputerScienceResourceController.php +++ b/app/Http/Controllers/ComputerScienceResourceController.php @@ -45,8 +45,8 @@ public function store(StoreResourceRequest $request) $resource = ComputerScienceResource::create([ 'user_id' => Auth::id(), 'name' => $validatedData['name'], + 'image_url' => $validatedData['image_url'], 'description' => $validatedData['description'], - 'image_url' => $validatedData['image_url'] ?? null, 'page_url' => $validatedData['page_url'], 'platforms' => $validatedData['platforms'], 'difficulty' => $validatedData['difficulty'], @@ -55,7 +55,7 @@ public function store(StoreResourceRequest $request) // Add topics as tags $resource->topic_tags = $validatedData['topic_tags']; - + // Add programming languages as tags (if provided) if (isset($validatedData['programming_language_tags'])) { $resource->programming_language_tags = $validatedData['programming_language_tags']; diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 9389ca59..1d5a2b5b 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -2,10 +2,12 @@ namespace App\Http\Controllers; -use App\Http\Requests\ResourceEdit\StoreResourceEdit; use App\Models\ComputerScienceResource; use App\Models\ResourceEdits; use App\Services\ResourceEditsService; +use App\Http\Requests\ResourceEdit\StoreResourceEdit; +use App\Http\Resources\ComputerScienceResourceResource; +use Arr; use Auth; use Inertia\Inertia; use Log; @@ -39,6 +41,18 @@ public function create(ComputerScienceResource $computerScienceResource) ]); } + + // TODO: Make an array facade + function normalize($array) { + ksort($array); + foreach ($array as &$value) { + if (is_array($value)) { + sort($value); // Assumes it's a flat array of values + } + } + return $array; + } + /** * Store the edits request. */ @@ -47,15 +61,23 @@ public function store(ComputerScienceResource $computerScienceResource, StoreRes $validatedData = $request->validated(); Log::debug("Creating a resource edit: " . json_encode($validatedData)); + // Ensure that they are not the same + $originalData = $this->normalize((new ComputerScienceResourceResource($computerScienceResource))->resolve()); + $editData = $this->normalize($validatedData); + unset($editData['edit_title'], $editData['edit_description']); + + if ($originalData == $editData) { + return response()->json(['message' => 'No changes detected'], 422); + } + $resourceEdit = ResourceEdits::create([ 'user_id' => Auth::id(), 'computer_science_resource_id' => $computerScienceResource->id, 'edit_title' => $validatedData['edit_title'], 'edit_description' => $validatedData['edit_description'], - + 'image_url' => $validatedData['image_url'], 'name' => $validatedData['name'], 'description' => $validatedData['description'], - 'image_url' => $validatedData['image_url'] ?? null, 'page_url' => $validatedData['page_url'], 'platforms' => $validatedData['platforms'], 'difficulty' => $validatedData['difficulty'], diff --git a/app/Http/Controllers/ResourceReviewController.php b/app/Http/Controllers/ResourceReviewController.php index 8702c70d..6673ad51 100644 --- a/app/Http/Controllers/ResourceReviewController.php +++ b/app/Http/Controllers/ResourceReviewController.php @@ -8,17 +8,29 @@ use App\Models\ResourceReview; use Auth; use Illuminate\Support\Facades\Log; +use Redirect; class ResourceReviewController extends Controller { // Store the review on the resource public function store(StoreResourceReview $request, ComputerScienceResource $computerScienceResource) { - Log::debug("Storing resource review: " . json_encode($request)); - // Validate the request data $validatedData = $request->validated(); + $existingReview = ResourceReview::where([ + 'user_id' => Auth::id(), + 'computer_science_resource_id' => $computerScienceResource->id, + ])->first(); + + if ($existingReview) { + Log::debug("User has already posted a review"); + // TODO: Make it a json with errors instead + return back()->with('warning', 'You already have a review posted, you should edit your existing one instead.'); + } + + Log::debug("Storing resource review: " . json_encode($validatedData)); + // Create the resource review $review = ResourceReview::create([ 'user_id' => Auth::id(), @@ -37,6 +49,7 @@ public function store(StoreResourceReview $request, ComputerScienceResource $com ResourceReviewProcessed::dispatch($computerScienceResource->id, null, $review->attributesToArray()); + // Json with success return to_route('resources.show', ['computerScienceResource' => $review->computer_science_resource_id]) ->with('success', 'Review created successfully!'); } diff --git a/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php b/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php index dde55e1b..3923bf33 100644 --- a/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php +++ b/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php @@ -2,12 +2,14 @@ namespace App\Http\Requests\ComputerScienceResource; +use App\Http\Requests\Shared\ComputerScienceResourceRequest; use Auth; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Validation\Rule; class StoreResourceRequest extends FormRequest { + use ComputerScienceResourceRequest; + /** * Determine if the user is authorized to make this request. */ @@ -23,23 +25,6 @@ public function authorize(): bool */ public function rules(): array { - return [ - 'name' => ['required', 'string', 'max:100'], - 'description' => ['required', 'string', 'max:10000'], - 'platforms' => ['required', 'array'], - 'platforms.*' => ['distinct', 'string', Rule::in(config('computerScienceResource.platforms'))], - 'page_url' => ['required', 'string', 'url:http,https', 'max:255'], - 'difficulty' => ['required', 'string', Rule::in(config('computerScienceResource.difficulties'))], - 'pricing' => ['required', 'string', Rule::in(config('computerScienceResource.pricings'))], - 'topic_tags' => ['required', 'array', 'min:3'], - 'topic_tags.*' => ['required', 'distinct', 'string', 'max:50'], - - // Optional fields - 'image_url' => ['nullable', 'string', 'url:http,https', 'max:255'], - 'general_tags' => ['nullable', 'array'], - 'general_tags.*' => ['distinct', 'string', 'max:50'], - 'programming_language_tags' => ['nullable', 'array'], - 'programming_language_tags.*' => ['distinct', 'string', 'max:50'] - ]; + return $this->baseResourceRules(); } } diff --git a/app/Http/Requests/ResourceEdit/StoreResourceEdit.php b/app/Http/Requests/ResourceEdit/StoreResourceEdit.php index 80ee44e9..52e2065a 100644 --- a/app/Http/Requests/ResourceEdit/StoreResourceEdit.php +++ b/app/Http/Requests/ResourceEdit/StoreResourceEdit.php @@ -2,12 +2,13 @@ namespace App\Http\Requests\ResourceEdit; -use App\Http\Requests\ComputerScienceResource\StoreResourceRequest; +use App\Http\Requests\Shared\ComputerScienceResourceRequest; use Auth; use Illuminate\Foundation\Http\FormRequest; class StoreResourceEdit extends FormRequest { + use ComputerScienceResourceRequest; /** * Determine if the user is authorized to make this request. */ @@ -23,12 +24,9 @@ public function authorize(): bool */ public function rules(): array { - // Have the same validation rules as a resource - $storeResourceRequest = new StoreResourceRequest(); - - return array_merge([ + return array_merge($this->baseResourceRules(), [ 'edit_title' => ['required', 'string', 'max:100'], 'edit_description' => ['required', 'string', 'max:10000'], - ], $storeResourceRequest->rules()); + ]); } } diff --git a/app/Http/Requests/ResourceReview/StoreResourceReview.php b/app/Http/Requests/ResourceReview/StoreResourceReview.php index 7109ff75..de979421 100644 --- a/app/Http/Requests/ResourceReview/StoreResourceReview.php +++ b/app/Http/Requests/ResourceReview/StoreResourceReview.php @@ -3,7 +3,6 @@ namespace App\Http\Requests\ResourceReview; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Validation\Rule; class StoreResourceReview extends FormRequest { diff --git a/app/Http/Requests/Shared/ComputerScienceResourceRequest.php b/app/Http/Requests/Shared/ComputerScienceResourceRequest.php new file mode 100644 index 00000000..79780869 --- /dev/null +++ b/app/Http/Requests/Shared/ComputerScienceResourceRequest.php @@ -0,0 +1,32 @@ + ['required', 'string', 'max:100'], + 'description' => ['required', 'string', 'max:10000'], + 'platforms' => ['required', 'array', 'min:1'], + 'platforms.*' => ['required', 'distinct', 'string', Rule::in(config('computerScienceResource.platforms'))], + 'page_url' => ['required', 'string', 'url:http,https', 'max:255'], + 'image_url' => ['nullable', 'string', 'url:http,https', 'max:255'], + 'difficulty' => ['required', 'string', Rule::in(config('computerScienceResource.difficulties'))], + 'pricing' => ['required', 'string', Rule::in(config('computerScienceResource.pricings'))], + + 'topic_tags' => ['required', 'array', 'min:3'], + 'topic_tags.*' => ['required', 'distinct', 'string', 'max:50'], + + // Optional + 'general_tags' => ['array'], + 'general_tags.*' => ['required', 'distinct', 'string', 'max:50'], + 'programming_language_tags' => ['array'], + 'programming_language_tags.*' => ['required', 'distinct', 'string', 'max:50'], + ]; + } + +} \ No newline at end of file diff --git a/app/Http/Resources/ComputerScienceResourceResource.php b/app/Http/Resources/ComputerScienceResourceResource.php new file mode 100644 index 00000000..c347f3f3 --- /dev/null +++ b/app/Http/Resources/ComputerScienceResourceResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'name' => $this->name, + 'description' => $this->description, + 'page_url' => $this->page_url, + 'image_url' => $this->image_url, + 'platforms' => $this->platforms, + 'difficulty' => $this->difficulty, + 'pricing' => $this->pricing, + 'topic_tags' => $this->topic_tags, + 'programming_language_tags' => $this->programming_language_tags, + 'general_tags' => $this->general_tags, + ]; + } +} diff --git a/config/comment.php b/config/comment.php index 2bca98fc..3d6b4997 100644 --- a/config/comment.php +++ b/config/comment.php @@ -3,6 +3,6 @@ return [ 'max_replies' => 150, 'max_depth' => 7, // 1-indexed, 1 is the start - 'pagination_limit' => 2, + 'pagination_limit' => 3, 'commentable_types' => ['review', 'comment', 'edit', 'resource'] ]; \ No newline at end of file diff --git a/config/computerScienceResource.php b/config/computerScienceResource.php index 09e461c8..e7a66c61 100644 --- a/config/computerScienceResource.php +++ b/config/computerScienceResource.php @@ -1,7 +1,5 @@ ['book', 'podcast', 'youtube_channel', 'blog', 'website', 'organization', 'bootcamp', 'newsletter', 'workshop', 'course', 'forum', 'mobile_app', 'desktop_app', 'magazine'], 'difficulties' => ['beginner', 'industry_simple', 'industry_standard', 'industry_professional', 'academic'], diff --git a/database/factories/ResourceEditsFactory.php b/database/factories/ResourceEditsFactory.php index a565691f..3bd842da 100644 --- a/database/factories/ResourceEditsFactory.php +++ b/database/factories/ResourceEditsFactory.php @@ -21,9 +21,16 @@ public function definition(): array $pricings = config('computerScienceResource.pricings'); return [ - 'computer_science_resource_id' => ComputerScienceResource::factory(), - 'user_id' => User::factory(), - + 'computer_science_resource_id' => function () { + return ComputerScienceResource::inRandomOrder()->firstOr(function () { + return ComputerScienceResource::factory()->create(); + })->id; + }, + 'user_id' => function () { + return User::inRandomOrder()->firstOr(function () { + return User::factory()->create(); + })->id; + }, 'edit_title' => $this->faker->sentence, 'edit_description' => $this->faker->paragraph, diff --git a/database/factories/ResourceReviewFactory.php b/database/factories/ResourceReviewFactory.php index d1829ed4..16f2a035 100644 --- a/database/factories/ResourceReviewFactory.php +++ b/database/factories/ResourceReviewFactory.php @@ -20,11 +20,7 @@ class ResourceReviewFactory extends Factory public function definition(): array { return [ - 'user_id' => function () { - return User::inRandomOrder()->firstOr(function () { - return User::factory()->create(); - })->id; - }, + 'user_id' => User::factory()->create()->id, // Is creating a new user since we need users to be unique per review 'computer_science_resource_id' => function () { return ComputerScienceResource::firstOr(function () { return ComputerScienceResource::factory()->create(); diff --git a/database/migrations/2025_01_24_171530_create_computer_science_resources_table.php b/database/migrations/2025_01_24_171530_create_computer_science_resources_table.php index 0ce7f7b5..1a97f74e 100644 --- a/database/migrations/2025_01_24_171530_create_computer_science_resources_table.php +++ b/database/migrations/2025_01_24_171530_create_computer_science_resources_table.php @@ -21,8 +21,8 @@ public function up(): void $table->string('name')->fulltext(); $table->text('description')->fulltext(); - $table->string('image_url'); - + $table->string('image_url')->nullable(); + // TODO: Have a url for each platform the resource is on. $table->string('page_url'); diff --git a/database/migrations/2025_02_14_225555_create_resource_reviews_table.php b/database/migrations/2025_02_14_225555_create_resource_reviews_table.php index 62d26a70..f58acb54 100644 --- a/database/migrations/2025_02_14_225555_create_resource_reviews_table.php +++ b/database/migrations/2025_02_14_225555_create_resource_reviews_table.php @@ -17,6 +17,11 @@ public function up(): void $table->id(); $table->foreignIdFor(User::class)->index(); $table->foreignIdFor(ComputerScienceResource::class)->index(); + + $table->unique([ + 'user_id', + 'computer_science_resource_id' + ]); // Text $table->string('title'); diff --git a/database/migrations/2025_03_24_154128_create_resource_edits_table.php b/database/migrations/2025_03_24_154128_create_resource_edits_table.php index c6c9a374..74886f2e 100644 --- a/database/migrations/2025_03_24_154128_create_resource_edits_table.php +++ b/database/migrations/2025_03_24_154128_create_resource_edits_table.php @@ -29,7 +29,9 @@ public function up(): void // Copied Schema of Computer Science Resource $table->string('name')->fulltext(); $table->text('description')->fulltext(); - $table->string('image_url'); + + // TODO: have it be nullable or something + $table->string('image_url')->nullable(); $table->string('page_url'); diff --git a/database/seeders/ComputerScienceResourceSeeder.php b/database/seeders/ComputerScienceResourceSeeder.php index af3defe5..340c9edd 100644 --- a/database/seeders/ComputerScienceResourceSeeder.php +++ b/database/seeders/ComputerScienceResourceSeeder.php @@ -14,7 +14,6 @@ class ComputerScienceResourceSeeder extends Seeder */ public function run(): void { - Log::info('Running ComputerScienceResourceSeeder'); ComputerScienceResource::factory(1)->create(); } } diff --git a/tests/Feature/ComputerScienceResourceControllerTest.php b/tests/Feature/ComputerScienceResourceControllerTest.php index 3f71e031..270401b8 100644 --- a/tests/Feature/ComputerScienceResourceControllerTest.php +++ b/tests/Feature/ComputerScienceResourceControllerTest.php @@ -28,6 +28,7 @@ public function test_can_post_resource() $formData = ComputerScienceResourceTestResource::fake(); + Log::debug("Form data: ". json_encode($formData)); $response = $this->postJson(route('resources.store'), $formData); $response->assertStatus(302); // a redirect after successful creation @@ -36,11 +37,6 @@ public function test_can_post_resource() // Check it is created $createdResource = ComputerScienceResource::where('name', $formData['name'])->first(); $this->assertNotNull($createdResource); - - // Check tags - $this->assertEquals($formData['topic_tags'], $createdResource->topic_tags); - $this->assertEquals($formData['programming_language_tags'], $createdResource->programming_language_tags); - $this->assertEquals($formData['general_tags'], $createdResource->general_tags); } public function test_cannot_post_resource_unauthed() @@ -84,8 +80,8 @@ public function test_cannot_post_resource_with_invalid_fields() 'pricing' => 'invalid_pricing', 'topic_tags' => ['tag1', 'tag2'], // Less than required minimum of 3 'image_url' => 'not-a-url', - 'general_tags' => 'not-an-array', - 'programming_language_tags' => 'not-an-array' + 'programming_language_tags' => null, + 'general_tags' => ['a','a','a'], // Not distinct ]; // Choose from one of the invalid fields diff --git a/tests/Feature/ResourceEditsTest.php b/tests/Feature/ResourceEditsTest.php new file mode 100644 index 00000000..8ba301ef --- /dev/null +++ b/tests/Feature/ResourceEditsTest.php @@ -0,0 +1,288 @@ +user = User::factory()->create(); + } + + /** + * Test that invalid edit payloads are rejected. + */ + public function test_cannot_post_resource_with_invalid_fields(): void + { + $this->actingAs($this->user); + + $resource = ComputerScienceResource::factory()->create(); + $validData = ComputerScienceResourceTestResource::fake(); + + $validData['edit_title'] = 'title'; + $validData['edit_description'] = 'description'; + + $invalidDataSets = [ + 'name' => str_repeat('a', 101), // Too long + 'description' => str_repeat('a', 10001), // Too long + 'platforms' => ['invalid_platform'], + 'page_url' => 'not-a-url', + 'difficulty' => 'invalid_difficulty', + 'pricing' => 'invalid_pricing', + 'topic_tags' => ['tag1', 'tag2'], // Fewer than required minimum of 3 + 'image_url' => 'not-a-url', + 'general_tags' => 'not-an-array', + 'programming_language_tags' => 'not-an-array' + ]; + + foreach ($invalidDataSets as $field => $invalidValue) { + $testData = $validData; + $testData[$field] = $invalidValue; + + $response = $this->postJson(route('resource_edits.store', [ + 'computerScienceResource' => $resource->id, + ]), $testData); + + $this->assertTrue( + $response->status() === 422, + "Failed asserting that the server responded with a 422 status code for invalid '$field'. Response status: " . $response->status() + ); + + $notCreatedEdit = ResourceEdits::first(); + $this->assertNull( + $notCreatedEdit, + "Failed asserting that a resource edit with name '{$testData['name']}' was not created. Invalid field: " . $field + ); + } + } + + /** + * Test that submitting an edit that has no changes (relative to the original resource) is not allowed. + */ + public function test_cannot_post_resource_edit_with_no_changes(): void + { + $this->actingAs($this->user); + + $times = 7; + + for ($i = 0; $i < $times; $i++) + { + // Create the original resource. + $resource = ComputerScienceResource::factory()->create(); + + // Create valid edit payload, then set fields to exactly match the resource. + + $editData = array(); + $editData['name'] = $resource->name; + $editData['description'] = $resource->description; + $editData['page_url'] = $resource->page_url; + $editData['image_url'] = $resource->image_url; + $editData['platforms'] = $resource->platforms; + $editData['difficulty'] = $resource->difficulty; + $editData['pricing'] = $resource->pricing; + $editData['topic_tags'] = $resource->topic_tags; + $editData['programming_language_tags'] = $resource->programming_language_tags; + $editData['general_tags'] = $resource->general_tags; + + // Add required edit-specific fields. + $editData['edit_title'] = 'Proposed edit with no changes'; + $editData['edit_description'] = 'This edit does nothing.'; + + $response = $this->postJson(route('resource_edits.store', $resource), $editData); + + if ($response->status() !== 422) { + Log::debug("here"); + Log::debug('editData:'. json_encode($editData)); + Log::debug('resource:'. json_encode(new ComputerScienceResourceResource($resource))); + } + + $response->assertStatus(422); + } + } + + /** + * Test that a valid resource edit can be posted. + */ + public function test_can_post_valid_resource_edit(): void + { + $this->actingAs($this->user); + + // Create the original resource. + $resource = ComputerScienceResource::factory()->create(); + + // Create valid edit payload and change at least one attribute. + $editData = ComputerScienceResourceTestResource::fake(); + $editData['edit_title'] = 'Proposed Update'; + $editData['edit_description'] = 'Proposing an update to the resource'; + + $editData['name'] = $resource->name . ' Updated'; + + $response = $this->post(route('resource_edits.store', $resource), $editData); + + // Expect redirection to the edit show page with a success message. + $response->assertRedirect() + ->assertSessionHas('success', "The proposed edits were created. Other's can now view it."); + + $this->assertDatabaseHas('resource_edits', [ + 'computer_science_resource_id' => $resource->id, + 'name' => $editData['name'], + ]); + } + + /** + * Test that merging an edit updates the original resource. + * We run multiple merges to simulate multiple edit merges. + */ + + // TODO: Handle null images + // TODO: Handle all fields and attributes + public function test_merged_edit_reflects_changes_on_original_resource(): void + { + $resource = ComputerScienceResource::factory()->create(); + + // Stub the ResourceEditsService to always allow merging + $this->instance( + ResourceEditsService::class, + Mockery::mock(ResourceEditsService::class, function (MockInterface $mock) { + $mock->shouldReceive('canMergeEdits')->andReturnTrue(); + }) + ); + + $mergeAttempts = 10; + + for ($i = 0; $i < $mergeAttempts; $i++) { + $resource->refresh(); + + $editData = ComputerScienceResourceTestResource::fake(); + + $editData['edit_title'] = "Edit #$i"; + $editData['edit_description'] = "This is edit number $i."; + + $editData['name'] = "Resource Name Edited {$i}"; + $editData['description'] = "Resource Description Changed {$i}"; + $editData['image_url'] = fake()->randomElement(["http://{$i}.com", null]); + $editData['page_url'] = "http://{$i}.com"; + $editData['difficulty'] = fake()->randomElement(config('computerScienceResource.difficulties')); + $editData['platforms'] = fake()->randomElements(config('computerScienceResource.platforms'), fake()->numberBetween(1, 3)); + $editData['pricing'] = fake()->randomElement(config('computerScienceResource.pricings')); + $editData['topic_tags'] = ["{$i}_a", "{$i}_b", "{$i}_c"]; + + $editData['programming_language_tags'] = ["{$i}_a", "{$i}_b", "{$i}_c"]; + $editData['general_tags'] = ["{$i}_a", "{$i}_b", "{$i}_c"]; + + // Submit the edit + $this->actingAs($this->user); + $response = $this->post( + route('resource_edits.store', ['computerScienceResource' => $resource->id]), + $editData + ); + $response->assertStatus(302); + + $edit = ResourceEdits::latest()->first(); + $this->assertNotNull($edit, 'Failed to create resource edit'); + + // Merge the edit + $mergeResponse = $this->post(route('resource_edits.merge', ['resourceEdits' => $edit->id])); + $mergeResponse + ->assertRedirect(route('resources.show', ['computerScienceResource' => $resource->id])) + ->assertSessionHas('success', 'Successfully merged new changed!'); + + // Refresh and assert + $resource->refresh(); + + $this->assertEquals($editData['name'], $resource->name); + $this->assertEquals($editData['description'], $resource->description); + $this->assertEquals($editData['image_url'], $resource->image_url); + $this->assertEquals($editData['page_url'], $resource->page_url); + $this->assertEquals($editData['difficulty'], $resource->difficulty); + $this->assertEquals($editData['pricing'], $resource->pricing); + + // Arrays + $this->assertEqualsCanonicalizing($editData['platforms'], $resource->platforms); + $this->assertEqualsCanonicalizing($editData['topic_tags'], $resource->topic_tags); + $this->assertEqualsCanonicalizing($editData['programming_language_tags'], $resource->programming_language_tags); + $this->assertEqualsCanonicalizing($editData['general_tags'], $resource->general_tags); + + $this->assertDatabaseMissing('resource_edits', ['id' => $edit->id]); + } + } + + public function test_merged_delete_edit_reflects_changes_on_original_resource(): void + { + $resource = ComputerScienceResource::factory()->create(); + + // Stub the ResourceEditsService to always allow merging + $this->instance( + ResourceEditsService::class, + Mockery::mock(ResourceEditsService::class, function (MockInterface $mock) { + $mock->shouldReceive('canMergeEdits')->andReturnTrue(); + }) + ); + + $editData = (new ComputerScienceResourceTestResource($resource))->resolve(); + + $editData['edit_title'] = "Edit"; + $editData['edit_description'] = "This is edit"; + + $editData['name'] = "Resource Name Edited"; + $editData['description'] = "Resource Description Changed"; + $editData['image_url'] = null; // Delete operation + $editData['topic_tags'] = ['1', '2', '3']; + $editData['programming_language_tags'] = []; + $editData['general_tags'] = []; + + // Submit the edit + $this->actingAs($this->user); + $response = $this->post( + route('resource_edits.store', ['computerScienceResource' => $resource->id]), + $editData + ); + $response->assertStatus(302); + + $edit = ResourceEdits::latest()->first(); + $this->assertNotNull($edit, 'Failed to create resource edit'); + + // Merge the edit + $mergeResponse = $this->post(route('resource_edits.merge', ['resourceEdits' => $edit->id])); + $mergeResponse + ->assertRedirect(route('resources.show', ['computerScienceResource' => $resource->id])) + ->assertSessionHas('success', 'Successfully merged new changed!'); + + // Refresh and assert + $resource->refresh(); + + $this->assertEquals("Resource Name Edited", $resource->name); + $this->assertEquals("Resource Description Changed", $resource->description); + $this->assertNull($resource->image_url); // Since we unset it + $this->assertEquals($editData['page_url'], $resource->page_url); + $this->assertEquals($editData['difficulty'], $resource->difficulty); + $this->assertEquals($editData['pricing'], $resource->pricing); + + // Arrays + $this->assertEqualsCanonicalizing($editData['platforms'], $resource->platforms); + $this->assertEqualsCanonicalizing($editData['topic_tags'], $resource->topic_tags); + $this->assertEqualsCanonicalizing($editData['programming_language_tags'], $resource->programming_language_tags); + $this->assertEqualsCanonicalizing($editData['general_tags'], $resource->general_tags); + + $this->assertDatabaseMissing('resource_edits', ['id' => $edit->id]); + } +} diff --git a/tests/Feature/ResourceReviewsTest.php b/tests/Feature/ResourceReviewsTest.php index 09d58ab1..bc751d0a 100644 --- a/tests/Feature/ResourceReviewsTest.php +++ b/tests/Feature/ResourceReviewsTest.php @@ -67,18 +67,39 @@ public function test_resource_review_can_be_posted(): void $data = ResourceReviewTestResource::fake(); - $response = $this->actingAs($user) + $this->actingAs($user) ->post(route('reviews.store', $resource), $data); - $response->assertRedirect(route('resources.show', ['computerScienceResource' => $resource->id])) - ->assertSessionHas('success', 'Review created successfully!'); - $this->assertDatabaseHas('resource_reviews', [ 'computer_science_resource_id' => $resource->id, 'title' => $data['title'] ]); } + public function test_resource_review_cannot_be_posted_twice(): void + { + $user = User::factory()->create(); + $resource = ComputerScienceResource::factory()->create(); + + $data1 = ResourceReviewTestResource::fake(); + $this->actingAs($user) + ->post(route('reviews.store', $resource), $data1); + + $this->assertDatabaseHas('resource_reviews', [ + 'computer_science_resource_id' => $resource->id, + 'title' => $data1['title'] + ]); + + $data2 = ResourceReviewTestResource::fake(); + $this->actingAs($user) + ->post(route('reviews.store', $resource), $data2); + + $this->assertDatabaseMissing('resource_reviews', [ + 'computer_science_resource_id' => $resource->id, + 'title' => $data2['title'] + ]); + } + public function test_resource_average_has_changed(): void { $user = User::factory()->create(); diff --git a/tests/Feature/UpvoteTest.php b/tests/Feature/UpvoteTest.php index 369ab700..3a087e45 100644 --- a/tests/Feature/UpvoteTest.php +++ b/tests/Feature/UpvoteTest.php @@ -150,9 +150,45 @@ public function test_multiple_upvotes() } /** - * Test upvote after downvote makes the score 0. + * Test two upvotes from the same user is actually deleting the original vote. */ - public function test_upvote_after_downvote_resets_to_zero() + public function test_same_user_double_upvotes() + { + $user = User::factory()->create(); + $resource = ComputerScienceResource::factory()->create(); + + // Upvote twice + $this->actingAs($user); + $this->postJson(route('upvote', ['type' => 'resource', 'id' => $resource->id])); + $this->postJson(route('upvote', ['type' => 'resource', 'id' => $resource->id])); + + // Check if the votes have been deleted correctly + $this->assertEquals(0, $resource->upvoteSummary->upvotes); // No upvotes + $this->assertEquals(0, $resource->upvoteSummary->downvotes); // No downvotes + } + + /** + * Test two upvotes from the same user is actually deleting the original vote. + */ + public function test_same_user_double_downvotes() + { + $user = User::factory()->create(); + $resource = ComputerScienceResource::factory()->create(); + + // Upvote twice + $this->actingAs($user); + $this->postJson(route('downvote', ['type' => 'resource', 'id' => $resource->id])); + $this->postJson(route('downvote', ['type' => 'resource', 'id' => $resource->id])); + + // Check if the votes have been deleted correctly + $this->assertEquals(0, $resource->upvoteSummary->upvotes); // No upvotes + $this->assertEquals(0, $resource->upvoteSummary->downvotes); // No downvotes + } + + /** + * Test upvote after downvote makes the score 1. + */ + public function test_upvote_after_downvote_is_1() { $user = User::factory()->create(); $resource = ComputerScienceResource::factory()->create(); @@ -171,9 +207,9 @@ public function test_upvote_after_downvote_resets_to_zero() } /** - * Test downvote after upvote makes the score 0. + * Test downvote after upvote makes the score -1. */ - public function test_downvote_after_upvote_resets_to_zero() + public function test_downvote_after_upvote_is_negative_1() { $user = User::factory()->create(); $resource = ComputerScienceResource::factory()->create(); diff --git a/tests/TestResources/ComputerScienceResourceTestResource.php b/tests/TestResources/ComputerScienceResourceTestResource.php index 77f0f3c1..d1a2eec8 100644 --- a/tests/TestResources/ComputerScienceResourceTestResource.php +++ b/tests/TestResources/ComputerScienceResourceTestResource.php @@ -3,7 +3,6 @@ namespace Tests\TestResources; use App\Models\ComputerScienceResource; -use Event; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -20,7 +19,7 @@ public function toArray(Request $request): array 'difficulty' => $this->difficulty, 'pricing' => $this->pricing, 'topic_tags' => $this->topic_tags, - 'programming_language_tags' => $this->programming_language_tags, + 'programming_language_tags' => $this->programming_language_tags, 'general_tags' => $this->general_tags, ]; } @@ -28,7 +27,7 @@ public function toArray(Request $request): array public static function fake(): array { // Create the model with disabled events - $model = Event::fakeFor(fn() => ComputerScienceResource::factory()->create()); + $model = ComputerScienceResource::factory()->create(); // Transform it to API form $formData = (new self($model))->toArray(request()); diff --git a/tests/TestResources/ResourceReviewTestResource.php b/tests/TestResources/ResourceReviewTestResource.php index d9a5f8eb..d563ca80 100644 --- a/tests/TestResources/ResourceReviewTestResource.php +++ b/tests/TestResources/ResourceReviewTestResource.php @@ -2,6 +2,7 @@ namespace Tests\TestResources; +use App\Events\ResourceReviewProcessed; use App\Models\ResourceReview; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -27,13 +28,15 @@ public function toArray(Request $request): array public static function fake(): array { - // Create the model with disabled events - $model = Event::fakeFor(fn() => ResourceReview::factory()->create()); - + // Fake all events except the ones you still want to fire + $model = Event::fakeFor(function () { + return ResourceReview::factory()->create(); + }, [ResourceReviewProcessed::class]); + // Transform it to API form $formData = (new self($model))->toArray(request()); - // Delete after getting the array to avoid polluting the DB + // Delete after getting the array $model->delete(); return $formData;