Skip to content

Commit 872f8a2

Browse files
PHP: Update password API (#73)
* PHP: Update password API Signed-off-by: Prateek Kumar <[email protected]>
1 parent f83964a commit 872f8a2

8 files changed

+513
-2
lines changed

tests/TestValkeyGlide.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
require_once __DIR__ . "/ValkeyGlideClusterFeaturesTest.php";
8383
require_once __DIR__ . "/ValkeyGlideBatchTest.php";
8484
require_once __DIR__ . "/ValkeyGlideClusterBatchTest.php";
85+
require_once __DIR__ . "/UpdateConnectionPasswordTest.php";
8586
echo "Loading ValkeyGlide tests...\n";
8687
function getClassArray($classes)
8788
{
@@ -112,7 +113,8 @@ function getTestClass($class)
112113
'valkeyglideclientfeatures' => 'ValkeyGlideFeaturesTest',
113114
'valkeyglideclusterfeatures' => 'ValkeyGlideClusterFeaturesTest',
114115
'valkeyglideclientbatch' => 'ValkeyGlideBatchTest',
115-
'valkeyglideclusterbatch' => 'ValkeyGlideClusterBatchTest'
116+
'valkeyglideclusterbatch' => 'ValkeyGlideClusterBatchTest',
117+
'updateconnectionpassword' => 'UpdateConnectionPasswordTest'
116118
];
117119

118120
/* Return early if the class is one of our built-in ones */
@@ -143,7 +145,7 @@ function raHosts($host, $ports)
143145
$opt = getopt('', ['host:', 'port:', 'class:', 'test:', 'nocolors', 'user:', 'auth:', 'tls']);
144146

145147
/* The test class(es) we want to run */
146-
$classes = getClassArray($opt['class'] ?? 'connectionrequest,valkeyglide,valkeyglidecluster,valkeyglideclientfeatures,valkeyglideclusterfeatures,valkeyglideclientbatch,valkeyglideclusterbatch');
148+
$classes = getClassArray($opt['class'] ?? 'connectionrequest,valkeyglide,valkeyglidecluster,valkeyglideclientfeatures,valkeyglideclusterfeatures,valkeyglideclientbatch,valkeyglideclusterbatch,updateconnectionpassword');
147149

148150
$colorize = !isset($opt['nocolors']);
149151

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
<?php
2+
3+
require_once 'TestSuite.php';
4+
5+
class UpdateConnectionPasswordTest extends TestSuite
6+
{
7+
public function setUp()
8+
{
9+
// No-op.
10+
}
11+
12+
private function createClient($password = null)
13+
{
14+
$credentials = $password ? ['password' => $password] : null;
15+
return new ValkeyGlide(
16+
[['host' => '127.0.0.1', 'port' => 6379]],
17+
false,
18+
$credentials
19+
);
20+
}
21+
22+
private function createClusterClient($password = null)
23+
{
24+
$credentials = $password ? ['password' => $password] : null;
25+
return new ValkeyGlideCluster(
26+
[['host' => '127.0.0.1', 'port' => 7001]],
27+
false,
28+
$credentials,
29+
ValkeyGlide::READ_FROM_PRIMARY,
30+
null, null, null, null, null, null, null, 0
31+
);
32+
}
33+
34+
// ========================================
35+
// 1. VALIDATION TESTS (Edge Cases)
36+
// ========================================
37+
38+
// Test that empty password throws exception
39+
public function testUpdateConnectionPasswordEmptyString()
40+
{
41+
$client = $this->createClient();
42+
43+
try {
44+
$client->updateConnectionPassword("", false);
45+
$this->fail("Expected exception for empty password");
46+
} catch (Exception $e) {
47+
$this->assertStringContains("Password cannot be empty", $e->getMessage());
48+
}
49+
50+
$client->close();
51+
}
52+
53+
// Test that empty password throws exception (cluster)
54+
public function testUpdateConnectionPasswordEmptyStringCluster()
55+
{
56+
$client = $this->createClusterClient();
57+
58+
try {
59+
$client->updateConnectionPassword("", false);
60+
$this->fail("Expected exception for empty password");
61+
} catch (Exception $e) {
62+
$this->assertStringContains("Password cannot be empty", $e->getMessage());
63+
}
64+
65+
$client->close();
66+
}
67+
68+
// Test null password - PHP converts to empty string which throws exception
69+
public function testUpdateConnectionPasswordNull()
70+
{
71+
$client = $this->createClient();
72+
73+
try {
74+
// Suppress deprecation warning - null is converted to empty string
75+
@$client->updateConnectionPassword(null, false);
76+
$this->fail("Expected exception for null/empty password");
77+
} catch (Exception $e) {
78+
// Should throw exception for empty password
79+
$this->assertStringContains("Password cannot be empty", $e->getMessage());
80+
}
81+
82+
$client->close();
83+
}
84+
85+
// Test cluster null password - PHP converts to empty string which throws exception
86+
public function testUpdateConnectionPasswordNullCluster()
87+
{
88+
$client = $this->createClusterClient();
89+
90+
try {
91+
// Suppress deprecation warning - null is converted to empty string
92+
@$client->updateConnectionPassword(null, false);
93+
$this->fail("Expected exception for null/empty password");
94+
} catch (Exception $e) {
95+
// Should throw exception for empty password
96+
$this->assertStringContains("Password cannot be empty", $e->getMessage());
97+
}
98+
99+
$client->close();
100+
}
101+
102+
// ========================================
103+
// 2. LONG PASSWORD TESTS
104+
// ========================================
105+
106+
// Test long password (1000+ characters)
107+
public function testUpdateConnectionPasswordLongString()
108+
{
109+
$client = $this->createClient();
110+
111+
$longPassword = str_repeat("a", 1000);
112+
$result = $client->updateConnectionPassword($longPassword, false);
113+
$this->assertEquals("OK", $result, "Update with long password should return OK");
114+
115+
$client->close();
116+
}
117+
118+
// Test cluster client with long password string
119+
public function testUpdateConnectionPasswordLongStringCluster()
120+
{
121+
$client = $this->createClusterClient();
122+
123+
$longPassword = str_repeat("a", 1000);
124+
$result = $client->updateConnectionPassword($longPassword, false);
125+
$this->assertEquals("OK", $result, "Cluster update with long password should return OK");
126+
127+
$client->close();
128+
}
129+
130+
// ========================================
131+
// 3. INVALID PASSWORD TESTS (Auth Failures)
132+
// ========================================
133+
134+
// Test immediate auth with invalid password fails
135+
public function testUpdateConnectionPasswordImmediateAuthInvalidPassword()
136+
{
137+
$client = $this->createClient();
138+
139+
// Verify initial connection
140+
$this->assertNotNull($client->ping(), "Client should be connected");
141+
142+
// Try immediate auth with wrong password (server has no password)
143+
try {
144+
$client->updateConnectionPassword("wrong_password", true);
145+
$this->fail("Expected exception for immediate auth with wrong password");
146+
} catch (Exception $e) {
147+
$this->assertStringContains("AUTH", $e->getMessage(), "Should fail authentication");
148+
}
149+
150+
$client->close();
151+
}
152+
153+
// Test cluster immediate auth with invalid password fails (server has no password)
154+
public function testUpdateConnectionPasswordClusterImmediateAuthInvalidPassword()
155+
{
156+
$client = $this->createClusterClient();
157+
158+
try {
159+
$client->updateConnectionPassword("invalid_password", true);
160+
$this->fail("Expected exception for immediate auth with wrong password");
161+
} catch (Exception $e) {
162+
// Server has no password, so immediate auth with any password should fail
163+
$this->assertStringContains("AUTH", $e->getMessage());
164+
}
165+
166+
$client->close();
167+
}
168+
169+
// ========================================
170+
// 4. SERVER ROTATION - DELAY AUTH
171+
// ========================================
172+
173+
// Test password update with server password rotation (delay auth)
174+
public function testUpdateConnectionPasswordWithServerRotationDelayAuth()
175+
{
176+
$client = $this->createClient();
177+
$adminClient = $this->createClient();
178+
179+
try {
180+
$this->assertNotNull($client->ping(), "Client should be connected");
181+
$this->assertNotNull($adminClient->ping(), "Admin client should be connected");
182+
183+
// Update client connection password
184+
$result = $client->updateConnectionPassword("test_password", false);
185+
$this->assertEquals("OK", $result);
186+
187+
$this->assertNotNull($client->ping(), "Client should still work without reconnect");
188+
189+
// Update server password using admin client
190+
$adminClient->config("SET", "requirepass", "test_password");
191+
192+
// Get client ID and kill only the test client
193+
$clientId = $client->client("ID");
194+
$adminClient->client("KILL", "ID", $clientId);
195+
sleep(1);
196+
197+
$this->assertNotNull($client->ping(), "Client should reconnect with new password");
198+
199+
// Clear client connection password
200+
$result = $client->clearConnectionPassword(false);
201+
$this->assertEquals("OK", $result);
202+
203+
$this->assertNotNull($client->ping(), "Client should still work without reconnect");
204+
205+
// Clear server password using admin client
206+
$adminClient->config("SET", "requirepass", "");
207+
208+
// Kill test client again
209+
$clientId = $client->client("ID");
210+
$adminClient->client("KILL", "ID", $clientId);
211+
sleep(1);
212+
213+
$this->assertNotNull($client->ping(), "Client should reconnect without password");
214+
} finally {
215+
// Ensure server password is cleared even if test fails
216+
try {
217+
if (isset($adminClient)) {
218+
$adminClient->config("SET", "requirepass", "");
219+
}
220+
} catch (Exception $e) {
221+
// Ignore cleanup errors
222+
}
223+
224+
if (isset($client)) {
225+
$client->close();
226+
}
227+
if (isset($adminClient)) {
228+
$adminClient->close();
229+
}
230+
}
231+
}
232+
233+
// Test cluster password rotation with delay auth
234+
public function testUpdateConnectionPasswordClusterWithServerRotationDelayAuth()
235+
{
236+
// TODO: Re-enable once ValkeyGlideCluster::config() is implemented
237+
// SKIP: Cluster config() method is not yet implemented (marked as TODO in stub)
238+
// Cannot set server password on cluster nodes without config() method
239+
// Github issue: https://github.com/valkey-io/valkey-glide-php/issues/77
240+
$this->markTestSkipped("Skipped: ValkeyGlideCluster::config() not yet implemented");
241+
}
242+
243+
// ========================================
244+
// 5. SERVER ROTATION - IMMEDIATE AUTH
245+
// ========================================
246+
247+
// Test password update with immediate auth
248+
public function testUpdateConnectionPasswordWithServerRotationImmediateAuth()
249+
{
250+
$client = $this->createClient();
251+
252+
try {
253+
$this->assertNotNull($client->ping(), "Client should be connected");
254+
255+
// Update server password
256+
$client->config("SET", "requirepass", "test_password");
257+
sleep(1);
258+
259+
// Update client connection password with immediate auth
260+
$result = $client->updateConnectionPassword("test_password", true);
261+
$this->assertEquals("OK", $result);
262+
263+
$this->assertNotNull($client->ping(), "Client should work after immediate auth");
264+
265+
// Clear server password
266+
$client->config("SET", "requirepass", "");
267+
sleep(1);
268+
269+
// Clear client connection password
270+
$result = $client->clearConnectionPassword(false);
271+
$this->assertEquals("OK", $result);
272+
273+
$this->assertNotNull($client->ping(), "Client should work after clearing password");
274+
} finally {
275+
// Ensure server password is cleared even if test fails
276+
try {
277+
if (isset($client)) {
278+
$client->config("SET", "requirepass", "");
279+
}
280+
} catch (Exception $e) {
281+
// Ignore cleanup errors
282+
}
283+
284+
if (isset($client)) {
285+
$client->close();
286+
}
287+
}
288+
}
289+
290+
// Test cluster immediate auth
291+
public function testUpdateConnectionPasswordClusterImmediateAuth()
292+
{
293+
// TODO: Re-enable once ValkeyGlideCluster::config() is implemented
294+
// SKIP: Cluster config() method is not yet implemented (marked as TODO in stub)
295+
// Cannot set server password on cluster nodes without config() method
296+
// Gtihub issue: https://github.com/valkey-io/valkey-glide-php/issues/77
297+
$this->markTestSkipped("Skipped: ValkeyGlideCluster::config() not yet implemented");
298+
}
299+
}

valkey_glide.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "valkey_glide_arginfo.h" // Include generated arginfo header
1414
#include "valkey_glide_cluster_arginfo.h" // Include generated arginfo header
1515
#include "valkey_glide_commands_common.h"
16+
#include "valkey_glide_core_common.h"
1617
#include "valkey_glide_hash_common.h"
1718

1819
/* Enum support includes - must be BEFORE arginfo includes */
@@ -616,6 +617,17 @@ PHP_METHOD(ValkeyGlide, close) {
616617
}
617618
/* }}} */
618619

620+
/* {{{ proto string ValkeyGlide::updateConnectionPassword(string $password, bool $immediateAuth =
621+
* false)
622+
*/
623+
UPDATE_CONNECTION_PASSWORD_METHOD_IMPL(ValkeyGlide)
624+
/* }}} */
625+
626+
/* {{{ proto string ValkeyGlide::clearConnectionPassword(bool $immediateAuth = false)
627+
*/
628+
CLEAR_CONNECTION_PASSWORD_METHOD_IMPL(ValkeyGlide)
629+
/* }}} */
630+
619631
/* Basic method stubs - these need to be implemented with ValkeyGlide */
620632

621633
PHP_METHOD(ValkeyGlide, publish) { /* TODO: Implement */

valkey_glide.stub.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,24 @@ public function client(string $opt, mixed ...$args): mixed;
528528

529529
public function close(): bool;
530530

531+
/**
532+
* Update the connection password.
533+
*
534+
* @param string $password The new password to set
535+
* @param bool $immediateAuth If true, re-authenticate immediately after updating password
536+
* @return string Returns "OK" on success
537+
* @throws Exception if the operation fails or IAM authentication is enabled
538+
*/
539+
public function updateConnectionPassword(string $password, bool $immediateAuth = false): string;
540+
541+
/**
542+
* Clear the connection password.
543+
*
544+
* @param bool $immediateAuth If true, re-authenticate immediately after clearing password
545+
* @return string Returns "OK" on success
546+
* @throws Exception if the operation fails or IAM authentication is enabled
547+
*/
548+
public function clearConnectionPassword(bool $immediateAuth = false): string;
531549

532550
/**
533551
* Execute the ValkeyGlide CONFIG command in a variety of ways.

0 commit comments

Comments
 (0)