Repository: IT-Academy-BCN/ita-wiki-backend
Pull request #29: Feature/bookmarks
Author: login: frankpulido
Created: 2025-03-13T20:41:27Z | Merged: 2025-03-18T07:13:10Z

[2025 03 17 : añado swagger... Falta ese enlace tan chulo para probar el endpoint]
Requerían adaptarse por la supresión del Role "anonymous" (tests)
app/Http/Controllers/BookmarkController.php
app/Http/Requests/BookmarkRequest.php
database/factories/ResourceFactory.php
tests/Feature/BookmarkControllerTest.php
tests/Feature/RoleControllerTest.php


Diff:
diff --git a/app/Http/Controllers/BookmarkController.php b/app/Http/Controllers/BookmarkController.php
index c4a227d4..da0c87f7 100644
--- a/app/Http/Controllers/BookmarkController.php
+++ b/app/Http/Controllers/BookmarkController.php
@@ -10,10 +10,30 @@
 
 class BookmarkController extends Controller
 {
+    /**
+     * @OA\Post(
+     *     path="/bookmarks",
+     *     summary="Create a bookmark",
+     *     tags={"Bookmarks"},
+     *     @OA\RequestBody(
+     *         required=true,
+     *         @OA\JsonContent(
+     *             required={"github_id","resource_id"},
+     *             @OA\Property(property="github_id", type="integer", example=6729608),
+     *             @OA\Property(property="resource_id", type="integer", example=11)
+     *         )
+     *     ),
+     *     @OA\Response(
+     *         response=201,
+     *         description="Created",
+     *         @OA\JsonContent(ref="#/components/schemas/Bookmark")
+     *     )
+     * )
+    */
+
     public function createStudentBookmark(BookmarkRequest $request)
     {
         $validated = $request->validated();
-
         $existingBookmark = Bookmark::where('github_id', $validated['github_id'])
         ->where('resource_id', $validated['resource_id'])
         ->first();
@@ -28,6 +48,29 @@ public function createStudentBookmark(BookmarkRequest $request)
         return response()->json($bookmark, 201);
     }
 
+    /**
+     * @OA\Delete(
+     *     path="/bookmarks",
+     *     summary="Delete a bookmark",
+     *     tags={"Bookmarks"},
+     *     @OA\RequestBody(
+     *         required=true,
+     *         @OA\JsonContent(
+     *             required={"github_id","resource_id"},
+     *             @OA\Property(property="github_id", type="integer", example=6729608),
+     *             @OA\Property(property="resource_id", type="integer", example=11)
+     *         )
+     *     ),
+     *     @OA\Response(
+     *         response=200,
+     *         description="Success",
+     *         @OA\JsonContent(
+     *             @OA\Property(property="message", type="string", example="Bookmark deleted successfully")
+     *         )
+     *     )
+     * )
+    */
+
     public function deleteStudentBookmark(BookmarkRequest $request)
     {
         $validated = $request->validated();
@@ -41,9 +84,33 @@ public function deleteStudentBookmark(BookmarkRequest $request)
         return response()->json(['error' => 'Bookmark not found'], 404);
     }
 
-    public function getStudentBookmarks(BookmarkRequest $request, $github_id)
+    /**
+     * @OA\Get(
+     *     path="/bookmarks/{github_id}",
+     *     summary="Get all bookmarks for a student",
+     *     tags={"Bookmarks"},
+     *     @OA\Parameter(
+     *         name="github_id",
+     *         in="path",
+     *         required=true,
+     *         @OA\Schema(type="integer", example=6729608)
+     *     ),
+     *     @OA\Response(
+     *         response=200,
+     *         description="Success",
+     *         @OA\JsonContent(
+     *             type="array",
+     *             @OA\Items(ref="#/components/schemas/Bookmark")
+     *         )
+     *     )
+     * )
+    */
+
+    public function getStudentBookmarks(BookmarkRequest $request)
     {
-        $bookmarks = Bookmark::where('github_id', $github_id)->get();
-        return response()->json($bookmarks, 200);
+        return response()->json(
+            Bookmark::where('github_id', $request->validated('github_id'))->get(),
+            200
+        );
     }
 }
