Skip to content

Add PHPUnit tests for dismiss-issue REST endpoint (single & large-batch modes) - #1715

Merged
pattonwebz merged 7 commits into
developfrom
william/no-issue/large-batch-dismiss-rest-tests-and-updates
May 28, 2026
Merged

Add PHPUnit tests for dismiss-issue REST endpoint (single & large-batch modes)#1715
pattonwebz merged 7 commits into
developfrom
william/no-issue/large-batch-dismiss-rest-tests-and-updates

Conversation

@pattonwebz

@pattonwebz pattonwebz commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

Adds comprehensive PHPUnit tests for the POST /accessibility-checker/v1/dismiss-issue/{issue_id} REST endpoint covering both single-issue and large-batch dismiss modes.

Tests Added

All tests added to RestApiEndpointsTest:

Test Scenario Expected
test_single_issue_dismiss_authorized_user User with edit_post on the post dismisses a single issue 200, DB updated with ignre=1, reason, comment, and user recorded
test_single_issue_dismiss_unauthorized_user User without edit_post attempts to dismiss a single issue 403 Forbidden
test_large_batch_dismiss_authorized_on_all User can edit all posts in batch, uses largeBatch=true 200, large_batch=true, all issues in batch updated via single bulk UPDATE
test_large_batch_dismiss_authorized_on_some User can edit first issue's post but not all posts in batch 403, no issues modified
test_large_batch_dismiss_unauthorized_on_all User cannot edit any post in the batch 403, no issues modified

What This Verifies

  • Permission gate (edit_post) blocks access before any DB write
  • Large batch mode checks every row's post permission before executing the bulk UPDATE
  • Fail-safe: partial permission failures result in zero DB changes (atomicity of the gate-then-execute pattern)
  • Response shape is correct for both success and failure cases
  • Database state is verified directly after each request

Summary by CodeRabbit

  • Bug Fixes

    • Large-batch dismiss now verifies permissions for every affected item before updating; requests are rejected if any item lacks permission, and a not-found response is returned when no matching issues exist.
  • Tests

    • Added end-to-end tests for the dismiss-issue REST endpoint covering authorized, partially authorized, and unauthorized single-item and large-batch scenarios to validate responses and data updates.

Review Change Stack

Tests cover both single-issue and large-batch dismiss modes:
- Single issue dismissed by authorized user (success case)
- Single issue dismissed by unauthorized user (403 failure)
- Large batch dismissed by user with edit permission on all posts (success with bulk update)
- Large batch dismissed by user with partial authorization (403 failure before bulk query)
- Large batch dismissed by user with no authorization (403 failure)
The tests verify:
- Proper permission checking via edit_post capability
- Correct response status codes and data
- Database state changes (single vs batch updates)
- Fail-safe behavior (no partial updates on permission failure)
Copilot AI review requested due to automatic review settings May 21, 2026 17:35
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds per-row permission checks to the dismiss_issue largeBatch path and PHPUnit tests that cover single-issue and large-batch dismiss scenarios for fully authorized, partially authorized, and unauthorized users.

Changes

Dismiss-Issue Endpoint Implementation & Tests

Layer / File(s) Summary
Server: largeBatch permission gating
includes/classes/class-rest-api.php
dismiss_issue largeBatch now queries matching issue rows, verifies current_user_can('edit_post', $postid) for each post, returns 404 if no rows, returns rest_forbidden if any post fails authorization, and only then performs the bulk UPDATE restricted to vetted issue IDs.
Test fixture setup
tests/phpunit/includes/classes/RestApiEndpointsTest.php
Adds static properties $dismiss_test_posts, $dismiss_test_issues and wpSetUpBeforeClass_DismissIssues() to create posts with different ownership and seed the accessibility_checker table for single and batch dismiss scenarios (full, partial, none).
Single-issue dismiss tests
tests/phpunit/includes/classes/RestApiEndpointsTest.php
test_single_issue_dismiss_authorized_user() asserts 200 with expected payload and DB updates (ignre, ignre_reason, ignre_comment). test_single_issue_dismiss_unauthorized_user() asserts 403 for insufficient permissions.
Batch dismiss tests
tests/phpunit/includes/classes/RestApiEndpointsTest.php
test_large_batch_dismiss_authorized_on_all() asserts 200 and confirms all batch issues updated. test_large_batch_dismiss_authorized_on_some() and test_large_batch_dismiss_unauthorized_on_all() assert 403 and confirm no issues were modified.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped through code with keen delight,

