PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / Integration / QuickEdit / Controllers / WPNavigationControllerTest.php

WPNavigationControllerTest.php in Extendify 3.1.0, at tests/Integration/QuickEdit/Controllers/WPNavigationControllerTest.php

297 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\Tests\Integration\QuickEdit\Controllers;
4
5 use Extendify\QuickEdit\Controllers\WPNavigationController;
6 use Extendify\QuickEdit\Schemas\Registry;
7 use ReflectionClass;
8 use WP_UnitTestCase;
9
10 class WPNavigationControllerTest extends WP_UnitTestCase
11 {
12 public function setUp(): void
13 {
14 parent::setUp();
15 $this->resetRegistry();
16 Registry::init();
17 $this->loginAsAdmin();
18 }
19
20 public function tearDown(): void
21 {
22 $this->resetRegistry();
23 parent::tearDown();
24 }
25
26 public function test_permission_callback_requires_admin_capability()
27 {
28 wp_set_current_user(0);
29 $this->assertFalse(WPNavigationController::permissionCallback());
30
31 $editor = self::factory()->user->create(['role' => 'editor']);
32 wp_set_current_user($editor);
33 $this->assertFalse(WPNavigationController::permissionCallback());
34
35 $admin = self::factory()->user->create(['role' => 'administrator']);
36 wp_set_current_user($admin);
37 $this->assertTrue(WPNavigationController::permissionCallback());
38 }
39
40 public function test_missing_required_fields_returns_400()
41 {
42 $res = WPNavigationController::handle($this->jsonRequest([]));
43 $this->assertSame(400, $res->get_status());
44 $this->assertStringContainsString('required', $res->get_data()['error']);
45 }
46
47 public function test_unsupported_blockType_returns_400()
48 {
49 $navId = $this->createNav('<!-- wp:navigation-link {"label":"Home","url":"/"} /-->');
50
51 $res = WPNavigationController::handle($this->jsonRequest([
52 'navPostId' => $navId,
53 'itemIndex' => 0,
54 'blockType' => 'core/paragraph',
55 'patches' => [['fieldKey' => 'label', 'value' => 'X']],
56 ]));
57
58 $this->assertSame(400, $res->get_status());
59 $this->assertSame('unsupported blockType for wp-navigation save', $res->get_data()['error']);
60 }
61
62 public function test_non_wp_navigation_post_returns_404()
63 {
64 $postId = self::factory()->post->create();
65
66 $res = WPNavigationController::handle($this->jsonRequest([
67 'navPostId' => $postId,
68 'itemIndex' => 0,
69 'blockType' => 'core/navigation-link',
70 'patches' => [['fieldKey' => 'label', 'value' => 'X']],
71 ]));
72
73 $this->assertSame(404, $res->get_status());
74 $this->assertSame('wp_navigation post not found', $res->get_data()['error']);
75 }
76
77 public function test_subscriber_user_forbidden()
78 {
79 $navId = $this->createNav('<!-- wp:navigation-link {"label":"Home","url":"/"} /-->');
80 $sub = self::factory()->user->create(['role' => 'subscriber']);
81 wp_set_current_user($sub);
82
83 $res = WPNavigationController::handle($this->jsonRequest([
84 'navPostId' => $navId,
85 'itemIndex' => 0,
86 'blockType' => 'core/navigation-link',
87 'patches' => [['fieldKey' => 'label', 'value' => 'X']],
88 ]));
89
90 $this->assertSame(403, $res->get_status());
91 }
92
93 public function test_itemIndex_past_end_returns_404()
94 {
95 $navId = $this->createNav('<!-- wp:navigation-link {"label":"Home","url":"/"} /-->');
96
97 $res = WPNavigationController::handle($this->jsonRequest([
98 'navPostId' => $navId,
99 'itemIndex' => 5,
100 'blockType' => 'core/navigation-link',
101 'patches' => [['fieldKey' => 'label', 'value' => 'X']],
102 ]));
103
104 $this->assertSame(404, $res->get_status());
105 $this->assertSame('nav item not found at index', $res->get_data()['error']);
106 $this->assertSame(5, $res->get_data()['itemIndex']);
107 }
108
109 public function test_blockType_mismatch_returns_409()
110 {
111 $navId = $this->createNav('<!-- wp:navigation-link {"label":"Home","url":"/"} /-->');
112
113 $res = WPNavigationController::handle($this->jsonRequest([
114 'navPostId' => $navId,
115 'itemIndex' => 0,
116 'blockType' => 'core/navigation-submenu',
117 'patches' => [['fieldKey' => 'label', 'value' => 'X']],
118 ]));
119
120 $this->assertSame(409, $res->get_status());
121 $this->assertSame('core/navigation-submenu', $res->get_data()['expected']);
122 $this->assertSame('core/navigation-link', $res->get_data()['actual']);
123 }
124
125 public function test_updates_label_via_index_0()
126 {
127 $navId = $this->createNav('<!-- wp:navigation-link {"label":"Home","url":"/"} /-->');
128
129 $res = WPNavigationController::handle($this->jsonRequest([
130 'navPostId' => $navId,
131 'itemIndex' => 0,
132 'blockType' => 'core/navigation-link',
133 'patches' => [['fieldKey' => 'label', 'value' => 'Homepage']],
134 ]));
135
136 $this->assertSame(200, $res->get_status());
137 $this->assertTrue($res->get_data()['ok']);
138
139 $blocks = parse_blocks(get_post($navId)->post_content);
140 $this->assertSame('Homepage', $blocks[0]['attrs']['label']);
141 }
142
143 public function test_updates_url_via_index()
144 {
145 $navId = $this->createNav('<!-- wp:navigation-link {"label":"Home","url":"/"} /-->');
146
147 WPNavigationController::handle($this->jsonRequest([
148 'navPostId' => $navId,
149 'itemIndex' => 0,
150 'blockType' => 'core/navigation-link',
151 'patches' => [['fieldKey' => 'url', 'value' => 'https://example.com/about']],
152 ]));
153
154 $blocks = parse_blocks(get_post($navId)->post_content);
155 $this->assertSame('https://example.com/about', $blocks[0]['attrs']['url']);
156 }
157
158 public function test_index_counts_only_nav_link_and_nav_submenu()
159 {
160 // Three nav items. Submenu wrapping nested links shouldn't double-count.
161 $content = '<!-- wp:navigation-link {"label":"One","url":"/1"} /-->'
162 . '<!-- wp:navigation-submenu {"label":"Two","url":"/2"} -->'
163 . '<!-- wp:navigation-link {"label":"Two-A","url":"/2a"} /-->'
164 . '<!-- /wp:navigation-submenu -->'
165 . '<!-- wp:navigation-link {"label":"Three","url":"/3"} /-->';
166 $navId = $this->createNav($content);
167
168 // index 0 = One, 1 = Two (submenu), 2 = Two-A (nested), 3 = Three.
169 WPNavigationController::handle($this->jsonRequest([
170 'navPostId' => $navId,
171 'itemIndex' => 3,
172 'blockType' => 'core/navigation-link',
173 'patches' => [['fieldKey' => 'label', 'value' => 'Last']],
174 ]));
175
176 $blocks = parse_blocks(get_post($navId)->post_content);
177 $this->assertSame('Last', $blocks[2]['attrs']['label']);
178 // Earlier items untouched.
179 $this->assertSame('One', $blocks[0]['attrs']['label']);
180 $this->assertSame('Two', $blocks[1]['attrs']['label']);
181 $this->assertSame('Two-A', $blocks[1]['innerBlocks'][0]['attrs']['label']);
182 }
183
184 public function test_save_to_index_3_after_deleting_item_at_index_1_targets_a_different_item()
185 {
186 // Characterize the index semantics: indices are positional. If items
187 // are reordered/deleted between client read and save, the index will
188 // address a different item — the controller doesn't reconcile.
189 $base = '<!-- wp:navigation-link {"label":"A","url":"/a"} /-->'
190 . '<!-- wp:navigation-link {"label":"B","url":"/b"} /-->'
191 . '<!-- wp:navigation-link {"label":"C","url":"/c"} /-->';
192 $navId = $this->createNav($base);
193
194 // Delete B (originally at index 1) externally.
195 $shortened = '<!-- wp:navigation-link {"label":"A","url":"/a"} /-->'
196 . '<!-- wp:navigation-link {"label":"C","url":"/c"} /-->';
197 wp_update_post([
198 'ID' => $navId,
199 'post_content' => wp_slash($shortened),
200 ]);
201
202 // Client thinks "edit C at index 2"; after deletion, index 2 doesn't exist.
203 $res = WPNavigationController::handle($this->jsonRequest([
204 'navPostId' => $navId,
205 'itemIndex' => 2,
206 'blockType' => 'core/navigation-link',
207 'patches' => [['fieldKey' => 'label', 'value' => 'C-renamed']],
208 ]));
209
210 $this->assertSame(404, $res->get_status());
211 }
212
213 // Submenu/ordering divergence can resolve a nav-item index to the
214 // wrong same-type sibling. The fingerprint of the clicked item's label is
215 // the safety net: a mismatch refuses instead of editing the wrong link.
216 public function test_fingerprint_mismatch_returns_409_without_writing()
217 {
218 $content = '<!-- wp:navigation-link {"label":"A","url":"/a"} /-->'
219 . '<!-- wp:navigation-link {"label":"B","url":"/b"} /-->';
220 $navId = $this->createNav($content);
221
222 // index 1 counts to "B"; client meant "A".
223 $res = WPNavigationController::handle($this->jsonRequest([
224 'navPostId' => $navId,
225 'itemIndex' => 1,
226 'blockType' => 'core/navigation-link',
227 'fingerprint' => ['text' => 'A'],
228 'patches' => [['fieldKey' => 'label', 'value' => 'corrupted']],
229 ]));
230
231 $this->assertSame(409, $res->get_status());
232 $this->assertSame('block fingerprint mismatch', $res->get_data()['error']);
233 $blocks = parse_blocks(get_post($navId)->post_content);
234 $this->assertSame('B', $blocks[1]['attrs']['label']);
235 }
236
237 public function test_matching_fingerprint_allows_save()
238 {
239 $content = '<!-- wp:navigation-link {"label":"A","url":"/a"} /-->'
240 . '<!-- wp:navigation-link {"label":"B","url":"/b"} /-->';
241 $navId = $this->createNav($content);
242
243 $res = WPNavigationController::handle($this->jsonRequest([
244 'navPostId' => $navId,
245 'itemIndex' => 1,
246 'blockType' => 'core/navigation-link',
247 'fingerprint' => ['text' => 'B'],
248 'patches' => [['fieldKey' => 'label', 'value' => 'Beta']],
249 ]));
250
251 $this->assertSame(200, $res->get_status());
252 $blocks = parse_blocks(get_post($navId)->post_content);
253 $this->assertSame('Beta', $blocks[1]['attrs']['label']);
254 }
255
256 public function test_init_hooks_registerRoutes_into_rest_api_init()
257 {
258 remove_all_filters('rest_api_init');
259 WPNavigationController::init();
260
261 $this->assertNotFalse(
262 has_action('rest_api_init', [WPNavigationController::class, 'registerRoutes'])
263 );
264 }
265
266 private function createNav(string $content): int
267 {
268 return self::factory()->post->create([
269 'post_type' => 'wp_navigation',
270 'post_status' => 'publish',
271 'post_content' => wp_slash($content),
272 ]);
273 }
274
275 private function jsonRequest(array $body): \WP_REST_Request
276 {
277 $req = new \WP_REST_Request('POST', '/extendify/v1/quick-edit/wp-navigation');
278 $req->set_header('Content-Type', 'application/json');
279 $req->set_body(wp_json_encode($body));
280 return $req;
281 }
282
283 private function loginAsAdmin(): void
284 {
285 $admin = self::factory()->user->create(['role' => 'administrator']);
286 wp_set_current_user($admin);
287 }
288
289 private function resetRegistry(): void
290 {
291 $reflection = new ReflectionClass(Registry::class);
292 $prop = $reflection->getProperty('schemas');
293 $prop->setAccessible(true);
294 $prop->setValue(null, []);
295 }
296 }
297