From fcfb9470c96c9bff054dbc28a7dea1d7b87ccb91 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 7 May 2016 14:29:43 +0100 Subject: [PATCH] Added further attribute endpoints and added tests --- app/Entity.php | 10 +- app/Http/Controllers/AttributeController.php | 41 ++++++- app/Http/Controllers/Controller.php | 11 ++ app/Http/routes.php | 2 + app/Repos/AttributeRepo.php | 71 ++++++++++++ app/Repos/PageRepo.php | 1 + database/factories/ModelFactory.php | 7 ++ tests/Auth/AuthTest.php | 2 +- tests/Entity/AttributeTests.php | 115 +++++++++++++++++++ tests/Entity/EntityTest.php | 14 +-- tests/Entity/PageDraftTest.php | 6 +- tests/Permissions/RestrictionsTest.php | 2 +- tests/TestCase.php | 21 +++- tests/UserProfileTest.php | 6 +- 14 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 tests/Entity/AttributeTests.php diff --git a/app/Entity.php b/app/Entity.php index abf3e834e..79ff9ff38 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -1,7 +1,7 @@ orderBy('title_relevance', 'desc'); } - - /** - * Get the url for this item. - * @return string - */ - abstract public function getUrl(); - + } diff --git a/app/Http/Controllers/AttributeController.php b/app/Http/Controllers/AttributeController.php index 09523af47..d7282696a 100644 --- a/app/Http/Controllers/AttributeController.php +++ b/app/Http/Controllers/AttributeController.php @@ -2,7 +2,6 @@ use BookStack\Repos\AttributeRepo; use Illuminate\Http\Request; - use BookStack\Http\Requests; class AttributeController extends Controller @@ -19,7 +18,6 @@ class AttributeController extends Controller $this->attributeRepo = $attributeRepo; } - /** * Get all the Attributes for a particular entity * @param $entityType @@ -27,6 +25,43 @@ class AttributeController extends Controller */ public function getForEntity($entityType, $entityId) { - + $attributes = $this->attributeRepo->getForEntity($entityType, $entityId); + return response()->json($attributes); } + + /** + * Update the attributes for a particular entity. + * @param $entityType + * @param $entityId + * @param Request $request + * @return mixed + */ + public function updateForEntity($entityType, $entityId, Request $request) + { + + $this->validate($request, [ + 'attributes.*.name' => 'required|min:3|max:250', + 'attributes.*.value' => 'max:250' + ]); + + $entity = $this->attributeRepo->getEntity($entityType, $entityId, 'update'); + if ($entity === null) return $this->jsonError("Entity not found", 404); + + $inputAttributes = $request->input('attributes'); + $attributes = $this->attributeRepo->saveAttributesToEntity($entity, $inputAttributes); + return response()->json($attributes); + } + + /** + * Get attribute name suggestions from a given search term. + * @param Request $request + */ + public function getNameSuggestions(Request $request) + { + $searchTerm = $request->get('search'); + $suggestions = $this->attributeRepo->getNameSuggestions($searchTerm); + return response()->json($suggestions); + } + + } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index f0cb47cd9..26eeb3002 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -110,4 +110,15 @@ abstract class Controller extends BaseController return true; } + /** + * Send back a json error message. + * @param string $messageText + * @param int $statusCode + * @return mixed + */ + protected function jsonError($messageText = "", $statusCode = 500) + { + return response()->json(['message' => $messageText], $statusCode); + } + } diff --git a/app/Http/routes.php b/app/Http/routes.php index 8b7ec3bc2..7c6911b2e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -88,6 +88,8 @@ Route::group(['middleware' => 'auth'], function () { // Attribute routes (AJAX) Route::group(['prefix' => 'ajax/attributes'], function() { Route::get('/get/{entityType}/{entityId}', 'AttributeController@getForEntity'); + Route::get('/suggest', 'AttributeController@getNameSuggestions'); + Route::post('/update/{entityType}/{entityId}', 'AttributeController@updateForEntity'); }); // Links diff --git a/app/Repos/AttributeRepo.php b/app/Repos/AttributeRepo.php index d5cf5b0fd..7318b253b 100644 --- a/app/Repos/AttributeRepo.php +++ b/app/Repos/AttributeRepo.php @@ -28,5 +28,76 @@ class AttributeRepo $this->permissionService = $ps; } + /** + * Get an entity instance of its particular type. + * @param $entityType + * @param $entityId + * @param string $action + */ + public function getEntity($entityType, $entityId, $action = 'view') + { + $entityInstance = $this->entity->getEntityInstance($entityType); + $searchQuery = $entityInstance->where('id', '=', $entityId)->with('attributes'); + $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action); + return $searchQuery->first(); + } + + /** + * Get all attributes for a particular entity. + * @param string $entityType + * @param int $entityId + * @return mixed + */ + public function getForEntity($entityType, $entityId) + { + $entity = $this->getEntity($entityType, $entityId); + if ($entity === null) return collect(); + + return $entity->attributes; + } + + /** + * Get attribute name suggestions from scanning existing attribute names. + * @param $searchTerm + * @return array + */ + public function getNameSuggestions($searchTerm) + { + if ($searchTerm === '') return []; + $query = $this->attribute->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc'); + $query = $this->permissionService->filterRestrictedEntityRelations($query, 'attributes', 'entity_id', 'entity_type'); + return $query->get(['name'])->pluck('name'); + } + + /** + * Save an array of attributes to an entity + * @param Entity $entity + * @param array $attributes + * @return array|\Illuminate\Database\Eloquent\Collection + */ + public function saveAttributesToEntity(Entity $entity, $attributes = []) + { + $entity->attributes()->delete(); + $newAttributes = []; + foreach ($attributes as $attribute) { + $newAttributes[] = $this->newInstanceFromInput($attribute); + } + + return $entity->attributes()->saveMany($newAttributes); + } + + /** + * Create a new Attribute instance from user input. + * @param $input + * @return static + */ + protected function newInstanceFromInput($input) + { + $name = trim($input['name']); + $value = isset($input['value']) ? trim($input['value']) : ''; + // Any other modification or cleanup required can go here + $values = ['name' => $name, 'value' => $value]; + return $this->attribute->newInstance($values); + } } \ No newline at end of file diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 549ec98a7..ef50b7181 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -582,6 +582,7 @@ class PageRepo extends EntityRepo { Activity::removeEntity($page); $page->views()->delete(); + $page->attributes()->delete(); $page->revisions()->delete(); $page->permissions()->delete(); $this->permissionService->deleteJointPermissionsForEntity($page); diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 2840356e8..a1a6a92a0 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -52,4 +52,11 @@ $factory->define(BookStack\Role::class, function ($faker) { 'display_name' => $faker->sentence(3), 'description' => $faker->sentence(10) ]; +}); + +$factory->define(BookStack\Attribute::class, function ($faker) { + return [ + 'name' => $faker->city, + 'value' => $faker->sentence(3) + ]; }); \ No newline at end of file diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 067840841..306771ed5 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -181,7 +181,7 @@ class AuthTest extends TestCase public function test_user_deletion() { $userDetails = factory(\BookStack\User::class)->make(); - $user = $this->getNewUser($userDetails->toArray()); + $user = $this->getEditor($userDetails->toArray()); $this->asAdmin() ->visit('/settings/users/' . $user->id) diff --git a/tests/Entity/AttributeTests.php b/tests/Entity/AttributeTests.php new file mode 100644 index 000000000..11b66b9e7 --- /dev/null +++ b/tests/Entity/AttributeTests.php @@ -0,0 +1,115 @@ +defaultAttrCount)->make(); + } + + $page->attributes()->saveMany($attributes); + return $page; + } + + public function test_get_page_attributes() + { + $page = $this->getPageWithAttributes(); + + // Add some other attributes to check they don't interfere + factory(Attribute::class, $this->defaultAttrCount)->create(); + + $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id) + ->shouldReturnJson(); + + $json = json_decode($this->response->getContent()); + $this->assertTrue(count($json) === $this->defaultAttrCount, "Returned JSON item count is not as expected"); + } + + public function test_attribute_name_suggestions() + { + // Create some attributes with similar names to test with + $attrs = collect(); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'country'])); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'color'])); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'city'])); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'county'])); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'planet'])); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'plans'])); + $page = $this->getPageWithAttributes($attrs); + + $this->asAdmin()->get('/ajax/attributes/suggest?search=dog')->seeJsonEquals([]); + $this->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country', 'county']); + $this->get('/ajax/attributes/suggest?search=cou')->seeJsonEquals(['country', 'county']); + $this->get('/ajax/attributes/suggest?search=pla')->seeJsonEquals(['planet', 'plans']); + } + + public function test_entity_permissions_effect_attribute_suggestions() + { + $permissionService = $this->app->make(PermissionService::class); + + // Create some attributes with similar names to test with and save to a page + $attrs = collect(); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'country'])); + $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'color'])); + $page = $this->getPageWithAttributes($attrs); + + $this->asAdmin()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']); + $this->asEditor()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']); + + // Set restricted permission the page + $page->restricted = true; + $page->save(); + $permissionService->buildJointPermissionsForEntity($page); + + $this->asAdmin()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']); + $this->asEditor()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals([]); + } + + public function test_entity_attribute_updating() + { + $page = $this->getPageWithAttributes(); + + $testJsonData = [ + ['name' => 'color', 'value' => 'red'], + ['name' => 'color', 'value' => ' blue '], + ['name' => 'city', 'value' => 'London '], + ['name' => 'country', 'value' => ' England'], + ]; + $testResponseJsonData = [ + ['name' => 'color', 'value' => 'red'], + ['name' => 'color', 'value' => 'blue'], + ['name' => 'city', 'value' => 'London'], + ['name' => 'country', 'value' => 'England'], + ]; + + $this->asAdmin()->json("POST", "/ajax/attributes/update/page/" . $page->id, ['attributes' => $testJsonData]); + $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id); + $jsonData = json_decode($this->response->getContent()); + // Check counts + $this->assertTrue(count($jsonData) === count($testJsonData), "The received attribute count is incorrect"); + // Check data is correct + $testDataCorrect = true; + foreach ($jsonData as $data) { + $testItem = ['name' => $data->name, 'value' => $data->value]; + if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false; + } + $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s"; + $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($jsonData))); + } + +} diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php index eebb0dc36..3bf6a3f2a 100644 --- a/tests/Entity/EntityTest.php +++ b/tests/Entity/EntityTest.php @@ -161,8 +161,8 @@ class EntityTest extends TestCase public function test_entities_viewable_after_creator_deletion() { // Create required assets and revisions - $creator = $this->getNewUser(); - $updater = $this->getNewUser(); + $creator = $this->getEditor(); + $updater = $this->getEditor(); $entities = $this->createEntityChainBelongingToUser($creator, $updater); $this->actingAs($creator); app('BookStack\Repos\UserRepo')->destroy($creator); @@ -174,8 +174,8 @@ class EntityTest extends TestCase public function test_entities_viewable_after_updater_deletion() { // Create required assets and revisions - $creator = $this->getNewUser(); - $updater = $this->getNewUser(); + $creator = $this->getEditor(); + $updater = $this->getEditor(); $entities = $this->createEntityChainBelongingToUser($creator, $updater); $this->actingAs($updater); app('BookStack\Repos\UserRepo')->destroy($updater); @@ -198,7 +198,7 @@ class EntityTest extends TestCase public function test_recently_created_pages_view() { - $user = $this->getNewUser(); + $user = $this->getEditor(); $content = $this->createEntityChainBelongingToUser($user); $this->asAdmin()->visit('/pages/recently-created') @@ -207,7 +207,7 @@ class EntityTest extends TestCase public function test_recently_updated_pages_view() { - $user = $this->getNewUser(); + $user = $this->getEditor(); $content = $this->createEntityChainBelongingToUser($user); $this->asAdmin()->visit('/pages/recently-updated') @@ -241,7 +241,7 @@ class EntityTest extends TestCase public function test_recently_created_pages_on_home() { - $entityChain = $this->createEntityChainBelongingToUser($this->getNewUser()); + $entityChain = $this->createEntityChainBelongingToUser($this->getEditor()); $this->asAdmin()->visit('/') ->seeInElement('#recently-created-pages', $entityChain['page']->name); } diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 2c9a28814..108b7459f 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -32,7 +32,7 @@ class PageDraftTest extends TestCase ->dontSeeInField('html', $addedContent); $newContent = $this->page->html . $addedContent; - $newUser = $this->getNewUser(); + $newUser = $this->getEditor(); $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit') ->dontSeeInField('html', $newContent); @@ -54,7 +54,7 @@ class PageDraftTest extends TestCase ->dontSeeInField('html', $addedContent); $newContent = $this->page->html . $addedContent; - $newUser = $this->getNewUser(); + $newUser = $this->getEditor(); $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser) @@ -79,7 +79,7 @@ class PageDraftTest extends TestCase { $book = \BookStack\Book::first(); $chapter = $book->chapters->first(); - $newUser = $this->getNewUser(); + $newUser = $this->getEditor(); $this->actingAs($newUser)->visit('/') ->visit($book->getUrl() . '/page/create') diff --git a/tests/Permissions/RestrictionsTest.php b/tests/Permissions/RestrictionsTest.php index 75d83cbfc..d3830cff7 100644 --- a/tests/Permissions/RestrictionsTest.php +++ b/tests/Permissions/RestrictionsTest.php @@ -9,7 +9,7 @@ class RestrictionsTest extends TestCase public function setUp() { parent::setUp(); - $this->user = $this->getNewUser(); + $this->user = $this->getEditor(); $this->viewer = $this->getViewer(); $this->restrictionService = $this->app[\BookStack\Services\PermissionService::class]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 5d0545b66..4c2893f4e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -14,7 +14,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase * @var string */ protected $baseUrl = 'http://localhost'; + + // Local user instances private $admin; + private $editor; /** * Creates the application. @@ -30,6 +33,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase return $app; } + /** + * Set the current user context to be an admin. + * @return $this + */ public function asAdmin() { if($this->admin === null) { @@ -39,6 +46,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase return $this->actingAs($this->admin); } + /** + * Set the current editor context to be an editor. + * @return $this + */ + public function asEditor() + { + if($this->editor === null) { + $this->editor = $this->getEditor(); + } + return $this->actingAs($this->editor); + } + /** * Quickly sets an array of settings. * @param $settingsArray @@ -79,7 +98,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase * @param array $attributes * @return mixed */ - protected function getNewUser($attributes = []) + protected function getEditor($attributes = []) { $user = factory(\BookStack\User::class)->create($attributes); $role = \BookStack\Role::getRole('editor'); diff --git a/tests/UserProfileTest.php b/tests/UserProfileTest.php index 170e7eed1..40ae004e9 100644 --- a/tests/UserProfileTest.php +++ b/tests/UserProfileTest.php @@ -33,7 +33,7 @@ class UserProfileTest extends TestCase public function test_profile_page_shows_created_content_counts() { - $newUser = $this->getNewUser(); + $newUser = $this->getEditor(); $this->asAdmin()->visit('/user/' . $newUser->id) ->see($newUser->name) @@ -52,7 +52,7 @@ class UserProfileTest extends TestCase public function test_profile_page_shows_recent_activity() { - $newUser = $this->getNewUser(); + $newUser = $this->getEditor(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); Activity::add($entities['book'], 'book_update', $entities['book']->id); @@ -66,7 +66,7 @@ class UserProfileTest extends TestCase public function test_clicking_user_name_in_activity_leads_to_profile_page() { - $newUser = $this->getNewUser(); + $newUser = $this->getEditor(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); Activity::add($entities['book'], 'book_update', $entities['book']->id);