Skip to content

Commit d81b817

Browse files
pattonwebzclaude
andcommitted
Add tests for User_Capability_Grant
Covers grant/revoke, attribution recording and defaulting to the current user, revoke clearing attribution, is_individually_granted() distinguishing a direct grant from a role-derived capability, graceful handling of a nonexistent user ID, and the key coexistence guarantee: a direct grant survives a role-level capability removal (proving Synced_Capability-style role sync can never clobber it, without depending on that class). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4efe2e8 commit d81b817

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php
2+
/**
3+
* Tests for User_Capability_Grant.
4+
*
5+
* @package Accessibility_Checker
6+
*/
7+
8+
use EqualizeDigital\AccessibilityChecker\Capabilities\User_Capability_Grant;
9+
10+
/**
11+
* Covers direct per-user capability grants, their attribution, and that
12+
* they coexist safely with role-level capability management (e.g.
13+
* Synced_Capability) rather than being clobbered by it.
14+
*/
15+
class UserCapabilityGrantTest extends WP_UnitTestCase {
16+
17+
/**
18+
* Capability string used only by this test class.
19+
*
20+
* @var string
21+
*/
22+
private const TEST_CAP = 'edac_test_direct_grant_capability';
23+
24+
/**
25+
* Remove the capability from every role and any test users after each
26+
* test so state doesn't leak between tests.
27+
*
28+
* @return void
29+
*/
30+
public function tearDown(): void {
31+
foreach ( wp_roles()->role_objects as $role ) {
32+
$role->remove_cap( self::TEST_CAP );
33+
}
34+
wp_set_current_user( 0 );
35+
parent::tearDown();
36+
}
37+
38+
/**
39+
* Granting a capability to a user should make user_can() true for that
40+
* user, without needing their role to have the capability at all.
41+
*
42+
* @return void
43+
*/
44+
public function test_grant_makes_user_can_true() {
45+
$user_id = self::factory()->user->create( [ 'role' => 'subscriber' ] );
46+
47+
$this->assertFalse( user_can( $user_id, self::TEST_CAP ) );
48+
49+
User_Capability_Grant::grant( $user_id, self::TEST_CAP );
50+
51+
$this->assertTrue( user_can( $user_id, self::TEST_CAP ) );
52+
}
53+
54+
/**
55+
* Revoking a directly-granted capability should make user_can() false
56+
* again.
57+
*
58+
* @return void
59+
*/
60+
public function test_revoke_removes_the_grant() {
61+
$user_id = self::factory()->user->create( [ 'role' => 'subscriber' ] );
62+
63+
User_Capability_Grant::grant( $user_id, self::TEST_CAP );
64+
$this->assertTrue( user_can( $user_id, self::TEST_CAP ) );
65+
66+
User_Capability_Grant::revoke( $user_id, self::TEST_CAP );
67+
$this->assertFalse( user_can( $user_id, self::TEST_CAP ) );
68+
}
69+
70+
/**
71+
* Granting should record who granted it and roughly when.
72+
*
73+
* @return void
74+
*/
75+
public function test_grant_records_attribution() {
76+
$granter_id = self::factory()->user->create( [ 'role' => 'administrator' ] );
77+
$user_id = self::factory()->user->create( [ 'role' => 'subscriber' ] );
78+
79+
User_Capability_Grant::grant( $user_id, self::TEST_CAP, $granter_id );
80+
81+
$info = User_Capability_Grant::get_grant_info( $user_id, self::TEST_CAP );
82+
83+
$this->assertIsArray( $info );
84+
$this->assertSame( $granter_id, $info['granted_by'] );
85+
$this->assertEqualsWithDelta( time(), $info['granted_at'], 5 );
86+
}
87+
88+
/**
89+
* With no explicit granter passed, attribution should default to the
90+
* current user.
91+
*
92+
* @return void
93+
*/
94+
public function test_grant_defaults_attribution_to_current_user() {
95+
$granter_id = self::factory()->user->create( [ 'role' => 'administrator' ] );
96+
$user_id = self::factory()->user->create( [ 'role' => 'subscriber' ] );
97+
98+
wp_set_current_user( $granter_id );
99+
User_Capability_Grant::grant( $user_id, self::TEST_CAP );
100+
101+
$info = User_Capability_Grant::get_grant_info( $user_id, self::TEST_CAP );
102+
103+
$this->assertSame( $granter_id, $info['granted_by'] );
104+
}
105+
106+
/**
107+
* Revoking should clear the attribution record, not just the WordPress
108+
* capability itself.
109+
*
110+
* @return void
111+
*/
112+
public function test_revoke_clears_attribution() {
113+
$user_id = self::factory()->user->create( [ 'role' => 'subscriber' ] );
114+
115+
User_Capability_Grant::grant( $user_id, self::TEST_CAP );
116+
User_Capability_Grant::revoke( $user_id, self::TEST_CAP );
117+
118+
$this->assertNull( User_Capability_Grant::get_grant_info( $user_id, self::TEST_CAP ) );
119+
}
120+
121+
/**
122+
* Get_grant_info() should be null for a user who was never individually
123+
* granted the capability, even if they can() it via their role.
124+
*
125+
* @return void
126+
*/
127+
public function test_grant_info_null_when_capability_only_from_role() {
128+
$user_id = self::factory()->user->create( [ 'role' => 'editor' ] );
129+
wp_roles()->get_role( 'editor' )->add_cap( self::TEST_CAP );
130+
131+
$this->assertTrue( user_can( $user_id, self::TEST_CAP ), 'Precondition: user has the capability via their role.' );
132+
$this->assertNull( User_Capability_Grant::get_grant_info( $user_id, self::TEST_CAP ) );
133+
}
134+
135+
/**
136+
* Is_individually_granted() should distinguish "granted directly to
137+
* this user" from "has it via their role" — both should pass
138+
* user_can(), but only the direct grant should read as individually
139+
* granted.
140+
*
141+
* @return void
142+
*/
143+
public function test_is_individually_granted_distinguishes_from_role_capability() {
144+
$role_user_id = self::factory()->user->create( [ 'role' => 'editor' ] );
145+
$granted_user_id = self::factory()->user->create( [ 'role' => 'subscriber' ] );
146+
147+
wp_roles()->get_role( 'editor' )->add_cap( self::TEST_CAP );
148+
User_Capability_Grant::grant( $granted_user_id, self::TEST_CAP );
149+
150+
$this->assertTrue( user_can( $role_user_id, self::TEST_CAP ) );
151+
$this->assertFalse( User_Capability_Grant::is_individually_granted( $role_user_id, self::TEST_CAP ), 'Role-derived capability is not an individual grant.' );
152+
153+
$this->assertTrue( user_can( $granted_user_id, self::TEST_CAP ) );
154+
$this->assertTrue( User_Capability_Grant::is_individually_granted( $granted_user_id, self::TEST_CAP ) );
155+
}
156+
157+
/**
158+
* A capability granted directly to a user must survive a role-level
159+
* sync that removes the capability from that user's role — this is the
160+
* core coexistence guarantee with Synced_Capability (or any other
161+
* role-only sync), proven here without depending on that class.
162+
*
163+
* @return void
164+
*/
165+
public function test_direct_grant_survives_role_level_capability_removal() {
166+
$user_id = self::factory()->user->create( [ 'role' => 'editor' ] );
167+
168+
wp_roles()->get_role( 'editor' )->add_cap( self::TEST_CAP );
169+
User_Capability_Grant::grant( $user_id, self::TEST_CAP );
170+
171+
// Simulate a role-level sync (like Synced_Capability::sync()) that
172+
// decides 'editor' should no longer have this capability.
173+
wp_roles()->get_role( 'editor' )->remove_cap( self::TEST_CAP );
174+
175+
$this->assertTrue( user_can( $user_id, self::TEST_CAP ), 'Direct grant should survive removal of the capability from the user\'s role.' );
176+
}
177+
178+
/**
179+
* Granting/revoking for a user ID that doesn't exist should fail
180+
* gracefully rather than erroring.
181+
*
182+
* @return void
183+
*/
184+
public function test_grant_and_revoke_return_false_for_nonexistent_user() {
185+
$bogus_id = 999999;
186+
187+
$this->assertFalse( User_Capability_Grant::grant( $bogus_id, self::TEST_CAP ) );
188+
$this->assertFalse( User_Capability_Grant::revoke( $bogus_id, self::TEST_CAP ) );
189+
}
190+
}

0 commit comments

Comments
 (0)