diff --git a/app/Http/Controllers/API/ConceptController.php b/app/Http/Controllers/API/ConceptController.php index 564ed56..0ad56e6 100644 --- a/app/Http/Controllers/API/ConceptController.php +++ b/app/Http/Controllers/API/ConceptController.php @@ -113,7 +113,7 @@ public function store(ConceptStoreRequest $request) DB::commit(); return response()->json([ "id" => $concept->id, - ]); + ], 201); } catch (\Throwable $th) { DB::rollback(); @@ -139,12 +139,11 @@ public function show(Concept $concept) * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request - * @param int $id + * @param \App\Concept $concept * @return \Illuminate\Http\Response */ - public function update(Request $request, int $id) + public function update(Request $request, Concept $concept) { - $concept = Concept::findOrFail($id); $attributes = $request->all(); // Sync concept categories @@ -163,15 +162,49 @@ public function update(Request $request, int $id) return new Response($concept); } + /** + * Relate Concepts + * + * @param \Illuminate\Http\Request $request + * @param \App\Concept $concept + * @return \Illuminate\Http\Response + */ + public function relateConcepts(Request $request, Concept $concept) + { + if ($request->user()->cannot('update', $concept)) { + abort(403); + } + + $relation_type = $request->input('relation_type'); + $related_id = $request->input('related_id'); + + switch ($relation_type) { + case "broader": + $concept->addBroader($related_id); + break; + case "narrower": + $concept->addNarrower($related_id); + break; + case "related": + $concept->addRelated($related_id); + break; + } + + return $concept; + } + /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ - public function deprecate(Request $request, $id) + public function deprecate(Request $request, Concept $concept) { - $concept = Concept::findOrFail($id); + if ($request->user()->cannot('update', $concept)) { + abort(403); + } + $to = $request->input('to'); if ($to) { $replaceConcept = Concept::findOrFail($to); @@ -180,7 +213,8 @@ public function deprecate(Request $request, $id) $concept->deprecated = !$concept->deprecated; $concept->save(); } - return $concept->deprecated ? 'true' : 'false'; + + return response()->json($concept, 200); } /** @@ -189,9 +223,11 @@ public function deprecate(Request $request, $id) * @param int $id * @return \Illuminate\Http\Response */ - public function destroy($id) + public function destroy(Concept $concept) { - // + $concept->conceptCategories()->detach(); + $concept->terms()->delete(); + $concept->delete(); } /** diff --git a/app/Http/Controllers/ConceptController.php b/app/Http/Controllers/ConceptController.php index 1823c01..be4dc9a 100644 --- a/app/Http/Controllers/ConceptController.php +++ b/app/Http/Controllers/ConceptController.php @@ -223,33 +223,4 @@ public function search(Concept $concept) return $terms->get(); } - - /** - * Relate Concepts - * - * @param \App\Concept $concept - * @return \Illuminate\Http\Response - */ - public function relateConcepts($concept_id) - { - $relation_type = $_GET["relation_type"]; - $related_id = $_GET["related_id"]; - - $concept = Concept::findOrFail($concept_id); - - switch ($relation_type) { - case "broader": - $concept->addBroader($related_id); - break; - case "narrower": - $concept->addNarrower($related_id); - break; - case "related": - $concept->addRelated($related_id); - break; - } - - return $concept; - } - } diff --git a/app/Policies/ConceptPolicy.php b/app/Policies/ConceptPolicy.php index 430b096..62ce152 100644 --- a/app/Policies/ConceptPolicy.php +++ b/app/Policies/ConceptPolicy.php @@ -10,7 +10,7 @@ class ConceptPolicy /** * Determine whether the user can view any models. */ - public function viewAny(User $user): bool + public function viewAny(?User $user): bool { return true; } @@ -18,7 +18,7 @@ public function viewAny(User $user): bool /** * Determine whether the user can view the model. */ - public function view(User $user, Concept $concept): bool + public function view(?User $user, Concept $concept): bool { return true; } diff --git a/routes/api.php b/routes/api.php index a8b3ec3..a4caa45 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,7 +22,7 @@ Route::get('concepts/reconcile/{id}', 'API\ConceptController@reconcile'); Route::get('concepts/reconcile', 'API\ConceptController@reconcile'); -Route::put('concepts/{id}/relate_concept', 'ConceptController@relateConcepts'); +Route::put('concepts/{id}/relate_concept', 'API\ConceptController@relateConcepts'); Route::put('concepts/{id}/deprecate', 'API\ConceptController@deprecate'); Route::apiResource('concepts', 'API\ConceptController'); diff --git a/tests/Feature/API/ConceptsTest.php b/tests/Feature/API/ConceptsTest.php index 597a1b2..f1c7797 100644 --- a/tests/Feature/API/ConceptsTest.php +++ b/tests/Feature/API/ConceptsTest.php @@ -5,11 +5,16 @@ use App\Models\Concept; use App\Models\Role; use App\Models\User; +use App\Models\Vocabulary; +use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Arr; use Laravel\Sanctum\Sanctum; use Tests\TestCase; class ConceptsTest extends TestCase { + use DatabaseTransactions; + public function test_any_can_list_concepts(): void { $response = $this->getJson('/api/concepts'); @@ -37,12 +42,22 @@ public function test_any_can_reconcile_concept(): void public function test_authorized_user_can_create_concept(): void { - $user = User::factory()->create(['role' => 'editor']); + $reviewerRole = Role::whereHas('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($reviewerRole)->create(); Sanctum::actingAs($user); + $categoryIds = Vocabulary::where('type', 'concept_category')->pluck('id')->toArray(); + $response = $this->postJson('/api/concepts', [ - 'category_id' => 1, - 'terms' => [['text' => 'New Term']], + 'preferred_term' => 'preferred', + 'category_id' => Arr::random($categoryIds), + 'alternate_terms' => [ + 'term1', + 'term2', + 'term3' + ], ]); $response->assertStatus(201); @@ -51,27 +66,40 @@ public function test_authorized_user_can_create_concept(): void public function test_authorized_user_can_update_concept(): void { $reviewerRole = Role::whereHas('permissions', function ($query) { - $query->where('name', 'Edit Vocabulary'); + $query->where('label', 'Edit Vocabulary'); })->first(); $user = User::factory()->hasAttached($reviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); - $response = $this->putJson("/api/concepts/{$concept->id}", [ - 'terms' => [['text' => 'Updated Term']], + $conceptCategories = Vocabulary::where('type', 'concept_category')->get()->random(2)->toArray(); + $response = $this->patchJson("/api/concepts/{$concept->id}", [ + 'conceptCategories' => $conceptCategories ]); + $updatedCategories = Concept::find($concept->id)->conceptCategories->toArray(); + $keysToRemove = ["pivot"]; + $cleanedCategories = array_map(function($item) use ($keysToRemove) { + return array_diff_key($item, array_flip($keysToRemove)); + }, $updatedCategories); + $this->assertEqualsCanonicalizing($conceptCategories, $cleanedCategories); + $response->assertStatus(200); } public function test_authorized_user_can_update_concept_relationships(): void { - $user = User::factory()->create(['role' => 'editor']); + $reviewerRole = Role::whereHas('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($reviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); - $response = $this->putJson("/api/concepts/{$concept->id}/relationships", [ - 'related' => [2, 3], + $relatedConcept = Concept::factory()->create(); + $response = $this->putJson("/api/concepts/{$concept->id}/relate_concept", [ + 'relation_type' => 'broader', + 'related_id' => $relatedConcept, ]); $response->assertStatus(200); @@ -79,18 +107,24 @@ public function test_authorized_user_can_update_concept_relationships(): void public function test_authorized_user_can_deprecate_concept(): void { - $user = User::factory()->create(['role' => 'editor']); + $reviewerRole = Role::whereHas('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($reviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); - $response = $this->patchJson("/api/concepts/{$concept->id}/deprecate"); + $response = $this->putJson("/api/concepts/{$concept->id}/deprecate"); $response->assertStatus(200); } public function test_authorized_user_can_delete_concept(): void { - $user = User::factory()->create(['role' => 'editor']); + $reviewerRole = Role::whereHas('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($reviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); @@ -101,7 +135,10 @@ public function test_authorized_user_can_delete_concept(): void public function test_unauthorized_user_cannot_create_concept(): void { - $user = User::factory()->create(['role' => 'viewer']); + $nonReviewerRole = Role::whereDoesntHave('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($nonReviewerRole)->create(); Sanctum::actingAs($user); $response = $this->postJson('/api/concepts', [ @@ -114,7 +151,10 @@ public function test_unauthorized_user_cannot_create_concept(): void public function test_unauthorized_user_cannot_update_concept(): void { - $user = User::factory()->create(['role' => 'viewer']); + $nonReviewerRole = Role::whereDoesntHave('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($nonReviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); @@ -127,12 +167,17 @@ public function test_unauthorized_user_cannot_update_concept(): void public function test_unauthorized_user_cannot_update_concept_relationships(): void { - $user = User::factory()->create(['role' => 'viewer']); + $nonReviewerRole = Role::whereDoesntHave('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($nonReviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); - $response = $this->putJson("/api/concepts/{$concept->id}/relationships", [ - 'related' => [2, 3], + $relatedConcept = Concept::factory()->create(); + $response = $this->putJson("/api/concepts/{$concept->id}/relate_concept", [ + 'relation_type' => 'broader', + 'related_id' => $relatedConcept, ]); $response->assertStatus(403); @@ -140,18 +185,24 @@ public function test_unauthorized_user_cannot_update_concept_relationships(): vo public function test_unauthorized_user_cannot_deprecate_concept(): void { - $user = User::factory()->create(['role' => 'viewer']); + $nonReviewerRole = Role::whereDoesntHave('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($nonReviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); - $response = $this->patchJson("/api/concepts/{$concept->id}/deprecate"); + $response = $this->putJson("/api/concepts/{$concept->id}/deprecate"); $response->assertStatus(403); } public function test_unauthorized_user_cannot_delete_concept(): void { - $user = User::factory()->create(['role' => 'viewer']); + $nonReviewerRole = Role::whereDoesntHave('permissions', function ($query) { + $query->where('label', 'Edit Vocabulary'); + })->first(); + $user = User::factory()->hasAttached($nonReviewerRole)->create(); Sanctum::actingAs($user); $concept = Concept::factory()->create(); diff --git a/tests/Unit/.gitkeep b/tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29