\ No newline at end of file
diff --git a/app/Http/Requests/BookmarkRequest.php b/app/Http/Requests/BookmarkRequest.php
index 92441b45..bea1f87e 100644
--- a/app/Http/Requests/BookmarkRequest.php
+++ b/app/Http/Requests/BookmarkRequest.php
@@ -7,7 +7,8 @@
 use Illuminate\Foundation\Http\FormRequest;
 use App\Rules\GithubIdRule;
 use App\Rules\RoleStudentRule;
-//use App\Models\Role;
+use Illuminate\Contracts\Validation\Validator;
+use Illuminate\Http\Exceptions\HttpResponseException;
 
 class BookmarkRequest extends FormRequest
 {
@@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
     {
         if ($this->route('github_id')) {
             $this->merge(['github_id' => $this->route('github_id')]);
         }
+    }
 
+     public function rules(): array
+    {
         $rules = [
             'github_id' => [
                 new GithubIdRule(), // General validation for github_id
diff --git a/app/Models/Bookmark.php b/app/Models/Bookmark.php
index ac567570..4957de2c 100644
--- a/app/Models/Bookmark.php
+++ b/app/Models/Bookmark.php
@@ -7,6 +7,19 @@
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 
+/**
+ * @OA\Schema(
+ *     schema="Bookmark",
+ *     type="object",
+ *     title="Bookmark",
+ *     @OA\Property(property="id", type="integer", example=9),
+ *     @OA\Property(property="github_id", type="integer", description="Foreign key representing the GitHub ID of the user", example=6729608),
+ *     @OA\Property(property="resource_id", type="integer", description="Foreign key representing the ID of the bookmarked resource", example=11),
+ *     @OA\Property(property="created_at", type="string", format="date-time", example="2025-03-17T19:23:41.000000Z"),
+ *     @OA\Property(property="updated_at", type="string", format="date-time", example="2025-03-17T19:23:41.000000Z")
+ * )
+ */
+
 class Bookmark extends Model
 {
     use HasFactory;
diff --git a/database/factories/BookmarkFactory.php b/database/factories/BookmarkFactory.php
index 2e321dc1..6a9b0d5f 100644
--- a/database/factories/BookmarkFactory.php
+++ b/database/factories/BookmarkFactory.php
@@ -50,26 +50,5 @@ public function definition()
             'github_id' => $randomPair[0],
             'resource_id' => $randomPair[1],
         ];
-        /*
-        do {
-            $role = Role::where('role', 'student')->inRandomOrder()->first();
-            $resource = Resource::inRandomOrder()->first();
-        
-            if (!$role || !$resource) {
-                throw new \Exception('Roles or Resources table is empty.');
-            }
-
-            $github_id = $role->github_id;
-            $resource_id = $resource->id;
-
-        } while (Bookmark::where('github_id', $github_id)
-            ->where('resource_id', $resource_id)
-            ->exists());
-
-        return [
-            'github_id' => $github_id,
-            'resource_id' => $resource_id,
-        ];
-        */
     }
 }
\ No newline at end of file
diff --git a/database/factories/ResourceFactory.php b/database/factories/ResourceFactory.php
index 4584cc26..2275b68c 100644
--- a/database/factories/ResourceFactory.php
+++ b/database/factories/ResourceFactory.php
@@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
 
diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json
index 38639c7b..a4e3e21c 100755
--- a/storage/api-docs/api-docs.json
+++ b/storage/api-docs/api-docs.json
@@ -6,6 +6,135 @@
         "version": "1.0.0.0"
     },
     "paths": {
+        "/bookmarks": {
+            "post": {
+                "tags": [
+                    "Bookmarks"
+                ],
+                "summary": "Create a bookmark",
+                "operationId": "5df9fe3aa431ffd75eeed821c6c30074",
+                "requestBody": {
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "required": [
+                                    "github_id",
+                                    "resource_id"
+                                ],
+                                "properties": {
+                                    "github_id": {
+                                        "type": "integer",
+                                        "example": 6729608
+                                    },
+                                    "resource_id": {
+                                        "type": "integer",
+                                        "example": 11
+                                    }
+                                },
+                                "type": "object"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "201": {
+                        "description": "Created",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Bookmark"
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            "delete": {
+                "tags": [
+                    "Bookmarks"
+                ],
+                "summary": "Delete a bookmark",
+                "operationId": "156c8c638a41e9c78f8ab28de90dec7d",
+                "requestBody": {
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "required": [
+                                    "github_id",
+                                    "resource_id"
+                                ],
+                                "properties": {
+                                    "github_id": {
+                                        "type": "integer",
+                                        "example": 6729608
+                                    },
+                                    "resource_id": {
+                                        "type": "integer",
+                                        "example": 11
+                                    }
+                                },
+                                "type": "object"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "properties": {
+                                        "message": {
+                                            "type": "string",
+                                            "example": "Bookmark deleted successfully"
+                                        }
+                                    },
+                                    "type": "object"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/bookmarks/{github_id}": {
+            "get": {
+                "tags": [
+                    "Bookmarks"
+                ],
+                "summary": "Get all bookmarks for a student",
+                "operationId": "151fbc647d8dbc35020878212602047d",
+                "parameters": [
+                    {
+                        "name": "github_id",
+                        "in": "path",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "example": 6729608
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Bookmark"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
         "/api/resources": {
             "get": {
                 "summary": "Get all resources",
@@ -83,6 +212,36 @@
     },
     "components": {
         "schemas": {
+            "Bookmark": {
+                "title": "Bookmark",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "example": 9
+                    },
+                    "github_id": {
+                        "description": "Foreign key representing the GitHub ID of the user",
+                        "type": "integer",
+                        "example": 6729608
+                    },
+                    "resource_id": {
+                        "description": "Foreign key representing the ID of the bookmarked resource",
+                        "type": "integer",
+                        "example": 11
+                    },
+                    "created_at": {
+                        "type": "string",
+                        "format": "date-time",
+                        "example": "2025-03-17T19:23:41.000000Z"
+                    },
+                    "updated_at": {
+                        "type": "string",
+                        "format": "date-time",
+                        "example": "2025-03-17T19:23:41.000000Z"
+                    }
+                },
+                "type": "object"
+            },
             "Resource": {
                 "title": "Resource",
                 "properties": {
@@ -109,6 +268,48 @@
                         "format": "url",
                         "example": "https://www.hola.com",
                         "nullable": true
+                    },
+                    "category": {
+                        "type": "string",
+                        "enum": [
+                            "Node",
+                            "React",
+                            "Angular",
+                            "JavaScript",
+                            "Java",
+                            "Fullstack PHP",
+                            "Data Science",
+                            "BBDD"
+                        ],
+                        "example": "Node"
+                    },
+                    "theme": {
+                        "type": "string",
+                        "enum": [
+                            "All",
+                            "Components",
+                            "UseState & UseEffect",
+                            "Eventos",
+                            "Renderizado condicional",
+                            "Listas",
+                            "Estilos",
+                            "Debugging",
+                            "React Router"
+                        ],
+                        "example": "All"
+                    },
+                    "type": {
+                        "type": "string",
+                        "enum": [
+                            "Video",
+                            "Cursos",
+                            "Blog"
+                        ],
+                        "example": "Video"
+                    },
+                    "votes": {
+                        "type": "integer",
+                        "example": 1
                     }
                 },
                 "type": "object"
@@ -150,5 +351,11 @@
                 "type": "object"
             }
         }
-    }
+    },
+    "tags": [
+        {
+            "name": "Bookmarks",
+            "description": "Bookmarks"
+        }
+    ]
 }
\ No newline at end of file
diff --git a/tests/Feature/BookmarkControllerTest.php b/tests/Feature/BookmarkControllerTest.php
index e12b01c9..ca403f9c 100644
--- a/tests/Feature/BookmarkControllerTest.php
+++ b/tests/Feature/BookmarkControllerTest.php
@@ -13,7 +13,6 @@
 class BookmarkControllerTest extends TestCase
 {
     protected $student;
-    protected $anonymous;
     protected $resources;
     protected $bookmarks;
 
@@ -25,14 +24,8 @@ public function setUp(): void
             'github_id' => 9871315,
             'role' => 'student'
         ]);
-        $this->anonymous = Role::factory()->create([
-            'github_id' => 9861725,
-            'role' => 'anonymous'
-        ]);
 
-        $this->resources = Resource::factory(3)->create([
-            'github_id' => $this->student->github_id
-        ]);
+        $this->resources = Resource::factory(10)->create();
 
         $this->bookmarks = [
             Bookmark::create([
@@ -50,13 +43,13 @@ public function testGetStudentBookmarks(): void
         $response->assertStatus(200)
             ->assertJsonCount(2)
             ->assertJson([
-                ['github_id' => 9871315, 'resource_id' => $this->resources[0]->id],
-                ['github_id' => 9871315, 'resource_id' => $this->resources[1]->id]
+                ['github_id' => $this->student->github_id, 'resource_id' => $this->resources[0]->id],
+                ['github_id' => $this->student->github_id, 'resource_id' => $this->resources[1]->id]
             ]);
     }
 
     public function testGetBookmarksForUnexistentRoleFails(): void {
-        $nonExistentGithubId = 92920;
+        $nonExistentGithubId = 38928374;
         $response = $this->get('api/bookmarks/' . $nonExistentGithubId);
         $response->assertStatus(422);
     }
@@ -113,17 +106,4 @@ public function testCreateBookmarkForNonexistentResourceFails(): void {
         ]);
         $response->assertStatus(422);
     }
-
-    public function testCreateAnonymousBookmarkFails() : void {
-        $response = $this->post('api/bookmarks', [
-            'github_id' => $this->anonymous->github_id,
-            'resource_id' => 2]);
-        $response->assertStatus(422);
-    }
-
-    public function testGetAnonymousBookmarksFails(): void
-    {
-        $response = $this->get('api/bookmarks/' . $this->anonymous->github_id);
-        $response->assertStatus(422);
-    }
 }
\ No newline at end of file
diff --git a/tests/Feature/RoleControllerTest.php b/tests/Feature/RoleControllerTest.php
index aed0c005..6764077d 100644
--- a/tests/Feature/RoleControllerTest.php
+++ b/tests/Feature/RoleControllerTest.php
@@ -36,19 +36,11 @@ public function testCanGetRoleByGithubId(): void
         ]);
     }
 
-    public function testSignsUpAsAnonymous(): void
+    public function testUnexistentGithubIdSignsUpFails(): void
     {
         $random_github_id = random_int(1, 10000000);
-        $response = $this->get('/api/users/user-signedin-as?github_id=' . $random_github_id);
-        $response->assertStatus(201)
-        ->assertJsonStructure(['message', 'role'])
-        ->assertJson([
-            'message' => 'Role not found. Created as new anonymous user.',
-            'role' => [
-                'github_id' => $random_github_id,
-                'role' => 'anonymous' 
-            ]
-        ]);
+        $response = $this->get('/api/users/' . $random_github_id);
+        $response->assertStatus(404);
     }
 
 }


Reviews:
user: login: llotjer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T08:45:08Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T09:06:55Z
user: login: llotjer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T09:13:50Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T09:22:14Z
user: login: llotjer
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T09:23:45Z
user: login: llotjer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T09:25:45Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T09:36:10Z
user: login: diegdar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:37:11Z
user: login: diegdar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:42:06Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T17:13:46Z
user: login: diegdar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T17:17:00Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T17:19:00Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T17:19:17Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T17:22:19Z
user: login: diegdar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T18:57:04Z
user: login: diegdar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T18:57:36Z
user: login: diegdar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T19:00:03Z
user: login: diegdar
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T19:00:25Z
user: login: CotaGM
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-16T19:14:27Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T07:50:15Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T07:53:33Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T08:10:10Z
user: login: jordimorillo
body: Te he puesto un par de observaciones más, pero prioriza la publicación.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:41:58Z
user: login: frankpulido
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T07:11:34Z

Review comments:
diff_hunk: @@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
path: database/factories/ResourceFactory.php
user: login: llotjer
body: Aquí estás limitando la creación de recursos a student solo, y los mentores también pueden tener recursos asociados no?
created_at: 2025-03-14T08:45:08Z
author_association: COLLABORATOR
diff_hunk: @@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
path: database/factories/ResourceFactory.php
user: login: frankpulido
body: Hola Xavi, me he percatado de ello, seguramente todos los roles podrán hacerlo y al no existir anónimo ya no habrá que poner ninguna restricción, pero como no tengo del todo claro que clase de usuarios podrán crear resursos y cuales no, no he querido ir por libre... ;P
Esta tarea no fue modificada :
#45 Como alumno, puedo visualizar mis recursos creados
https://tree.taiga.io/project/luisvi-ita-wiki/us/45?milestone=435721
No tengo claro que cualquiera vaya a poder crear recursos...
Si me lo indicáis así, puedo poner un resource factory  con rol random
created_at: 2025-03-14T09:06:54Z
author_association: COLLABORATOR
diff_hunk: @@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
path: database/factories/ResourceFactory.php
user: login: llotjer
body: Creo que con añadir mentor así:

$role = Role::where('role', '=', 'student', 'mentor')
        ->inRandomOrder()
        ->first();
        
ya habría suficiente, y si quieres asegurarte que se genere uno, en el caso que no encontrara ninguno de los dos, puedes añadir esto después de first:

->first() ?? Role::create(['role' => 'student']);

Así siempre habrá por lo menos un student. (o cambiarlo por mentor si se quiere, pero como dices, hasta ahora solo se ha especificado que sean los students los que pueden tener resources asociados).
created_at: 2025-03-14T09:13:50Z
author_association: COLLABORATOR
diff_hunk: @@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
path: database/factories/ResourceFactory.php
user: login: frankpulido
body: Estupendo, pondré esto :
$role = Role::where('role', '=', 'student', 'mentor')
student seguro que hay uno porque lo siembra antes la migración 'roles' y me parece que genera 20 registros en total...
Espero por si hay más sugerencias antes de hacer el push...
(estamos seguros que los mentores crean recursos, no?... el Sprint 5 no lo dice)
created_at: 2025-03-14T09:22:14Z
author_association: COLLABORATOR
diff_hunk: @@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
path: database/factories/ResourceFactory.php
user: login: llotjer
body: A mi me parecería lo más normal, pero es cierto que si no está escrito en la tarea, no existe.
created_at: 2025-03-14T09:25:45Z
author_association: COLLABORATOR
diff_hunk: @@ -19,7 +19,7 @@ class ResourceFactory extends Factory
      */
     public function definition(): array
     {
-        $role = Role::where('role', '!=', 'anonymous')
+        $role = Role::where('role', '=', 'student')
         ->inRandomOrder()
         ->first();
path: database/factories/ResourceFactory.php
user: login: frankpulido
body: OK, lo dejamos como está de momento a menos que Jordi sentencie a favor del cambio...
created_at: 2025-03-14T09:36:10Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
path: app/Http/Requests/BookmarkRequest.php
user: login: diegdar
body: No entiendo para que creaste este metodo, no lo estas utilizando en ninguna parte.
created_at: 2025-03-14T16:25:58Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
     {
         if ($this->route('github_id')) {
             $this->merge(['github_id' => $this->route('github_id')]);
         }
+    }
 
+     public function rules(): array
+    {
         $rules = [
             'github_id' => [
                 new GithubIdRule(), // General validation for github_id
path: app/Http/Requests/BookmarkRequest.php
user: login: diegdar
body: -las reglas que has creado en esta GithubIdRule no hacia falta abstraerlas en una clase si ya puedes utilizar las que te ofrece laravel, creo que has hecho sobre ingenieria.

-En RoleStudentRule has especificado que solo un student puede crear bookmarks, porque no podria hacerlo tambien un mentor?
created_at: 2025-03-14T16:30:18Z
author_association: COLLABORATOR
diff_hunk: @@ -50,26 +50,5 @@ public function definition()
             'github_id' => $randomPair[0],
path: database/factories/BookmarkFactory.php
user: login: diegdar
body: Muy bien trabajo el factory, me ha gustado mucho como te ha quedado.
created_at: 2025-03-14T16:42:06Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
path: app/Http/Requests/BookmarkRequest.php
user: login: frankpulido
body: Se usa aunque no se invoque
created_at: 2025-03-14T17:13:46Z
author_association: COLLABORATOR
diff_hunk: @@ -43,4 +48,12 @@ public function rules(): array
 
         return $rules;
     }
+
+    public function failedValidation(Validator $validator)
path: app/Http/Requests/BookmarkRequest.php
user: login: diegdar
body: este método no haría falta pues ya se establece una configuracion para los errores 422 en el archivo bootstrap/app
created_at: 2025-03-14T17:16:20Z
author_association: COLLABORATOR
diff_hunk: @@ -50,26 +50,5 @@ public function definition()
             'github_id' => $randomPair[0],
path: database/factories/BookmarkFactory.php
user: login: frankpulido
body: gracias!
created_at: 2025-03-14T17:19:00Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
     {
         if ($this->route('github_id')) {
             $this->merge(['github_id' => $this->route('github_id')]);
         }
+    }
 
+     public function rules(): array
+    {
         $rules = [
             'github_id' => [
                 new GithubIdRule(), // General validation for github_id
path: app/Http/Requests/BookmarkRequest.php
user: login: frankpulido
body: Xavi y yo estuvimos viendo esta mañana que la historia de usuario sólo lo requería para el estudiante (a menos que se haya cambiado en el transcurso del día)
![Screenshot 2025-03-14 at 18 17 56](https://github.com/user-attachments/assets/f0dc26ec-8c8b-42f6-b1b3-be5cb8f113a4)

created_at: 2025-03-14T17:19:17Z
author_association: COLLABORATOR
diff_hunk: @@ -43,4 +48,12 @@ public function rules(): array
 
         return $rules;
     }
+
+    public function failedValidation(Validator $validator)
path: app/Http/Requests/BookmarkRequest.php
user: login: frankpulido
body: no la había usado antes, la puse porque vi que la usabáis vosotros
created_at: 2025-03-14T17:22:19Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
     {
         if ($this->route('github_id')) {
             $this->merge(['github_id' => $this->route('github_id')]);
         }
+    }
 
+     public function rules(): array
+    {
         $rules = [
             'github_id' => [
                 new GithubIdRule(), // General validation for github_id
path: app/Http/Requests/BookmarkRequest.php
user: login: diegdar
body: vale, tienes razon, en ese caso la historia de usuario manda
created_at: 2025-03-14T18:57:04Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
path: app/Http/Requests/BookmarkRequest.php
user: login: diegdar
body: ya me contaras como lo hace y para que se usa porque no termino pillarlo.
created_at: 2025-03-14T18:57:36Z
author_association: COLLABORATOR
diff_hunk: @@ -43,4 +48,12 @@ public function rules(): array
 
         return $rules;
     }
+
+    public function failedValidation(Validator $validator)
path: app/Http/Requests/BookmarkRequest.php
user: login: diegdar
body: si, yo lo hice en mi issue anterior pero en esta me di cuenta que no hacia falta, pues con configurarlo una vez en el archivo app.php se gestionan todas los errores 422 y ya no haria falta ponerlo cada FormRequest
created_at: 2025-03-14T19:00:03Z
author_association: COLLABORATOR
path: tests/Feature/RoleControllerTest.php
user: login: CotaGM
body: Hola Frank :) Me imagino que este test lo modificaste para que pasaran todos. De todas formas sería momentáneo, ya que este test en teoría ahora con el cambio hecho en el controller ya no debería existir. Espero mejor a que hagas merge para trabajar desde tu cambio.
created_at: 2025-03-16T19:14:27Z
author_association: COLLABORATOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
path: app/Http/Requests/BookmarkRequest.php
user: login: frankpulido
body: holaa, es la primera vez que la uso. aquí se explica con caso práctico :
https://medium.com/@aiman.asfia/a-guide-to-understanding-and-utilizing-prepareforvalidation-in-laravel-926f156650d5
created_at: 2025-03-17T07:50:14Z
author_association: COLLABORATOR
path: tests/Feature/RoleControllerTest.php
user: login: frankpulido
body: hola Cota... ya no sé que conviene porque todos los que tenías tareas nuevas habéis avanzado mucho y estáis listos para mergear y probar el front en producción... igual debo mergear el último (?!)...
created_at: 2025-03-17T07:53:33Z
author_association: COLLABORATOR
diff_hunk: @@ -43,4 +48,12 @@ public function rules(): array
 
         return $rules;
     }
+
+    public function failedValidation(Validator $validator)
path: app/Http/Requests/BookmarkRequest.php
user: login: frankpulido
body: hecho :) ... cuando enviaste el comentario ya había salido de barcelona
created_at: 2025-03-17T08:10:10Z
author_association: COLLABORATOR
diff_hunk: @@ -41,9 +84,32 @@ public function deleteStudentBookmark(BookmarkRequest $request)
         return response()->json(['error' => 'Bookmark not found'], 404);
     }
 
+    /**
+     * @OA\Get(
+     *     path="/bookmarks/{github_id}",
+     *     summary="Get all bookmarks for a student",
+     *     tags={"Bookmarks"},
+     *     @OA\Parameter(
+     *         name="github_id",
+     *         in="path",
+     *         required=true,
+     *         @OA\Schema(type="integer", example=6729608)
+     *     ),
+     *     @OA\Response(
+     *         response=200,
+     *         description="Success",
+     *         @OA\JsonContent(
+     *             type="array",
+     *             @OA\Items(ref="#/components/schemas/Bookmark")
+     *         )
+     *     )
+     * )
+    */
+
     public function getStudentBookmarks(BookmarkRequest $request, $github_id)
path: app/Http/Controllers/BookmarkController.php
user: login: jordimorillo
body: En el método `getStudentBookmarks`, se espera que `$github_id` se pase a través de la solicitud. Sin embargo, la validación ya se realiza a través del `BookmarkRequest`. Dado que `$github_id` es un parámetro de la ruta, no necesitas validarlo nuevamente aquí. Sería mejor no hacer la validación del `$request` si el `$github_id` se está tomando directamente de la ruta.
created_at: 2025-03-17T20:33:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,12 +25,16 @@ public function authorize(): bool
      *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
-    public function rules(): array
+    
+     protected function prepareForValidation()
path: app/Http/Requests/BookmarkRequest.php
user: login: jordimorillo
body: Es recomendable seguir las convenciónes de estilo de código de PSR-12. En tu código hay algunas inconsistencias en la indentación y el espaciado. Por ejemplo, asegúrate de dejar un espacio después de cada bloque, como después del método `prepareForValidation`.
created_at: 2025-03-17T20:35:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,9 +84,32 @@ public function deleteStudentBookmark(BookmarkRequest $request)
         return response()->json(['error' => 'Bookmark not found'], 404);
     }
 
+    /**
+     * @OA\Get(
+     *     path="/bookmarks/{github_id}",
+     *     summary="Get all bookmarks for a student",
+     *     tags={"Bookmarks"},
+     *     @OA\Parameter(
+     *         name="github_id",
+     *         in="path",
+     *         required=true,
+     *         @OA\Schema(type="integer", example=6729608)
+     *     ),
+     *     @OA\Response(
+     *         response=200,
+     *         description="Success",
+     *         @OA\JsonContent(
+     *             type="array",
+     *             @OA\Items(ref="#/components/schemas/Bookmark")
+     *         )
+     *     )
+     * )
+    */
+
     public function getStudentBookmarks(BookmarkRequest $request, $github_id)
path: app/Http/Controllers/BookmarkController.php
user: login: frankpulido
body: modificado!!
created_at: 2025-03-18T07:11:34Z
author_association: COLLABORATOR

PR comments:
user: login: frankpulido
created_at: 2025-03-14T17:20:46Z
body: salgo de viaje, alguien más se tendrá que ocupar de hacer el merge, si se aprueba
esta PR no estaba programada, la motivó la necesidad de corregir los tests por la eliminación del rol "anónimo"
author_association: COLLABORATOR
user: login: diegdar
created_at: 2025-03-14T18:58:17Z
body: te apruebo la PR, pero mirate lo que te deje comentado antes de hacer merge.
author_association: COLLABORATOR