-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest-multi-association.php
More file actions
288 lines (237 loc) · 9.39 KB
/
Copy pathtest-multi-association.php
File metadata and controls
288 lines (237 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
<?php
/**
* Class TestMultiAssociation
*
* Regression tests covering the three call sites where `wp_set_object_terms`
* was invoked without `$append = true`, which silently destroyed existing
* shadow-term associations on the connected post.
*
* Each test exercises one of the three call sites:
* - includes/sync.php:73 (restore loop on draft → publish)
* - includes/sync.php:110 (recovery loop when the shadow term is missing)
* - includes/taxonomy.php:170 (REST `associate` endpoint for a published shadow post)
*
* @package shadow-terms
*/
/**
* Test that managing one shadow term preserves a connected post's other
* shadow-term associations in the same taxonomy.
*/
class TestMultiAssociation extends WP_UnitTestCase {
/**
* REST server used to dispatch requests in REST-level tests.
*
* @var WP_REST_Server|null
*/
protected $rest_server;
/**
* Spin up a REST server so the `associate` route can be dispatched.
*/
public function set_up(): void {
parent::set_up();
global $wp_rest_server;
$wp_rest_server = new WP_REST_Server();
$this->rest_server = $wp_rest_server;
do_action( 'rest_api_init' );
}
/**
* Tear down the REST server created for each test.
*/
public function tear_down(): void {
global $wp_rest_server;
$wp_rest_server = null;
$this->rest_server = null;
parent::tear_down();
}
/**
* Create a post and fail the test if creation returns an error.
*
* @param string $post_type The post type to create.
* @param string $post_title The post title.
* @param string $post_status The post status.
* @return int The new post ID.
*/
private function create_post( string $post_type, string $post_title, string $post_status = 'publish' ): int {
$post_id = $this->factory()->post->create(
array(
'post_type' => $post_type,
'post_title' => $post_title,
'post_status' => $post_status,
)
);
if ( is_wp_error( $post_id ) ) {
$this->fail( "Failed to create {$post_title} post." );
}
return $post_id;
}
/**
* Return a connected post's `example_connect` term slugs, sorted.
*
* @param int $post_id The connected post ID.
* @return string[] The associated term slugs.
*/
private function associated_slugs( int $post_id ): array {
$terms = wp_get_object_terms( $post_id, 'example_connect' );
if ( is_wp_error( $terms ) ) {
$this->fail( 'Failed to read associated terms.' );
}
$slugs = wp_list_pluck( $terms, 'slug' );
sort( $slugs );
return $slugs;
}
/**
* Restore loop on draft → publish should preserve other associations
* (sync.php:73).
*
* When a shadow post returns to published, the plugin re-attaches the new
* shadow term to every previously associated post. That loop must append the
* term so it does not replace the connected post's other `example_connect`
* terms.
*/
public function test_restore_on_publish_preserves_other_associations(): void {
$acme_id = $this->create_post( 'example', 'Acme' );
$this->create_post( 'example', 'Globex' );
$acme_term = get_term_by( 'slug', 'acme', 'example_connect' );
$globex_term = get_term_by( 'slug', 'globex', 'example_connect' );
if ( ! $acme_term || ! $globex_term ) {
$this->fail( 'Expected shadow terms for both example posts.' );
}
$article_id = $this->create_post( 'post', 'Article' );
wp_set_object_terms(
$article_id,
array( (int) $acme_term->term_id, (int) $globex_term->term_id ),
'example_connect'
);
$this->assertSame(
array( 'acme', 'globex' ),
$this->associated_slugs( $article_id ),
'Sanity: article should start with both shadow terms attached.'
);
// Acme → draft. The plugin deletes the acme shadow term and stores the
// list of previously-associated posts in postmeta on the acme post.
$acme = get_post( $acme_id );
$acme->post_status = 'draft';
wp_update_post( $acme );
// Acme → publish. The restore loop in sync.php should re-attach the new
// acme term to the article WITHOUT wiping its globex association.
$acme->post_status = 'publish';
wp_update_post( $acme );
$this->assertSame(
array( 'acme', 'globex' ),
$this->associated_slugs( $article_id ),
'Restoring a shadow post should not wipe the connected post\'s other shadow-term associations.'
);
}
/**
* Recovery loop when the shadow term is missing should preserve other
* associations (sync.php:110).
*
* If a published shadow post has lost its term (e.g., manually deleted in
* admin) and is saved again, the plugin recreates the term and restores
* known associations. That restore loop must append rather than replace.
*/
public function test_missing_term_recovery_preserves_other_associations(): void {
$acme_id = $this->create_post( 'example', 'Acme' );
$this->create_post( 'example', 'Globex' );
$acme_term = get_term_by( 'slug', 'acme', 'example_connect' );
$globex_term = get_term_by( 'slug', 'globex', 'example_connect' );
if ( ! $acme_term || ! $globex_term ) {
$this->fail( 'Expected shadow terms for both example posts.' );
}
$article_id = $this->create_post( 'post', 'Article' );
wp_set_object_terms(
$article_id,
array( (int) $acme_term->term_id, (int) $globex_term->term_id ),
'example_connect'
);
// Simulate the missing-term precondition. Deleting the term also
// detaches it from the article, so after this call the article has
// only the globex term — that's the state the recovery loop must
// not destroy.
wp_delete_term( $acme_term->term_id, 'example_connect' );
// Seed the prior-association meta on the acme post so the recovery
// loop in sync.php has something to restore.
update_post_meta( $acme_id, 'example_connect_associated_posts', array( $article_id ) );
// publish → publish with the term missing triggers the recovery branch.
wp_update_post( get_post( $acme_id ) );
$this->assertSame(
array( 'acme', 'globex' ),
$this->associated_slugs( $article_id ),
'Recovering a missing shadow term should not wipe other shadow-term associations on the connected post.'
);
}
/**
* REST `associate` endpoint should preserve prior associations
* (taxonomy.php:170).
*
* Associating a connected post with a second published shadow post via the
* REST endpoint must be additive rather than replacing the first
* association.
*/
public function test_rest_associate_preserves_prior_associations(): void {
$editor_id = $this->factory()->user->create( array( 'role' => 'editor' ) );
if ( is_wp_error( $editor_id ) ) {
$this->fail( 'Failed to create editor user.' );
}
wp_set_current_user( $editor_id );
$acme_id = $this->create_post( 'example', 'Acme' );
$globex_id = $this->create_post( 'example', 'Globex' );
$article_id = $this->create_post( 'post', 'Article' );
// First association: Article ↔ Acme.
$request = new WP_REST_Request( 'POST', '/shadow-terms/v1/associate' );
$request->set_param( 'postId', $acme_id );
$request->set_param( 'associatedPostId', $article_id );
$response = $this->rest_server->dispatch( $request );
$this->assertSame( 200, $response->get_status(), 'First associate call should succeed.' );
// Second association: Article ↔ Globex. This should be additive.
$request = new WP_REST_Request( 'POST', '/shadow-terms/v1/associate' );
$request->set_param( 'postId', $globex_id );
$request->set_param( 'associatedPostId', $article_id );
$response = $this->rest_server->dispatch( $request );
$this->assertSame( 200, $response->get_status(), 'Second associate call should succeed.' );
$this->assertSame(
array( 'acme', 'globex' ),
$this->associated_slugs( $article_id ),
'Associating a connected post with a second shadow term via REST should not wipe the prior association.'
);
}
/**
* REST `associate` endpoint should recreate a missing term rather than
* silently succeeding (taxonomy.php).
*
* If a published shadow post's term has been deleted and the post has not
* been re-saved, the term cannot be resolved. The endpoint must recreate it
* and attach it instead of reporting success while attaching nothing.
*/
public function test_rest_associate_recreates_missing_published_term(): void {
$editor_id = $this->factory()->user->create( array( 'role' => 'editor' ) );
if ( is_wp_error( $editor_id ) ) {
$this->fail( 'Failed to create editor user.' );
}
wp_set_current_user( $editor_id );
$acme_id = $this->create_post( 'example', 'Acme' );
$article_id = $this->create_post( 'post', 'Article' );
// Delete the term but leave the post published and un-saved, so the sync
// recovery branch never runs and the term stays missing.
$acme_term = get_term_by( 'slug', 'acme', 'example_connect' );
if ( ! $acme_term ) {
$this->fail( 'Expected an acme shadow term.' );
}
wp_delete_term( $acme_term->term_id, 'example_connect' );
$request = new WP_REST_Request( 'POST', '/shadow-terms/v1/associate' );
$request->set_param( 'postId', $acme_id );
$request->set_param( 'associatedPostId', $article_id );
$response = $this->rest_server->dispatch( $request );
$this->assertSame( 200, $response->get_status(), 'Associate call should return 200.' );
$data = $response->get_data();
$this->assertTrue(
is_array( $data ) && true === $data['success'],
'Associate should report success only after the missing term is recreated.'
);
$this->assertSame(
array( 'acme' ),
$this->associated_slugs( $article_id ),
'A recreated shadow term should actually be attached to the connected post.'
);
}
}