3 namespace Tests\Sorting;
5 use BookStack\Activity\ActivityType;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Sorting\SortRule;
8 use BookStack\Sorting\SortRuleOperation;
9 use Tests\Api\TestsApi;
12 class SortRuleTest extends TestCase
16 public function test_manage_settings_permission_required()
18 $rule = SortRule::factory()->create();
19 $user = $this->users->viewer();
20 $this->actingAs($user);
23 ['GET', '/settings/sorting'],
24 ['POST', '/settings/sorting/rules'],
25 ['GET', "/settings/sorting/rules/{$rule->id}"],
26 ['PUT', "/settings/sorting/rules/{$rule->id}"],
27 ['DELETE', "/settings/sorting/rules/{$rule->id}"],
30 foreach ($actions as [$method, $path]) {
31 $resp = $this->call($method, $path);
32 $this->assertPermissionError($resp);
35 $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
37 foreach ($actions as [$method, $path]) {
38 $resp = $this->call($method, $path);
39 $this->assertNotPermissionError($resp);
43 public function test_create_flow()
45 $resp = $this->asAdmin()->get('/settings/sorting');
46 $this->withHtml($resp)->assertLinkExists(url('/settings/sorting/rules/new'));
48 $resp = $this->get('/settings/sorting/rules/new');
49 $this->withHtml($resp)->assertElementExists('form[action$="/settings/sorting/rules"] input[name="name"]');
50 $resp->assertSeeText('Name - Alphabetical (Asc)');
52 $details = ['name' => 'My new sort', 'sequence' => 'name_asc'];
53 $resp = $this->post('/settings/sorting/rules', $details);
54 $resp->assertRedirect('/settings/sorting');
56 $this->assertActivityExists(ActivityType::SORT_RULE_CREATE);
57 $this->assertDatabaseHas('sort_rules', $details);
60 public function test_listing_in_settings()
62 $rule = SortRule::factory()->create(['name' => 'My super sort rule', 'sequence' => 'name_asc']);
63 $books = Book::query()->limit(5)->get();
64 foreach ($books as $book) {
65 $book->sort_rule_id = $rule->id;
69 $resp = $this->asAdmin()->get('/settings/sorting');
70 $resp->assertSeeText('My super sort rule');
71 $resp->assertSeeText('Name - Alphabetical (Asc)');
72 $this->withHtml($resp)->assertElementContains('.item-list-row [title="Assigned to 5 Books"]', '5');
75 public function test_update_flow()
77 $rule = SortRule::factory()->create(['name' => 'My sort rule to update', 'sequence' => 'name_asc']);
79 $resp = $this->asAdmin()->get("/settings/sorting/rules/{$rule->id}");
80 $respHtml = $this->withHtml($resp);
81 $respHtml->assertElementContains('.configured-option-list', 'Name - Alphabetical (Asc)');
82 $respHtml->assertElementNotContains('.available-option-list', 'Name - Alphabetical (Asc)');
84 $updateData = ['name' => 'My updated sort', 'sequence' => 'name_desc,chapters_last'];
85 $resp = $this->put("/settings/sorting/rules/{$rule->id}", $updateData);
87 $resp->assertRedirect('/settings/sorting');
88 $this->assertActivityExists(ActivityType::SORT_RULE_UPDATE);
89 $this->assertDatabaseHas('sort_rules', $updateData);
92 public function test_update_triggers_resort_on_assigned_books()
94 $book = $this->entities->bookHasChaptersAndPages();
95 $chapter = $book->chapters()->first();
96 $rule = SortRule::factory()->create(['name' => 'My sort rule to update', 'sequence' => 'name_asc']);
97 $book->sort_rule_id = $rule->id;
99 $chapter->priority = 10000;
102 $resp = $this->asAdmin()->put("/settings/sorting/rules/{$rule->id}", ['name' => $rule->name, 'sequence' => 'chapters_last']);
103 $resp->assertRedirect('/settings/sorting');
106 $this->assertNotEquals(10000, $chapter->priority);
109 public function test_delete_flow()
111 $rule = SortRule::factory()->create();
113 $resp = $this->asAdmin()->get("/settings/sorting/rules/{$rule->id}");
114 $resp->assertSeeText('Delete Sort Rule');
116 $resp = $this->delete("settings/sorting/rules/{$rule->id}");
117 $resp->assertRedirect('/settings/sorting');
119 $this->assertActivityExists(ActivityType::SORT_RULE_DELETE);
120 $this->assertDatabaseMissing('sort_rules', ['id' => $rule->id]);
123 public function test_delete_requires_confirmation_if_books_assigned()
125 $rule = SortRule::factory()->create();
126 $books = Book::query()->limit(5)->get();
127 foreach ($books as $book) {
128 $book->sort_rule_id = $rule->id;
132 $resp = $this->asAdmin()->get("/settings/sorting/rules/{$rule->id}");
133 $resp->assertSeeText('Delete Sort Rule');
135 $resp = $this->delete("settings/sorting/rules/{$rule->id}");
136 $resp->assertRedirect("/settings/sorting/rules/{$rule->id}#delete");
137 $resp = $this->followRedirects($resp);
139 $resp->assertSeeText('This sort rule is currently used on 5 book(s). Are you sure you want to delete this?');
140 $this->assertDatabaseHas('sort_rules', ['id' => $rule->id]);
142 $resp = $this->delete("settings/sorting/rules/{$rule->id}", ['confirm' => 'true']);
143 $resp->assertRedirect('/settings/sorting');
144 $this->assertDatabaseMissing('sort_rules', ['id' => $rule->id]);
145 $this->assertDatabaseMissing('books', ['sort_rule_id' => $rule->id]);
148 public function test_page_create_triggers_book_sort()
150 $book = $this->entities->bookHasChaptersAndPages();
151 $rule = SortRule::factory()->create(['sequence' => 'name_asc,chapters_first']);
152 $book->sort_rule_id = $rule->id;
155 $resp = $this->actingAsApiEditor()->post("/api/pages", [
156 'book_id' => $book->id,
157 'name' => '1111 page',
162 $this->assertDatabaseHas('pages', [
163 'book_id' => $book->id,
164 'name' => '1111 page',
165 'priority' => $book->chapters()->count() + 1,
169 public function test_auto_book_sort_does_not_touch_timestamps()
171 $book = $this->entities->bookHasChaptersAndPages();
172 $rule = SortRule::factory()->create(['sequence' => 'name_asc,chapters_first']);
173 $book->sort_rule_id = $rule->id;
175 $page = $book->pages()->first();
176 $chapter = $book->chapters()->first();
178 $resp = $this->actingAsApiEditor()->put("/api/pages/{$page->id}", [
179 'name' => '1111 page',
183 $oldTime = $chapter->updated_at->unix();
184 $oldPriority = $chapter->priority;
186 $this->assertEquals($oldTime, $chapter->updated_at->unix());
187 $this->assertNotEquals($oldPriority, $chapter->priority);
190 public function test_name_alphabetical_ordering()
192 $book = Book::factory()->create();
193 $rule = SortRule::factory()->create(['sequence' => 'name_asc']);
194 $book->sort_rule_id = $rule->id;
196 $this->permissions->regenerateForEntity($book);
210 $reverseNamesToAdd = array_reverse($namesToAdd);
211 foreach ($reverseNamesToAdd as $name) {
212 $this->actingAsApiEditor()->post("/api/pages", [
213 'book_id' => $book->id,
215 'markdown' => 'Hello'
219 foreach ($namesToAdd as $index => $name) {
220 $this->assertDatabaseHas('pages', [
221 'book_id' => $book->id,
223 'priority' => $index + 1,
228 public function test_name_numeric_ordering()
230 $book = Book::factory()->create();
231 $rule = SortRule::factory()->create(['sequence' => 'name_numeric_asc']);
232 $book->sort_rule_id = $rule->id;
234 $this->permissions->regenerateForEntity($book);
244 $reverseNamesToAdd = array_reverse($namesToAdd);
245 foreach ($reverseNamesToAdd as $name) {
246 $this->actingAsApiEditor()->post("/api/pages", [
247 'book_id' => $book->id,
249 'markdown' => 'Hello'
253 foreach ($namesToAdd as $index => $name) {
254 $this->assertDatabaseHas('pages', [
255 'book_id' => $book->id,
257 'priority' => $index + 1,
262 public function test_each_sort_rule_operation_has_a_comparison_function()
264 $operations = SortRuleOperation::cases();
266 foreach ($operations as $operation) {
267 $comparisonFunc = $operation->getSortFunction();
268 $this->assertIsCallable($comparisonFunc);