diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 8b9cdf94..535d56f7 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -7,12 +7,9 @@ use App\Http\Resources\CommentResource; use App\Models\Comment; use App\Services\ModelResolverService; -use Illuminate\Http\Request; use Illuminate\Database\Eloquent\Collection; use App\Http\Resources\UserResource; -use App\Models\CommentsCount; use App\Services\CommentService; -use DB; use Auth; use Illuminate\Validation\Rule; use Log; @@ -42,10 +39,17 @@ public function store(StoreCommentRequest $request) $comment->content = $validatedData['content']; $comment->user_id = Auth::id(); - // Set the commentable type $commentableType = $this->modelResolver->getModelClass($validatedData['commentable_type']); $commentableId = $validatedData['commentable_id']; + + // Ensure that the model exists + $model = $this->modelResolver->resolve($validatedData['commentable_type'], $commentableId); + if (!$model) { + return response()->json(['message' => 'Model not found'], 404); + } + Log::debug("Resolved model class: " . $commentableType); + // Set the commentable type $comment->commentable_type = $commentableType; $comment->commentable_id = $commentableId; diff --git a/app/Http/Controllers/UpvoteController.php b/app/Http/Controllers/UpvoteController.php index c0152cc5..e2961ab2 100644 --- a/app/Http/Controllers/UpvoteController.php +++ b/app/Http/Controllers/UpvoteController.php @@ -7,11 +7,11 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Illuminate\Validation\Rule; class UpvoteController extends Controller { protected $modelResolver; - protected $upvotableTypes = 'review,resource,comment,edit'; function __construct(ModelResolverService $modelResolver) { @@ -28,7 +28,7 @@ public function upvote($type, $id) 'type' => $type, ], [ - 'type' => 'required|in:' . $this->upvotableTypes, + 'type' => ['required', Rule::in(config('upvotes.upvotable_types'))] ] )->validate(); @@ -57,7 +57,7 @@ public function downvote($type, $id) 'type' => $type, ], [ - 'type' => 'required|in:' . $this->upvotableTypes, + 'type' => ['required', Rule::in(config('upvotes.upvotable_types'))] ] )->validate(); diff --git a/app/Http/Requests/Comment/StoreCommentRequest.php b/app/Http/Requests/Comment/StoreCommentRequest.php index f86a00d5..d961b4ad 100644 --- a/app/Http/Requests/Comment/StoreCommentRequest.php +++ b/app/Http/Requests/Comment/StoreCommentRequest.php @@ -5,7 +5,7 @@ use App\Services\ModelResolverService; use Illuminate\Foundation\Http\FormRequest; use Auth; -use Closure; +use Illuminate\Validation\Rule; class StoreCommentRequest extends FormRequest { @@ -37,20 +37,7 @@ public function rules(): array "commentable_type" => [ 'required', 'string', - function (string $_attribute, mixed $value, Closure $fail) { - if (!in_array($value, config('comment.commentable_types'))) - { - $fail("Not a valid commentable type"); - } - - $id = request('commentable_id'); - $model = $this->modelResolver->resolve($value, $id); - - if ($model == null) - { - $fail("commentable id and type does not exist."); - } - }, + Rule::in(config('comment.commentable_types')), ], "content" => ["required", "string", "max:4000"], "parent_comment_id" => ["nullable", "exists:App\Models\Comment,id"] diff --git a/app/Services/ModelResolverService.php b/app/Services/ModelResolverService.php index 3b7cd055..58b78191 100644 --- a/app/Services/ModelResolverService.php +++ b/app/Services/ModelResolverService.php @@ -15,6 +15,9 @@ class ModelResolverService /** * Finds the model that exists for the given type and id * + * @param $type, the colloquial name for the type + * @param $id, the id for the type + * * returns null if no model exists, otherwise, it will return the model */ public function resolve($type, $id) diff --git a/config/upvotes.php b/config/upvotes.php new file mode 100644 index 00000000..2c8edb6e --- /dev/null +++ b/config/upvotes.php @@ -0,0 +1,5 @@ + ['review', 'comment', 'edit', 'resource'] +]; \ No newline at end of file diff --git a/database/factories/CommentFactory.php b/database/factories/CommentFactory.php index fc49d82b..e9809318 100644 --- a/database/factories/CommentFactory.php +++ b/database/factories/CommentFactory.php @@ -3,11 +3,10 @@ namespace Database\Factories; use App\Events\CommentCreated; -use App\Models\ComputerScienceResource; use Illuminate\Database\Eloquent\Factories\Factory; use App\Models\User; use App\Models\Comment; -use App\Models\ResourceReview; +use App\Services\ModelResolverService; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment> @@ -19,55 +18,44 @@ class CommentFactory extends Factory * * @return array */ + + // TODO: Double check this logic public function definition(): array { - // Set the random commentable type. - $commentableType = $this->faker->randomElement([ - ResourceReview::class, - Comment::class, - ComputerScienceResource::class, - ]); - - // Create the commented type - $commenting = isset($models[$commentableType]) - ? $models[$commentableType]::inRandomOrder()->first() ?? $models[$commentableType]::factory()->create() - : null; + // Pick a random commentable type from config. + $commentableName = $this->faker->randomElement(['comment', 'resource']); + $modelResolver = app(ModelResolverService::class); + $modelClass = $modelResolver->getModelClass($commentableName); - - $commentableId = $commenting->id; - - // Get a random user (or create one if necessary). - $user = User::inRandomOrder()->first() - ?? User::factory()->create(); - - // Randomly decide if this is a reply comment. - $isReply = $this->faker->boolean; - - $parent = null; - if ($isReply) { - // Try to find an existing comment on the same resource. - $parent = Comment::where('commentable_type', $commentableType) - ->where('commentable_id', $commentableId) - ->inRandomOrder() - ->first(); - } + // Use an existing user or create one. + $user = User::inRandomOrder()->first() ?? User::factory()->create(); + + // If the commentable type is a Comment, it means this new comment is a reply. + if ($modelClass === Comment::class) { + // Get an existing comment or create one if none exists. + $existingComment = Comment::inRandomOrder()->first() ?? Comment::factory()->create(); - if ($parent) { - // Set the parent comment id. + $commentableId = $existingComment->commentable_id; + $commentableType = $existingComment->commentable_type; + + // Since it's a recursive comment, the existing comment becomes the parent. + $parent = $existingComment; $parentCommentId = $parent->id; - - // Get the parent's root, and set that as this comment's root, unless it is the root itself. - $rootCommentId = ($parent->depth == 0) ? $parent->id : $parent->root_comment_id; - - // Set the new depth. + // If parent's depth is 1, it is the root; otherwise, use its stored root. + $rootCommentId = ($parent->depth == 1) ? $parent->id : $parent->root_comment_id; $depth = $parent->depth + 1; } else { - // Top-level comment. + // For non-comment targets, fetch or create the commentable model. + $commenting = $modelClass::inRandomOrder()->first() ?? $modelClass::factory()->create(); + $commentableId = $commenting->id; + $commentableType = $modelClass; + + // For non-comment targets we always create a top-level comment. $parentCommentId = null; $rootCommentId = null; - $depth = 0; + $depth = 1; } - + return [ 'user_id' => $user->id, 'content' => $this->faker->paragraph, @@ -79,7 +67,7 @@ public function definition(): array 'children_count' => 0, ]; } - + public function configure() { return $this->afterCreating(function (Comment $comment) { diff --git a/database/factories/ResourceEditsFactory.php b/database/factories/ResourceEditsFactory.php index 9f0b2258..a565691f 100644 --- a/database/factories/ResourceEditsFactory.php +++ b/database/factories/ResourceEditsFactory.php @@ -2,22 +2,44 @@ namespace Database\Factories; +use App\Models\ResourceEdits; +use App\Models\User; +use App\Models\ComputerScienceResource; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ResourceEdits> + * @extends Factory */ class ResourceEditsFactory extends Factory { - /** - * Define the model's default state. - * - * @return array - */ + protected $model = ResourceEdits::class; + public function definition(): array { + $platforms = config('computerScienceResource.platforms'); + $difficulties = config('computerScienceResource.difficulties'); + $pricings = config('computerScienceResource.pricings'); + return [ - // + 'computer_science_resource_id' => ComputerScienceResource::factory(), + 'user_id' => User::factory(), + + 'edit_title' => $this->faker->sentence, + 'edit_description' => $this->faker->paragraph, + + // Copied fields from the resource + 'name' => $this->faker->name(), + 'description' => $this->faker->realText(), + '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' => $this->faker->url(), + + 'platforms' => $this->faker->randomElements($platforms, rand(1, 3)), + 'difficulty' => $this->faker->randomElement($difficulties), + 'pricing' => $this->faker->randomElement($pricings), + + 'topic_tags' => ['data structures', 'algorithms'], + 'programming_language_tags' => ['python'], + 'general_tags' => ['interactive', 'challenging'], ]; } } diff --git a/database/seeders/CommentSeeder.php b/database/seeders/CommentSeeder.php index b89e1ae2..27d5e138 100644 --- a/database/seeders/CommentSeeder.php +++ b/database/seeders/CommentSeeder.php @@ -13,6 +13,6 @@ class CommentSeeder extends Seeder */ public function run(): void { - Comment::factory(10)->create(); + Comment::factory(100)->create(); } } diff --git a/database/seeders/ComputerScienceResourceSeeder.php b/database/seeders/ComputerScienceResourceSeeder.php index 3a1b859a..af3defe5 100644 --- a/database/seeders/ComputerScienceResourceSeeder.php +++ b/database/seeders/ComputerScienceResourceSeeder.php @@ -15,6 +15,6 @@ class ComputerScienceResourceSeeder extends Seeder public function run(): void { Log::info('Running ComputerScienceResourceSeeder'); - ComputerScienceResource::factory(50)->create(); + ComputerScienceResource::factory(1)->create(); } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index c2c440f4..d85a8485 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,12 +2,12 @@ namespace Database\Seeders; -use App\Models\ResourceEdits; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Database\Seeders\ComputerScienceResourceSeeder; use Database\Seeders\UserSeeder; use Database\Seeders\ResourceReviewSeeder; +use Database\Seeders\ResourceEditsSeeder; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder @@ -21,6 +21,7 @@ public function run(): void UserSeeder::class, ComputerScienceResourceSeeder::class, ResourceReviewSeeder::class, + ResourceEditsSeeder::class, CommentSeeder::class, ]); } diff --git a/database/seeders/ResourceEditsSeeder.php b/database/seeders/ResourceEditsSeeder.php index 5d0fd441..5483162b 100644 --- a/database/seeders/ResourceEditsSeeder.php +++ b/database/seeders/ResourceEditsSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\ResourceEdits; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -12,6 +13,6 @@ class ResourceEditsSeeder extends Seeder */ public function run(): void { - + ResourceEdits::factory(10)->create(); } } diff --git a/database/seeders/ResourceReviewSeeder.php b/database/seeders/ResourceReviewSeeder.php index aac48f5f..2d9a08ba 100644 --- a/database/seeders/ResourceReviewSeeder.php +++ b/database/seeders/ResourceReviewSeeder.php @@ -13,6 +13,6 @@ class ResourceReviewSeeder extends Seeder */ public function run(): void { - ResourceReview::factory(50)->create(); + ResourceReview::factory(5)->create(); } } diff --git a/tests/Feature/CommentsTest.php b/tests/Feature/CommentsTest.php index 0329d892..9b07669d 100644 --- a/tests/Feature/CommentsTest.php +++ b/tests/Feature/CommentsTest.php @@ -5,6 +5,7 @@ use App\Models\Comment; use App\Models\User; use App\Models\ComputerScienceResource; +use App\Services\ModelResolverService; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -77,7 +78,43 @@ public function test_cannot_comment_on_non_existent_resource() ]; $response = $this->postJson(route('comments.store'), $payload); - $response->assertStatus(422); + $response->assertStatus(404); + } + + /** + * Test that commenting works on all commentable types defined in config. + */ + // TODO: + public function test_can_comment_all_commentable_types() + { + $user = User::factory()->create(); + $this->actingAs($user); + + foreach (config('comment.commentable_types') as $typeKey) { + $modelClass = app(ModelResolverService::class)->getModelClass($typeKey); + + // Skip comments + if ($modelClass === Comment::class) { + continue; + } + + $commentable = $modelClass::factory()->create(); + $payload = [ + 'content' => 'top level comment', + 'commentable_type' => $typeKey, + 'commentable_id' => $commentable->id, + 'parent_comment_id' => null, + ]; + + $response = $this->postJson(route('comments.store'), $payload); + $response->assertStatus(200, "Failed to comment on type {$typeKey}"); + + $this->assertDatabaseHas('comments', [ + 'content' => $payload['content'], + 'commentable_type' => $modelClass, + 'commentable_id' => $payload['commentable_id'], + ]); + } } /** @@ -97,7 +134,7 @@ public function test_cannot_reply_on_non_existent_comment() ]; $response = $this->postJson(route('comments.store'), $payload); - $response->assertStatus(422); + $response->assertStatus(404); } /** diff --git a/tests/Feature/ResourceReviewsTest.php b/tests/Feature/ResourceReviewsTest.php new file mode 100644 index 00000000..8be0005a --- /dev/null +++ b/tests/Feature/ResourceReviewsTest.php @@ -0,0 +1,30 @@ +get('/'); + + $response->assertStatus(200); + } + + // Test this: + // Cannot post any form of invalid data + // Do 3 tests of invalid data testing + + + // test that the resource's average has changed + + // Test that the resource review can be posted + // +} diff --git a/tests/Feature/UpvoteTest.php b/tests/Feature/UpvoteTest.php index 2711f0cb..369ab700 100644 --- a/tests/Feature/UpvoteTest.php +++ b/tests/Feature/UpvoteTest.php @@ -4,6 +4,7 @@ use App\Models\ComputerScienceResource; use App\Models\User; +use App\Services\ModelResolverService; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -23,6 +24,47 @@ public function test_upvote_on_invalid_type_returns_422() $response->assertStatus(422); } + /** + * Test upvote can only be done on an existing upvotable type and id. + */ + public function test_upvote_on_non_existing_type_returns_422() + { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->postJson(route('upvote', ['type' => 'resource', 'id' => 1])); + $response->assertStatus(404); + } + + /** + * Test that upvoting works on all upvotable types defined in config. + */ + public function test_can_upvote_all_upvotable_types() + { + $user = User::factory()->create(); + $this->actingAs($user); + + foreach (config('upvotes.upvotable_types') as $typeKey) { + // Get the Model service with app + $modelClass = app(ModelResolverService::class)->getModelClass($typeKey); // Resolve the model class. + + $model = $modelClass::factory()->create(); + + $response = $this->postJson(route('upvote', [ + 'type' => $typeKey, + 'id' => $model->id, + ])); + + $response->assertStatus(200); + $this->assertDatabaseHas('upvotes', [ + 'user_id' => $user->id, + 'upvotable_type' => $modelClass, + 'upvotable_id' => $model->id, + 'value' => 1, + ]); + } + } + /** * Test that a user must be logged in to upvote. */ @@ -171,7 +213,7 @@ public function test_upvote_and_downvote_sum_to_zero() } // Check if the final score is zero - $this->assertEquals(3, $resource->upvoteSummary->upvotes); + $this->assertEquals(3, $resource->upvoteSummary->upvotes); $this->assertEquals(3, $resource->upvoteSummary->downvotes); $this->assertEquals(0, $resource->upvoteSummary->voteScore); }