diff --git a/admin/tool/mfa/tests/behat/tool_mfa_setup_and_manage_user_factors.feature b/admin/tool/mfa/tests/behat/tool_mfa_setup_and_manage_user_factors.feature new file mode 100644 index 00000000000..7abfdb43655 --- /dev/null +++ b/admin/tool/mfa/tests/behat/tool_mfa_setup_and_manage_user_factors.feature @@ -0,0 +1,69 @@ +@tool @tool_mfa +Feature: Set up and manage user factors + In order to set up or manage my user factor + As a user + I need to configure the user factor settings in my preferences + + Background: + Given I log in as "admin" + And the following config values are set as admin: + | enabled | 1 | tool_mfa | + + Scenario: I see the correct buttons for factor setup and management displayed + Given the following config values are set as admin: + | enabled | 1 | factor_email | + And the following config values are set as admin: + | enabled | 1 | factor_webauthn | + And the following config values are set as admin: + | enabled | 1 | factor_totp | + And the following "tool_mfa > User factors" exist: + | username | factor | label | + | admin | email | test@test.com | + | admin | webauthn | MacBook | + And I follow "Preferences" in the user menu + When I click on "Multi-factor authentication preferences" "link" + # This is the only factor not yet set up. + Then I should not see "Active" in the "#factor-card-totp" "css_element" + # The following factors are already set up. + And I should see "Active" in the "#factor-card-email" "css_element" + And I should see "Active" in the "#factor-card-webauthn" "css_element" + And I click on "Set up authenticator app" "button" + And I should see "Set up authenticator app" + And I click on "Cancel" "button" + And I click on "Manage security key" "button" + And I should see "Manage security key" + + @javascript + Scenario: I can revoke a factor only when there is more than one active factor + Given the following config values are set as admin: + | enabled | 1 | factor_webauthn | + And the following config values are set as admin: + | enabled | 1 | factor_sms | + And the following "tool_mfa > User factors" exist: + | username | factor | label | + | admin | sms | +409111222 | + | admin | webauthn | MacBook | + And I follow "Preferences" in the user menu + And I click on "Multi-factor authentication preferences" "link" + And I click on "Manage SMS" "button" + And I click on "Remove" "button" in the "+409111222" "table_row" + When I click on "Yes, remove" "button" in the "Remove '+409111222' SMS?" "dialogue" + Then I should see "'SMS mobile phone - +409111222' successfully removed" + # Now there is only one active factor left. + And I click on "Manage security key" "button" + And I should see "Replace" in the "MacBook" "table_row" + And I should not see "Remove" in the "MacBook" "table_row" + + @javascript + Scenario: I can replace a factor + Given the following config values are set as admin: + | enabled | 1 | factor_webauthn | + And the following "tool_mfa > User factors" exist: + | username | factor | label | + | admin | webauthn | MacBook | + And I follow "Preferences" in the user menu + And I click on "Multi-factor authentication preferences" "link" + And I click on "Manage security key" "button" + And I click on "Replace" "button" in the "MacBook" "table_row" + When I click on "Yes, replace" "button" in the "Replace 'MacBook' security key?" "dialogue" + Then I should see "Replace security key" diff --git a/admin/tool/mfa/tests/generator/behat_tool_mfa_generator.php b/admin/tool/mfa/tests/generator/behat_tool_mfa_generator.php new file mode 100644 index 00000000000..4853d3fdb94 --- /dev/null +++ b/admin/tool/mfa/tests/generator/behat_tool_mfa_generator.php @@ -0,0 +1,46 @@ +. + +/** + * Behat data generator for tool_mfa. + * + * @package tool_mfa + * @category test + * @copyright 2024 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_tool_mfa_generator extends behat_generator_base { + + /** + * Get the list of creatable entities for a tool_mfa. + * + * @return array + */ + protected function get_creatable_entities(): array { + + return [ + 'User factors' => [ + 'singular' => 'User factor', + 'datagenerator' => 'user_factors', + 'required' => [ + 'username', + 'factor', + 'label', + ], + ], + ]; + } +} diff --git a/admin/tool/mfa/tests/generator/lib.php b/admin/tool/mfa/tests/generator/lib.php new file mode 100644 index 00000000000..9ac4e8f702f --- /dev/null +++ b/admin/tool/mfa/tests/generator/lib.php @@ -0,0 +1,64 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once(__DIR__ . '/../../lib.php'); + +/** + * Data generator for tool_mfa plugin. + * + * @package tool_mfa + * @category test + * @copyright 2024 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_mfa_generator extends component_generator_base { + /** + * Create user factors. + * + * @param array $record + * @return stdClass + */ + public function create_user_factors(array $record): \stdClass { + global $DB; + + $factorobject = \tool_mfa\plugininfo\factor::get_factor($record['factor']); + if (!$factorobject) { + throw new coding_exception('Unknown factor supplied.'); + } + + $user = $DB->get_record('user', ['username' => $record['username']]); + if (!$user) { + throw new coding_exception('No user found with that username.'); + } + + $record = (object) array_merge([ + 'userid' => $user->id, + 'secret' => '555553', + 'timecreated' => time() - DAYSECS, + 'createdfromip' => '0:0:0:0:0:0:0:1', + 'timemodified' => time() - MINSECS, + 'lastverified' => time(), + 'revoked' => 0, + 'lockcounter' => 0, + ], $record); + $record->id = $DB->insert_record('tool_mfa', $record); + + return $record; + } +} diff --git a/admin/tool/mfa/tests/object_factor_base_test.php b/admin/tool/mfa/tests/object_factor_base_test.php index a2ab262b167..8465f6fc4da 100644 --- a/admin/tool/mfa/tests/object_factor_base_test.php +++ b/admin/tool/mfa/tests/object_factor_base_test.php @@ -82,4 +82,37 @@ class object_factor_base_test extends \advanced_testcase { $this->assertTrue($totpfactor->revoke_user_factor($factorinstance2->id)); $this->assertEquals(0, count($totpfactor->get_active_user_factors($user))); } + + /** + * Tests the replacement of a factor. + * + * @covers ::setup_user_factor + * @covers ::replace_user_factor + */ + public function test_replace_user_factor(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $factor = \tool_mfa\plugininfo\factor::get_factor('totp'); + + // Set up the factor. + $data1 = new \stdClass(); + $data1->secret = 'fakesecret1'; + $data1->devicename = 'fakedevice1'; + $factor1 = $factor->setup_user_factor($data1); + + // Prepare some replacement data. + $data2 = new \stdClass(); + $data2->secret = 'fakesecret2'; + $data2->devicename = 'fakedevice2'; + + // Replace the active factor with the replacement data. + $factor2 = $factor->replace_user_factor($data2, $factor1->id); + + // Check the active factor is the newer one. + $activefactors = $factor->get_active_user_factors($user); + $this->assertEquals(1, count($activefactors)); + $this->assertEquals($factor2->id, $activefactors[0]->id); + } } diff --git a/admin/tool/mfa/tests/plugininfo_factor_test.php b/admin/tool/mfa/tests/plugininfo_factor_test.php index c1c21a6d33b..0c8b65f55e2 100644 --- a/admin/tool/mfa/tests/plugininfo_factor_test.php +++ b/admin/tool/mfa/tests/plugininfo_factor_test.php @@ -75,4 +75,48 @@ class plugininfo_factor_test extends \advanced_testcase { $this->assertEquals(2, count(\tool_mfa\plugininfo\factor::get_active_user_factor_types())); $this->assertEquals('fallback', \tool_mfa\plugininfo\factor::get_next_user_login_factor()->name); } + + /** + * Tests if a user has more than one active factor. + * + * @covers ::user_has_more_than_one_active_factors + */ + public function test_user_has_more_than_one_active_factors(): void { + global $DB; + + $this->resetAfterTest(true); + + // Create a user. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Create two active user factors. + set_config('enabled', 1, 'factor_totp'); + set_config('enabled', 1, 'factor_webauthn'); + + $data = new \stdClass(); + $data->userid = $user->id; + $data->factor = 'totp'; + $data->label = 'testtotp'; + $data->revoked = 0; + $DB->insert_record('tool_mfa', $data); + + $data = new \stdClass(); + $data->userid = $user->id; + $data->factor = 'webauthn'; + $data->label = 'testwebauthn'; + $data->revoked = 0; + $factorid = $DB->insert_record('tool_mfa', $data); + + // Test there is more than one active factor. + $hasmorethanonefactor = \tool_mfa\plugininfo\factor::user_has_more_than_one_active_factors(); + $this->assertTrue($hasmorethanonefactor); + + // Revoke a factor. + $DB->set_field('tool_mfa', 'revoked', 1, ['id' => $factorid]); + + // There should no longer be more than one active factor. + $hasmorethanonefactor = \tool_mfa\plugininfo\factor::user_has_more_than_one_active_factors(); + $this->assertFalse($hasmorethanonefactor); + } }