fix: verify option writes before treating update_option() false as failure
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -41,16 +41,41 @@ final class WpSettingsAbilitiesTest extends TestCase
|
||||
self::assertArrayHasKey('admin_email', $result->get_error_data()['fields']);
|
||||
}
|
||||
|
||||
public function testRejectsAGenuinelyUnknownKey(): void
|
||||
{
|
||||
Functions\expect('update_option')->never();
|
||||
|
||||
$result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), Mockery::mock(ChangeLog::class)))
|
||||
->execute(['settings' => ['template' => 'twentyten']]);
|
||||
|
||||
self::assertSame('lit_validation_failed', $result->get_error_code());
|
||||
self::assertArrayHasKey('template', $result->get_error_data()['fields']);
|
||||
}
|
||||
|
||||
public function testUpdatesOnlyChangedValuesAndFlushesPermalinks(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $key) => [
|
||||
// A mutating stub: writeOne() verifies a write via get_option() afterwards, so
|
||||
// the stub must actually reflect what update_option()/set_permalink_structure()
|
||||
// "wrote", not just return a fixed value.
|
||||
$options = [
|
||||
'blogname' => 'Alt',
|
||||
'blogdescription' => 'Gleich',
|
||||
'permalink_structure' => '',
|
||||
][$key]);
|
||||
Functions\expect('update_option')->once()->with('blogname', 'Neu');
|
||||
];
|
||||
Functions\when('get_option')->alias(static function (string $key) use (&$options) {
|
||||
return $options[$key];
|
||||
});
|
||||
Functions\expect('update_option')->once()->with('blogname', 'Neu')
|
||||
->andReturnUsing(static function (string $key, $value) use (&$options): bool {
|
||||
$options[$key] = $value;
|
||||
|
||||
return true;
|
||||
});
|
||||
$rewrite = Mockery::mock();
|
||||
$rewrite->expects('set_permalink_structure')->with('/%postname%/');
|
||||
$rewrite->expects('set_permalink_structure')->with('/%postname%/')
|
||||
->andReturnUsing(static function (string $structure) use (&$options): void {
|
||||
$options['permalink_structure'] = $structure;
|
||||
});
|
||||
$GLOBALS['wp_rewrite'] = $rewrite;
|
||||
Functions\expect('flush_rewrite_rules')->once();
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
@@ -67,6 +92,32 @@ final class WpSettingsAbilitiesTest extends TestCase
|
||||
self::assertSame(30, $result['log_id']);
|
||||
}
|
||||
|
||||
public function testDoesNotTreatAnAlreadyStoredValueAsAWriteFailure(): void
|
||||
{
|
||||
// update_option() returns false both for a genuine failure and for a value that
|
||||
// already matches the stored option. WPLANG's public value "en_US" normalizes to
|
||||
// WordPress' internal "" – if the "already set?" check compared the raw client
|
||||
// input against SettingsWhitelist::current()'s aliased public form instead of the
|
||||
// normalized value against the actual stored option, this looked like a change,
|
||||
// update_option() returned false because nothing needed to change, and that false
|
||||
// was wrongly read as a failure – rolling back the unrelated blogname change too.
|
||||
Functions\when('get_option')->alias(static fn (string $key) => [
|
||||
'blogname' => 'Alt',
|
||||
'WPLANG' => '',
|
||||
][$key]);
|
||||
Functions\expect('update_option')->once()->with('blogname', 'Neu')->andReturn(true);
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
$log->expects('record')->andReturn(31);
|
||||
|
||||
$result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), $log))->execute(['settings' => [
|
||||
'blogname' => 'Neu',
|
||||
'WPLANG' => 'en_US',
|
||||
]]);
|
||||
|
||||
self::assertSame(['blogname'], array_column($result['changed'], 'id'));
|
||||
self::assertSame(31, $result['log_id']);
|
||||
}
|
||||
|
||||
public function testRestoresPreviousValuesWhenAWriteFailsPartway(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $key) => [
|
||||
@@ -96,11 +147,21 @@ final class WpSettingsAbilitiesTest extends TestCase
|
||||
|
||||
public function testReportsPartialWriteWhenRestoringAlsoFails(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $key) => [
|
||||
// Stateful: the restore write must genuinely not take effect (stay "Neu") for
|
||||
// isStored() to correctly report the rollback as incomplete.
|
||||
$options = [
|
||||
'blogname' => 'Alt',
|
||||
'blogdescription' => 'Vorher',
|
||||
][$key]);
|
||||
Functions\expect('update_option')->once()->with('blogname', 'Neu')->andReturn(true);
|
||||
];
|
||||
Functions\when('get_option')->alias(static function (string $key) use (&$options) {
|
||||
return $options[$key];
|
||||
});
|
||||
Functions\expect('update_option')->once()->with('blogname', 'Neu')
|
||||
->andReturnUsing(static function (string $key, $value) use (&$options): bool {
|
||||
$options[$key] = $value;
|
||||
|
||||
return true;
|
||||
});
|
||||
Functions\expect('update_option')->once()->with('blogdescription', 'Auch neu')->andReturn(false);
|
||||
Functions\expect('update_option')->once()->with('blogname', 'Alt')->andReturn(false);
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
|
||||
Reference in New Issue
Block a user