I checked each post by day and night,
batches guarded, single ones too,
tests planted neat to prove it's true,
a carrot cheers the green light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding PHPUnit tests for the dismiss-issue REST endpoint with single and large-batch modes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch william/no-issue/large-batch-dismiss-rest-tests-and-updates

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a suite of PHPUnit tests for the dismiss-issue REST API endpoint, covering single and batch dismissal scenarios across different authorization levels. Feedback highlights a strict type check failure in an assertion where a boolean response is compared to an integer. Additionally, a potential security vulnerability was identified where the API's permission check for batch dismissals may be insufficient, potentially allowing unauthorized users to dismiss issues. Finally, several unused static properties and a dead setup method were found and should be removed to clean up the test suite.

Comment thread tests/phpunit/includes/classes/RestApiEndpointsTest.php Outdated
Comment thread tests/phpunit/includes/classes/RestApiEndpointsTest.php
Comment thread tests/phpunit/includes/classes/RestApiEndpointsTest.php
Comment thread tests/phpunit/includes/classes/RestApiEndpointsTest.php

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds PHPUnit coverage for the POST /accessibility-checker/v1/dismiss-issue/{issue_id} REST endpoint to validate permission gating and database updates for both single-issue dismiss and largeBatch dismiss behavior.

Changes:

  • Added new REST endpoint tests for single-issue dismiss success (authorized) and failure (unauthorized).
  • Added new REST endpoint tests for largeBatch dismiss success (all posts editable) and failure (partial/none editable), including DB state verification.
Comments suppressed due to low confidence (2)

tests/phpunit/includes/classes/RestApiEndpointsTest.php:684

  • Same capability issue as the single-issue test: these posts are created as publish for the limited user, but the limited user only has edit_posts (not edit_published_posts), so edit_post checks may fail and make this test flaky/incorrect. Consider making these posts drafts or extending the limited user's caps for the duration of the test.
		$post_1 = self::factory()->post->create(
			[
				'post_type'    => 'post',
				'post_status'  => 'publish',
				'post_author'  => self::$limited_id,
				'post_title'   => 'Batch Post 1',
				'post_content' => 'Batch Content 1',
			]
		);

		$post_2 = self::factory()->post->create(
			[
				'post_type'    => 'post',
				'post_status'  => 'publish',
				'post_author'  => self::$limited_id,
				'post_title'   => 'Batch Post 2',
				'post_content' => 'Batch Content 2',
			]
		);

tests/phpunit/includes/classes/RestApiEndpointsTest.php:770

  • This test intends to model a "partial authorization" batch, but the limited user's post is created with post_status = publish. Since the limited user only has edit_posts, they may not have edit_post on their own published post, turning this into a "no authorization" scenario and not validating the intended behavior. Use a draft status for the limited-owned post (or grant edit_published_posts) so exactly one row is editable.
		$limited_post = self::factory()->post->create(
			[
				'post_type'    => 'post',
				'post_status'  => 'publish',
				'post_author'  => self::$limited_id,
				'post_title'   => 'Limited Batch Post',
				'post_content' => 'Limited Batch Content',
			]
		);

Comment thread tests/phpunit/includes/classes/RestApiEndpointsTest.php
Comment thread tests/phpunit/includes/classes/RestApiEndpointsTest.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/phpunit/includes/classes/RestApiEndpointsTest.php (1)

87-92: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add edit_published_posts to the limited-user fixture for dismiss-issue authorization

tests/phpunit/includes/classes/RestApiEndpointsTest.php creates the limited user with only edit_posts (line 91). The REST route’s permission check uses current_user_can( 'edit_post', $post_id ), and for post_status => publish that meta-cap maps to requiring edit_published_posts as well—so the “authorized” dismiss cases can hit 403.

Suggested fix
 		$user = new WP_User( self::$limited_id );
 		$user->add_cap( 'edit_posts' );
+		$user->add_cap( 'edit_published_posts' );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/includes/classes/RestApiEndpointsTest.php` around lines 87 -
92, The limited test user fixture (self::$limited_id) only has edit_posts added
via new WP_User(...)->add_cap('edit_posts'), but dismiss-issue authorization for
published posts uses current_user_can('edit_post', $post_id) which requires the
meta-cap edit_published_posts; update the fixture in RestApiEndpointsTest
(around where self::$limited_id is created) to also
add_cap('edit_published_posts') to the WP_User instance so the limited user can
pass edit_post checks on published posts (i.e., call add_cap for
edit_published_posts on the WP_User created for self::$limited_id).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/phpunit/includes/classes/RestApiEndpointsTest.php`:
- Around line 87-92: The limited test user fixture (self::$limited_id) only has
edit_posts added via new WP_User(...)->add_cap('edit_posts'), but dismiss-issue
authorization for published posts uses current_user_can('edit_post', $post_id)
which requires the meta-cap edit_published_posts; update the fixture in
RestApiEndpointsTest (around where self::$limited_id is created) to also
add_cap('edit_published_posts') to the WP_User instance so the limited user can
pass edit_post checks on published posts (i.e., call add_cap for
edit_published_posts on the WP_User created for self::$limited_id).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0971c6a2-f5b3-430a-a426-0f332bc20643

