alias(static fn(string $name, $default) => $default); $settings = new AccessSettings(); self::assertTrue($settings->adminsAreStudioAdmins()); self::assertTrue($settings->adminsAreInstructors()); } public function testStoredZeroDisablesAGrant(): void { Functions\when('get_option')->alias(static function (string $name) { return $name === AccessSettings::OPT_GRANT_STUDIO ? '0' : '1'; }); $settings = new AccessSettings(); self::assertFalse($settings->adminsAreStudioAdmins()); self::assertTrue($settings->adminsAreInstructors()); } /** * Render the page with the given options stored and the given form posted, * returning the options as they end up. * * @param array $options * @param array $post * @return array */ private function submit(array $options, array $post): array { // A regular closure, not an arrow fn: arrow functions capture by value, // so reads after the save would still see the options as they were. Functions\when('get_option')->alias( static function (string $name, mixed $default = false) use (&$options): mixed { return $options[$name] ?? $default; } ); Functions\when('update_option')->alias( static function (string $name, mixed $value) use (&$options): bool { $options[$name] = $value; return true; } ); Functions\when('current_user_can')->justReturn(true); Functions\when('check_admin_referer')->justReturn(true); Functions\when('wp_unslash')->returnArg(); // The typed confirmation is read through sanitize_key, so "Delete", // " delete " and "DELETE" all arrive here as the same word. Functions\when('sanitize_key')->alias( static fn($v): string => (string) preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $v)) ); Functions\when('wp_nonce_field')->justReturn(''); Functions\when('submit_button')->justReturn(''); $_POST = $post + ['usc_action' => 'save']; ob_start(); (new AccessSettings())->renderPage(); ob_end_clean(); $_POST = []; return $options; } /** * Erasing the studio's records cannot be undone, and a checkbox is one stray * click. The tick alone must not be enough. */ public function testTickingDataRemovalWithoutTypingTheWordDoesNotEnableIt(): void { $options = $this->submit( [Uninstaller::OPT_DELETE_DATA => '0'], ['delete_data' => '1', 'grant_studio' => '1'] ); self::assertSame('0', $options[Uninstaller::OPT_DELETE_DATA]); // The rest of the page still saved: a mistyped confirmation must not // silently swallow a capability change made in the same submit. self::assertSame('1', $options[AccessSettings::OPT_GRANT_STUDIO]); } public function testTickAndTypedConfirmationTogetherEnableIt(): void { $options = $this->submit( [Uninstaller::OPT_DELETE_DATA => '0'], ['delete_data' => '1', 'delete_data_confirm' => ' DELETE '] ); self::assertSame('1', $options[Uninstaller::OPT_DELETE_DATA]); } public function testANearMissDoesNotCount(): void { $options = $this->submit( [Uninstaller::OPT_DELETE_DATA => '0'], ['delete_data' => '1', 'delete_data_confirm' => 'delete everything'] ); self::assertSame('0', $options[Uninstaller::OPT_DELETE_DATA]); } /** * Once it is on, saving the page for some other reason must not turn it off * — nor demand the word again for a setting already made. */ public function testSavingOtherSettingsLeavesAnEnabledChoiceAlone(): void { $options = $this->submit( [Uninstaller::OPT_DELETE_DATA => '1'], ['delete_data' => '1', 'grant_instructor' => '1'] ); self::assertSame('1', $options[Uninstaller::OPT_DELETE_DATA]); } public function testUntickingTurnsItOffWithNoCeremony(): void { $options = $this->submit([Uninstaller::OPT_DELETE_DATA => '1'], ['grant_studio' => '1']); self::assertSame('0', $options[Uninstaller::OPT_DELETE_DATA]); } }