From d2f7b480c5f18bfa24638ebcc19e7523a00dfeff Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Fri, 2 May 2025 15:14:19 -0600 Subject: [PATCH] Unit tests for the tags --- .../Controllers/ResourceEditsController.php | 2 +- .../Controllers/TagFrequencyController.php | 2 +- app/Models/ComputerScienceResource.php | 2 +- .../ComputerScienceResourceFactory.php | 35 ++-- tests/Feature/ResourceEditsTest.php | 2 - tests/Feature/TagSearchTest.php | 149 ++++++++++++++++++ .../ComputerScienceResourceTestResource.php | 13 +- .../ResourceReviewTestResource.php | 2 +- 8 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 tests/Feature/TagSearchTest.php diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 5ec1bda4..23dbde7b 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -111,7 +111,7 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc $resource->general_tags = $resourceEdits->general_tags; // Get the new tag counter - $new_tags = collect([$resourceEdits->topic_tags, $resourceEdits->programming_language_tags, $resourceEdits->general_tags])->flatten()->unique()->countBy()->toArray(); + $new_tags = collect([$resourceEdits->topic_tags, $resourceEdits->programming_language_tags, $resourceEdits->general_tags])->flatten()->countBy()->toArray(); // Change tag frequency TagFrequencyChanged::dispatch($old_tag_counter, $new_tags); diff --git a/app/Http/Controllers/TagFrequencyController.php b/app/Http/Controllers/TagFrequencyController.php index 89a13f4a..e3df355c 100644 --- a/app/Http/Controllers/TagFrequencyController.php +++ b/app/Http/Controllers/TagFrequencyController.php @@ -11,7 +11,7 @@ public function search(string $query = "") { if (strlen($query) > 50) { - return response(422)->json(); + return response()->json(['message' => 'Query too long.'], 422); } $prefixed_tags = TagFrequency::where('tag', 'like', $query.'%') diff --git a/app/Models/ComputerScienceResource.php b/app/Models/ComputerScienceResource.php index 5c296410..ce36c33b 100644 --- a/app/Models/ComputerScienceResource.php +++ b/app/Models/ComputerScienceResource.php @@ -108,6 +108,6 @@ protected function generalTags(): Attribute public function tagCounter(): array { $tag_collection = collect([$this->topic_tags, $this->programming_language_tags, $this->general_tags]); - return $tag_collection->flatten()->unique()->countBy()->toArray(); + return $tag_collection->flatten()->countBy()->toArray(); } } diff --git a/database/factories/ComputerScienceResourceFactory.php b/database/factories/ComputerScienceResourceFactory.php index bab74fd9..2a7cc9bc 100644 --- a/database/factories/ComputerScienceResourceFactory.php +++ b/database/factories/ComputerScienceResourceFactory.php @@ -15,11 +15,10 @@ class ComputerScienceResourceFactory extends Factory { protected $model = ComputerScienceResource::class; - /** - * Define the model's default state. - * - * @return array - */ + protected ?array $topicTags = null; + protected ?array $programmingLanguageTags = null; + protected ?array $generalTags = null; + public function definition(): array { $platforms = config('computerScienceResource.platforms'); @@ -32,26 +31,28 @@ public function definition(): array 'user_id' => User::inRandomOrder()->first() ?? User::factory()->create(), 'image_url' => 'https://cdn.iconscout.com/icon/free/png-256/free-leetcode-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-company-vol-1-pack-logos-icons-3030025.png', 'page_url' => fake()->url(), - 'platforms' => fake()->randomElements($platforms, rand(1, 3)), 'difficulty' => fake()->randomElement($difficulties), 'pricing' => fake()->randomElement($pricings), ]; } - /** - * Configure the tags - */ + public function setTags(array $topic = [], array $language = [], array $general = []): static + { + $this->topicTags = $topic; + $this->programmingLanguageTags = $language; + $this->generalTags = $general; + return $this; + } + public function configure(): Factory { - // Define your tags here - $tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', fake()->name(), fake()->name()]; - - // Create random tags for this resource - return $this->afterCreating(function (ComputerScienceResource $resource) use ($tags) { - $resource->topic_tags = fake()->randomElements($tags, fake()->numberBetween(3, count($tags))); - $resource->programming_language_tags = fake()->randomElements($tags); - $resource->general_tags = fake()->randomElements($tags); + return $this->afterCreating(function (ComputerScienceResource $resource) { + $fakerTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', fake()->word(), fake()->word()]; + + $resource->topic_tags = $this->topicTags ?? fake()->randomElements($fakerTags, fake()->numberBetween(3, count($fakerTags))); + $resource->programming_language_tags = $this->programmingLanguageTags ?? fake()->randomElements($fakerTags); + $resource->general_tags = $this->generalTags ?? fake()->randomElements($fakerTags); TagFrequencyChanged::dispatch(null, $resource->tagCounter()); }); diff --git a/tests/Feature/ResourceEditsTest.php b/tests/Feature/ResourceEditsTest.php index 8ba301ef..77561d76 100644 --- a/tests/Feature/ResourceEditsTest.php +++ b/tests/Feature/ResourceEditsTest.php @@ -153,8 +153,6 @@ public function test_can_post_valid_resource_edit(): void * 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(); diff --git a/tests/Feature/TagSearchTest.php b/tests/Feature/TagSearchTest.php new file mode 100644 index 00000000..56b7d9a6 --- /dev/null +++ b/tests/Feature/TagSearchTest.php @@ -0,0 +1,149 @@ +user = User::factory()->create(); + } + + + public function test_can_search_tags_by_prefix() + { + TagFrequencyChanged::dispatch(null, ['python' => 100, 'pygame' => 50, 'java' => 500]); + + $response = $this->getJson(route('tags.search', ['query' => 'py'])); + + $response->assertStatus(200); + $response->assertJsonCount(2, 'tags'); // only 'python' and 'pygame' + + $response->assertJsonFragment(['tag' => 'python', 'count' => 100]); + $response->assertJsonFragment(['tag' => 'pygame', 'count' => 50]); + + // Ensure order by count descending + $tags = collect($response->json('tags')); + $this->assertEquals(['python', 'pygame'], $tags->pluck('tag')->toArray()); + } + + public function test_query_too_long_returns_422() + { + $query = str_repeat('a', 51); // too long + + $response = $this->getJson(route('tags.search', ['query' => $query])); + $response->assertStatus(422); + } + + public function test_creating_resource_updates_tag_frequency() + { + $this->actingAs($this->user); + + $formData = ComputerScienceResourceTestResource::fake([ + 'topic_tags' => ['python', 'algorithms', 'java'], + 'programming_language_tags' => ['python'], + 'general_tags' => ['beginner'] + ]); + + $response = $this->postJson(route('resources.store'), $formData); + + $response->assertStatus(302); // a redirect after successful creation + $response->assertRedirect(); + + // Check that TagFrequency reflects counts + $this->assertDatabaseHas('tag_frequencies', ['tag' => 'python', 'count' => 2]); + $this->assertDatabaseHas('tag_frequencies', ['tag' => 'algorithms', 'count' => 1]); + $this->assertDatabaseHas('tag_frequencies', ['tag' => 'beginner', 'count' => 1]); + } + + public function test_dispatching_tag_frequency_change_removes_unused_tags() + { + // Step 1: Add initial tags via dispatch + TagFrequencyChanged::dispatch(null, [ + 'python' => 2, + 'java' => 1, + 'ruby' => 1, + ]); + + $this->assertDatabaseHas('tag_frequencies', ['tag' => 'python', 'count' => 2]); + $this->assertDatabaseHas('tag_frequencies', ['tag' => 'java', 'count' => 1]); + $this->assertDatabaseHas('tag_frequencies', ['tag' => 'ruby', 'count' => 1]); + + // Step 2: Dispatch with zero counts to simulate removal + TagFrequencyChanged::dispatch([ + 'python' => 2, + 'java' => 1, + 'ruby' => 1, + ], []); // no tags used now + + // Step 3: Ensure all tag frequencies are removed + $this->assertDatabaseMissing('tag_frequencies', ['tag' => 'python']); + $this->assertDatabaseMissing('tag_frequencies', ['tag' => 'java']); + $this->assertDatabaseMissing('tag_frequencies', ['tag' => 'ruby']); + + // Optional: search should return empty + $response = $this->getJson(route('tags.search', ['query' => 'py'])); + $response->assertStatus(200); + $this->assertEmpty($response->json('tags')); + } + + public function test_merging_edit_correctly_updates_tag_frequency() + { + $resource = ComputerScienceResource::factory() + ->setTags( + ['algorithms', 'tag1', 'tag2'], + ['c++'], + ['reference'] + ) + ->create(); + + $this->actingAs($this->user); + + // Create an edit with new tags + $editData = ComputerScienceResourceTestResource::fake([ + 'topic_tags' => ['python', 'algorithms', 'tag1'], // 'algorithms' already exists, 'python' is new + 'programming_language_tags' => ['python'], // replacing 'c++' + 'general_tags' => ['tutorial'], + ]); + $editData['edit_title'] = 'Tag Update'; + $editData['edit_description'] = 'Tag change for test'; + + // Create the edit + $response = $this->post(route('resource_edits.store', ['computerScienceResource' => $resource->id]), $editData); + $response->assertStatus(302); + + $edit = ResourceEdits::latest()->first(); + + // Mock approval + $this->instance(ResourceEditsService::class, Mockery::mock(ResourceEditsService::class, function ($mock) { + $mock->shouldReceive('canMergeEdits')->andReturnTrue(); + })); + + // Merge the edit + $mergeResponse = $this->post(route('resource_edits.merge', ['resourceEdits' => $edit->id])); + $mergeResponse->assertStatus(302); + + // TagFrequency should now reflect the changes + $this->assertEquals(2, TagFrequency::where('tag', 'python')->value('count')); + $this->assertEquals(1, TagFrequency::where('tag', 'algorithms')->value('count')); // Still used once + $this->assertDatabaseMissing('tag_frequencies', ['tag' => 'c++']); // Removed + $this->assertEquals(1, TagFrequency::where('tag', 'tutorial')->value('count')); + } +} diff --git a/tests/TestResources/ComputerScienceResourceTestResource.php b/tests/TestResources/ComputerScienceResourceTestResource.php index d1a2eec8..1ff62b06 100644 --- a/tests/TestResources/ComputerScienceResourceTestResource.php +++ b/tests/TestResources/ComputerScienceResourceTestResource.php @@ -2,7 +2,9 @@ namespace Tests\TestResources; +use App\Events\TagFrequencyChanged; use App\Models\ComputerScienceResource; +use Event; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -24,17 +26,20 @@ public function toArray(Request $request): array ]; } - public static function fake(): array + public static function fake(array $overrides = []): array { // Create the model with disabled events - $model = ComputerScienceResource::factory()->create(); - + $model = Event::fakeFor(function () { + return ComputerScienceResource::factory()->create(); + }, [TagFrequencyChanged::class]); + // Transform it to API form $formData = (new self($model))->toArray(request()); // Delete after getting the array to avoid polluting the DB $model->delete(); - return $formData; + // Merge and return + return array_merge($formData, $overrides); } } diff --git a/tests/TestResources/ResourceReviewTestResource.php b/tests/TestResources/ResourceReviewTestResource.php index d563ca80..1cf2eb1a 100644 --- a/tests/TestResources/ResourceReviewTestResource.php +++ b/tests/TestResources/ResourceReviewTestResource.php @@ -28,7 +28,7 @@ public function toArray(Request $request): array public static function fake(): array { - // Fake all events except the ones you still want to fire + // Fake certain events $model = Event::fakeFor(function () { return ResourceReview::factory()->create(); }, [ResourceReviewProcessed::class]);