📥 Commits

Reviewing files that changed from the base of the PR and between 51f70d0 and 5fbe333.

📒 Files selected for processing (1)
  • tests/phpunit/includes/classes/RestApiEndpointsTest.php

Before executing the bulk object-based UPDATE, load all issue rows
matching the object and verify the current user has edit_post
capability for each row's post ID.
If any issue in the batch belongs to a post the user cannot edit,
return rest_forbidden immediately without modifying any data. Only
when all permission checks pass does the single bulk UPDATE run.
This avoids thousands of individual UPDATE queries while ensuring
no partial dismissals can occur when a user lacks access to part
of a batch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
includes/classes/class-rest-api.php (1)

1245-1291: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Update only the vetted IDs in the bulk write.

Line 1277 switches back to WHERE ... object = %s, so there is still a TOCTOU auth gap: a row inserted for the same object after the permission scan but before the UPDATE will be dismissed without ever being checked. The bulk write should target the exact ids from $issue_rows (or run under a lock/transaction) so the authorized set and updated set stay identical.

Suggested direction
+			$issue_ids       = array_map( 'intval', wp_list_pluck( $issue_rows, 'id' ) );
+			$id_placeholders = implode( ', ', array_fill( 0, count( $issue_ids ), '%d' ) );

 			// Update all issues with the same object in one query after auth checks pass.
 			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update required, no caching needed.
 			$result = $wpdb->query(
 				$wpdb->prepare(
-					'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_reason = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d AND object = %s',
-					$table_name,
-					$ignre,
-					$ignre_user,
-					$ignre_date,
-					$ignre_reason,
-					$ignre_comment,
-					$ignre_global,
-					$site_id,
-					$object
+					"UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_reason = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d AND id IN ( {$id_placeholders} )",
+					array_merge(
+						[
+							$table_name,
+							$ignre,
+							$ignre_user,
+							$ignre_date,
+							$ignre_reason,
+							$ignre_comment,
+							$ignre_global,
+							$site_id,
+						],
+						$issue_ids
+					)
 				)
 			);
As per coding guidelines, "Follow WordPress security best practices (sanitization, validation, nonces) in all PHP code" and "Implement proper capability checks for all admin operations and AJAX requests in PHP".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/classes/class-rest-api.php` around lines 1245 - 1291, The bulk
UPDATE currently uses WHERE ... object = %s and can update rows inserted after
the permission scan; instead collect the vetted IDs from $issue_rows into a
sanitized $issue_ids array (e.g., map to (int) and ensure non-empty) and change
the UPDATE in the $wpdb->query call to target WHERE id IN (%d,...) AND siteid =
%d (or include object if still needed) so only the originally vetted rows (from
$issue_rows) are updated; alternatively wrap the read+update in a
transaction/lock, but the preferred fix is to use the explicit id list
($issue_ids) in the UPDATE query to close the TOCTOU gap (referencing
$issue_rows, $issue_ids, $wpdb->query, $table_name, $site_id, $object).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@includes/classes/class-rest-api.php`:
- Around line 1245-1291: The bulk UPDATE currently uses WHERE ... object = %s
and can update rows inserted after the permission scan; instead collect the
vetted IDs from $issue_rows into a sanitized $issue_ids array (e.g., map to
(int) and ensure non-empty) and change the UPDATE in the $wpdb->query call to
target WHERE id IN (%d,...) AND siteid = %d (or include object if still needed)
so only the originally vetted rows (from $issue_rows) are updated; alternatively
wrap the read+update in a transaction/lock, but the preferred fix is to use the
explicit id list ($issue_ids) in the UPDATE query to close the TOCTOU gap
(referencing $issue_rows, $issue_ids, $wpdb->query, $table_name, $site_id,
$object).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03c3825d-6275-4eb9-ba99-c5858cc9907f

📥 Commits

Reviewing files that changed from the base of the PR and between 5fbe333 and b47e6bc.

📒 Files selected for processing (1)
  • includes/classes/class-rest-api.php

- Use draft posts in single and batch authorized tests so the limited
  user (edit_posts only, no edit_published_posts) can pass the
  edit_post capability check on their own posts
- Fix assertSame(1, ...) -> assertTrue() for the ignre field since
  the endpoint returns bool $is_ignoring, not int 1
All 12 tests now pass (95 assertions).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/phpunit/includes/classes/RestApiEndpointsTest.php (1)

345-508: ⚡ Quick win

Dead code: wpSetUpBeforeClass_DismissIssues is never called.

The method wpSetUpBeforeClass_DismissIssues and the static properties $dismiss_test_posts / $dismiss_test_issues are never used. WordPress's test framework only auto-invokes wpSetUpBeforeClass—custom suffixes like _DismissIssues require an explicit call from wpSetUpBeforeClass.

Since all five test methods create their own fixtures inline, this code can be removed entirely, or you can call it from wpSetUpBeforeClass and refactor the tests to use the shared fixtures.

Option A: Remove the dead code
-	/**
-	 * Dismiss issue test data: post ID to object mapping.
-	 *
-	 * `@var` array
-	 */
-	protected static $dismiss_test_posts = [];
-
-	/**
-	 * Dismiss issue test data: issue IDs created for batch testing.
-	 *
-	 * `@var` array
-	 */
-	protected static $dismiss_test_issues = [];
-
-	/**
-	 * Set up dismiss-issue test fixtures.
-	 *
-	 * `@param` WP_UnitTest_Factory $factory Factory instance.
-	 * `@return` void
-	 */
-	public static function wpSetUpBeforeClass_DismissIssues( $factory ) {
-		// ... entire method body ...
-	}
Option B: Wire up the fixtures
 	public static function wpSetUpBeforeClass( $factory ) {
 		// Ensure posts are scannable by plugin.
 		update_option( 'edac_post_types', [ 'post' ] );

 		// Ensure plugin DB table exists for tests (normally created via admin_init).
 		( new \EDAC\Admin\Update_Database() )->edac_update_database();

 		self::$admin_id      = $factory->user->create( [ 'role' => 'administrator' ] );
 		self::$limited_id    = $factory->user->create( [ 'role' => 'subscriber' ] );
 		self::$subscriber_id = $factory->user->create( [ 'role' => 'subscriber' ] );
 		// Give limited user edit_posts but not edit_others_posts so they cannot edit this post.
 		$user = new WP_User( self::$limited_id );
 		$user->add_cap( 'edit_posts' );

 		self::$post_id = $factory->post->create(
 			[
 				'post_type'    => 'post',
 				'post_status'  => 'publish',
 				'post_author'  => self::$admin_id,
 				'post_title'   => 'EDAC PHPUnit Post',
 				'post_content' => '<main><h1>Title</h1><p>Img without alt <img src="/wp-includes/images/media/default.png"></p></main>',
 			]
 		);
+
+		// Initialize dismiss-issue fixtures.
+		self::wpSetUpBeforeClass_DismissIssues( $factory );
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/includes/classes/RestApiEndpointsTest.php` around lines 345 -
508, The wpSetUpBeforeClass_DismissIssues method and the static properties
$dismiss_test_posts and $dismiss_test_issues are dead code because WordPress
only auto-calls wpSetUpBeforeClass; either remove
wpSetUpBeforeClass_DismissIssues and the two properties entirely (Option A) or
invoke it from the class's wpSetUpBeforeClass and refactor tests to use the
shared fixtures (Option B); locate the method wpSetUpBeforeClass_DismissIssues
and the properties $dismiss_test_posts / $dismiss_test_issues and implement one
of these two fixes consistently across the test class.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/phpunit/includes/classes/RestApiEndpointsTest.php`:
- Around line 345-508: The wpSetUpBeforeClass_DismissIssues method and the
static properties $dismiss_test_posts and $dismiss_test_issues are dead code
because WordPress only auto-calls wpSetUpBeforeClass; either remove
wpSetUpBeforeClass_DismissIssues and the two properties entirely (Option A) or
invoke it from the class's wpSetUpBeforeClass and refactor tests to use the
shared fixtures (Option B); locate the method wpSetUpBeforeClass_DismissIssues
and the properties $dismiss_test_posts / $dismiss_test_issues and implement one
of these two fixes consistently across the test class.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cf744dbf-6a4f-4331-bcb2-9725dc771d1a

📥 Commits

Reviewing files that changed from the base of the PR and between 583aaeb and aa9e693.

📒 Files selected for processing (2)
  • includes/classes/class-rest-api.php
  • tests/phpunit/includes/classes/RestApiEndpointsTest.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants