diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 289c2adb77d2a..0000000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,82 +0,0 @@ -image: Visual Studio 2022 -build: false -platform: - - x64 -clone_folder: C:\projects\joomla-cms - -## Build matrix for lowest and highest possible targets -environment: - PHPBuild: "x64" - matrix: - - php_ver_target: 8.1 - - -init: - - SET PATH=C:\Tools\php;%PATH% - - SET COMPOSER_NO_INTERACTION=1 - - SET ANSICON=121x90 (121x90) - -## Install PHP and composer, and run the appropriate composer command -install: - - ps: >- - appveyor-retry choco install --no-progress --params '""/InstallDir:C:\tools\php""' --ignore-checksums -y php --version ((choco search php --exact --all-versions -r | select-string -pattern $env:php_ver_target | sort { [version]($_ -split '\|' | select -last 1) } -Descending | Select-Object -first 1) -replace '[php|]','') - - cd C:\tools\php - - copy php.ini-production php.ini /Y - - echo date.timezone="UTC" >> php.ini - - echo extension_dir=ext >> php.ini - - echo extension=php_openssl.dll >> php.ini - - echo extension=php_mbstring.dll >> php.ini - - echo extension=php_fileinfo.dll >> php.ini - - IF %php_ver_target% LSS 8 echo extension=php_gd2.dll >> php.ini - - IF %php_ver_target% GEQ 8 echo extension=gd >> php.ini - - echo extension=php_gmp.dll >> php.ini - - echo extension=php_pgsql.dll >> php.ini - - echo extension=php_pdo_pgsql.dll >> php.ini - - echo extension=php_pdo_mysql.dll >> php.ini - - echo extension=php_mysqli.dll >> php.ini - - echo extension=php_curl.dll >> php.ini - - echo zend_extension=php_opcache.dll >> php.ini - - echo opcache.enable_cli=1 >> php.ini - - echo extension=php_ldap.dll >> php.ini - - choco install composer - - cd C:\projects\joomla-cms - - refreshenv - - echo "TODO Ignore platform reqs till all composer dependencies are compatible with php 8.1" - - composer install --no-progress --profile --ignore-platform-req=ext-sodium --ignore-platform-reqs - -hosts: - openldap: 127.0.0.1 - -services: - - iis - -before_test: -# Run openldap docker image - - ps: docker pull bitnami/openldap:2.6.3 - - ps: docker run --rm --name openldap --publish 1389:1389 --publish 1636:1636 -v ${pwd}\tests\certs:/certificates --env LDAP_ADMIN_USERNAME=admin --env LDAP_ADMIN_PASSWORD=adminpassword --env LDAP_USERS=customuser --env LDAP_PASSWORDS=custompassword --env LDAP_ENABLE_TLS=yes --env LDAP_TLS_CERT_FILE=/certificates/openldap.crt --env LDAP_TLS_KEY_FILE=/certificates/openldap.key --env LDAP_TLS_CA_FILE=/certificates/CA.crt --env BITNAMI_DEBUG=true --env LDAP_CONFIG_ADMIN_ENABLED=yes --env LDAP_CONFIG_ADMIN_USERNAME=admin --env LDAP_CONFIG_ADMIN_PASSWORD=configpassword -d bitnami/openldap:2.6.3 -# Database setup for MySQL via PowerShell tools - - ps: Start-Service MySQL80 - - > - "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql" -u root -p"Password12!" -e "CREATE DATABASE IF NOT EXISTS test_joomla;" -# Wait till slapd has started - - ps: | - $Counter=0 - $Found=$false - While ( ! $Found -and $Counter -lt 60 ) { - $Found = ( docker logs openldap 2>&1 | Select-String -Quiet "\*\* Starting slapd \*\*" ) - Start-Sleep -Seconds 1 - $Counter++ - "$Counter Waiting for slapd" - } - if ( ! $Found ) { - Write-Error -Message "`nERROR: slapd not started (in time)!" -ErrorAction Stop - exit 1 - } - -test_script: - - cd C:\projects\joomla-cms - - libraries/vendor/bin/phpunit --testsuite Unit - - libraries/vendor/bin/phpunit --testsuite Integration --configuration tests/phpunit-appveyor.xml.dist - -on_failure: - - ps: docker logs openldap 2>&1 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bb3d40090994f..6041566aecfed 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,12 +16,13 @@ libraries/src/Installer/* @rdeutz libraries/src/Updater/* @rdeutz # Automated Testing +.github/workflows/ci.yml @rdeutz @hackwar @laoneo tests/* @hackwar @laoneo tests/Unit/* @rdeutz @laoneo -.appveyor.yml @rdeutz @hackwar @laoneo .drone.yml @rdeutz @hackwar @laoneo phpunit.xml.dist @rdeutz @hackwar @laoneo phpunit-pgsql.xml.dist @rdeutz @hackwar @laoneo +phpunit-windows.xml.dist @rdeutz @hackwar @laoneo # Workflow administrator/components/com_workflow/* @bembelimen @hleithner @@ -51,6 +52,7 @@ installation/tmpl/* @chmst # Translation GitHub Actions .github/workflows/create-translation-pull-request-v4.yml @hleithner +.github/workflows/create-translation-pull-request-v5.yml @hleithner # Libraries libraries/src/* @laoneo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d629b959f5a63..f67a2970a195a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,67 @@ jobs: POSTGRES_PASSWORD: joomla_ut POSTGRES_DB: test_joomla + tests-unit-windows: + name: Run Unit tests (Windows) + runs-on: windows-latest + needs: [code-style-php] + strategy: + matrix: + php_version: ['8.1', '8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + id: cache-php-windows + with: + path: libraries/vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + extensions: openssl, mbstring, fileinfo, gd, gmp, pgsql, mysql, mysqli, curl, opcache, ldap + ini-values: post_max_size=256M, date.timezone="UTC" + - name: Install Composer dependencies + if: steps.cache-php-windows.outputs.cache-hit != 'true' + run: composer install --no-progress --ignore-platform-reqs + - name: Run Unit tests + run: php libraries/vendor/bin/phpunit --testsuite Unit + + tests-integration-windows: + name: Run integration tests (Windows) + runs-on: windows-latest + needs: [code-style-php] + strategy: + matrix: + php_version: ['8.1', '8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + with: + path: libraries/vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + - uses: shogo82148/actions-setup-mysql@v1 + with: + mysql-version: "mariadb-10.4" + root-password: "joomla_ut" + user: "joomla_ut" + password: "joomla_ut" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + extensions: openssl, mbstring, fileinfo, gd, gmp, pgsql, mysql, mysqli, curl, opcache, ldap + ini-values: post_max_size=256M, date.timezone="UTC" + - name: Install Composer dependencies + if: steps.cache-php-windows.outputs.cache-hit != 'true' + run: | + composer install --no-progress --ignore-platform-reqs + mysql -uroot -pjoomla_ut -e 'CREATE DATABASE IF NOT EXISTS test_joomla;' + - name: Run Integration tests + run: | + sleep 3 + php libraries/vendor/bin/phpunit --testsuite Integration --configuration phpunit-windows.xml.dist + tests-system-prepare: name: Prepare system tests runs-on: ubuntu-latest diff --git a/.phan/config.php b/.phan/config.php deleted file mode 100644 index 3557073cdff62..0000000000000 --- a/.phan/config.php +++ /dev/null @@ -1,357 +0,0 @@ - '8.1', - - // If enabled, missing properties will be created when - // they are first seen. If false, we'll report an - // error message if there is an attempt to write - // to a class property that wasn't explicitly - // defined. - 'allow_missing_properties' => true, - - // If enabled, null can be cast to any type and any - // type can be cast to null. Setting this to true - // will cut down on false positives. - 'null_casts_as_any_type' => true, - - // If enabled, allow null to be cast as any array-like type. - // - // This is an incremental step in migrating away from `null_casts_as_any_type`. - // If `null_casts_as_any_type` is true, this has no effect. - 'null_casts_as_array' => true, - - // If enabled, allow any array-like type to be cast to null. - // This is an incremental step in migrating away from `null_casts_as_any_type`. - // If `null_casts_as_any_type` is true, this has no effect. - 'array_casts_as_null' => true, - - // If enabled, scalars (int, float, bool, string, null) - // are treated as if they can cast to each other. - // This does not affect checks of array keys. See `scalar_array_key_cast`. - 'scalar_implicit_cast' => true, - - // If enabled, any scalar array keys (int, string) - // are treated as if they can cast to each other. - // E.g. `array` can cast to `array` and vice versa. - // Normally, a scalar type such as int could only cast to/from int and mixed. - 'scalar_array_key_cast' => true, - - // If this has entries, scalars (int, float, bool, string, null) - // are allowed to perform the casts listed. - // - // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]` - // allows casting null to a string, but not vice versa. - // (subset of `scalar_implicit_cast`) - 'scalar_implicit_partial' => [], - - // If enabled, Phan will warn if **any** type in a method invocation's object - // is definitely not an object, - // or if **any** type in an invoked expression is not a callable. - // Setting this to true will introduce numerous false positives - // (and reveal some bugs). - 'strict_method_checking' => false, - - // If enabled, Phan will warn if **any** type of the object expression for a property access - // does not contain that property. - 'strict_object_checking' => false, - - // If enabled, Phan will warn if **any** type in the argument's union type - // cannot be cast to a type in the parameter's expected union type. - // Setting this to true will introduce numerous false positives - // (and reveal some bugs). - 'strict_param_checking' => false, - - // If enabled, Phan will warn if **any** type in a property assignment's union type - // cannot be cast to a type in the property's declared union type. - // Setting this to true will introduce numerous false positives - // (and reveal some bugs). - 'strict_property_checking' => false, - - // If enabled, Phan will warn if **any** type in a returned value's union type - // cannot be cast to the declared return type. - // Setting this to true will introduce numerous false positives - // (and reveal some bugs). - 'strict_return_checking' => false, - - // If true, seemingly undeclared variables in the global - // scope will be ignored. - // - // This is useful for projects with complicated cross-file - // globals that you have no hope of fixing. - 'ignore_undeclared_variables_in_global_scope' => true, - - // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for, - // but aren't available in the codebase, or from Reflection. - // (may lead to false positives if an extension isn't loaded) - // - // If this is true(default), then Phan will not warn. - // - // Even when this is false, Phan will still infer return values and check parameters of internal functions - // if Phan has the signatures. - 'ignore_undeclared_functions_with_known_signatures' => true, - - // Backwards Compatibility Checking. This is slow - // and expensive, but you should consider running - // it before upgrading your version of PHP to a - // new version that has backward compatibility - // breaks. - // - // If you are migrating from PHP 5 to PHP 7, - // you should also look into using - // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc) - // and [php7mar](https://github.com/Alexia/php7mar), - // which have different backwards compatibility checks. - // - // If you are still using versions of php older than 5.6, - // `PHP53CompatibilityPlugin` may be worth looking into if you are not running - // syntax checks for php 5.3 through another method such as - // `InvokePHPNativeSyntaxCheckPlugin` (see .phan/plugins/README.md). - 'backward_compatibility_checks' => false, - - // If true, check to make sure the return type declared - // in the doc-block (if any) matches the return type - // declared in the method signature. - 'check_docblock_signature_return_type_match' => true, - - // This setting maps case-insensitive strings to union types. - // - // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. - // - // If the corresponding value is the empty string, - // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) - // - // If the corresponding value is not empty, - // then Phan will act as though it saw the corresponding UnionTypes(s) - // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc. - // - // This matches the **entire string**, not parts of the string. - // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) - // - // (These are not aliases, this setting is ignored outside of doc comments). - // (Phan does not check if classes with these names exist) - // - // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']` - 'phpdoc_type_mapping' => [], - - // Set to true in order to attempt to detect dead - // (unreferenced) code. Keep in mind that the - // results will only be a guess given that classes, - // properties, constants and methods can be referenced - // as variables (like `$class->$property` or - // `$class->$method()`) in ways that we're unable - // to make sense of. - // - // To more aggressively detect dead code, - // you may want to set `dead_code_detection_prefer_false_negative` to `false`. - 'dead_code_detection' => false, - - // Set to true in order to attempt to detect unused variables. - // `dead_code_detection` will also enable unused variable detection. - // - // This has a few known false positives, e.g. for loops or branches. - 'unused_variable_detection' => false, - - // Set to true in order to attempt to detect redundant and impossible conditions. - // - // This has some false positives involving loops, - // variables set in branches of loops, and global variables. - 'redundant_condition_detection' => false, - - // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, - // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). - // - // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. - // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. - 'assume_real_types_for_internal_functions' => false, - - // If true, this runs a quick version of checks that takes less - // time at the cost of not running as thorough - // of an analysis. You should consider setting this - // to true only when you wish you had more **undiagnosed** issues - // to fix in your code base. - // - // In quick-mode the scanner doesn't rescan a function - // or a method's code block every time a call is seen. - // This means that the problem here won't be detected: - // - // ```php - // false, - - // Override to hardcode existence and types of (non-builtin) globals in the global scope. - // Class names should be prefixed with `\`. - // - // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`) - 'globals_type_map' => [], - - // The minimum severity level to report on. This can be - // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or - // `Issue::SEVERITY_CRITICAL`. Setting it to only - // critical issues is a good place to start on a big - // sloppy mature code base. - 'minimum_severity' => Issue::SEVERITY_NORMAL, - - // Add any issue types (such as `'PhanUndeclaredMethod'`) - // to this list to inhibit them from being reported. - 'suppress_issue_types' => ['PhanDeprecatedClass', 'PhanUndeclaredConstant','PhanDeprecatedFunction'], - - // A regular expression to match files to be excluded - // from parsing and analysis and will not be read at all. - // - // This is useful for excluding groups of test or example - // directories/files, unanalyzable files, or files that - // can't be removed for whatever reason. - // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`) - 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', - - // A list of files that will be excluded from parsing and analysis - // and will not be read at all. - // - // This is useful for excluding hopelessly unanalyzable - // files that can't be removed for whatever reason. - 'exclude_file_list' => [ - 'administrator\components\com_joomlaupdate\finalisation.php' - ], - - // A directory list that defines files that will be excluded - // from static analysis, but whose class and method - // information should be included. - // - // Generally, you'll want to include the directories for - // third-party code (such as "vendor/") in this list. - // - // n.b.: If you'd like to parse but not analyze 3rd - // party code, directories containing that code - // should be added to the `directory_list` as well as - // to `exclude_analysis_directory_list`. - 'exclude_analysis_directory_list' => [ - 'libraries/vendor/', - 'libraries/php-encryption', - 'libraries/phpass' - ], - - // Enable this to enable checks of require/include statements referring to valid paths. - // The settings `include_paths` and `warn_about_relative_include_statement` affect the checks. - 'enable_include_path_checks' => false, - - // The number of processes to fork off during the analysis - // phase. - 'processes' => 1, - - // List of case-insensitive file extensions supported by Phan. - // (e.g. `['php', 'html', 'htm']`) - 'analyzed_file_extensions' => [ - 'php', - ], - - // You can put paths to stubs of internal extensions in this config option. - // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. - // Phan will continue using its detailed type annotations, - // but load the constants, classes, functions, and classes (and their Reflection types) - // from these stub files (doubling as valid php files). - // Use a different extension from php to avoid accidentally loading these. - // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) - // - // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) - 'autoload_internal_extension_signatures' => [ - 'redis' => '.phan/redis.phan_php', - 'memcached' => '.phan/memcached.phan_php', - 'dom' => '.phan/dom.phan_php', - ], - - // A list of plugin files to execute. - // - // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) - // - // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/v4/.phan/plugins). - // - // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) - 'plugins' => [], - - // A list of directories that should be parsed for class and - // method information. After excluding the directories - // defined in `exclude_analysis_directory_list`, the remaining - // files will be statically analyzed for errors. - // - // Thus, both first-party and third-party code being used by - // your application should be included in this list. - 'directory_list' => [ - 'libraries', - 'libraries/vendor/joomla', - 'administrator', - 'components', - 'plugins' - ], - - // A list of individual files to include in analysis - // with a path relative to the root directory of the - // project. - 'file_list' => [], -]; diff --git a/.phan/dom.phan_php b/.phan/dom.phan_php deleted file mode 100644 index ebb92e84dd117..0000000000000 --- a/.phan/dom.phan_php +++ /dev/null @@ -1,418 +0,0 @@ - - + - ./Unit + ./tests/Unit - ./Integration + ./tests/Integration - - + + - + diff --git a/tests/System/exclude.txt b/tests/System/exclude.txt index f9f6c592d43b6..20d9ef17039a7 100644 --- a/tests/System/exclude.txt +++ b/tests/System/exclude.txt @@ -3,7 +3,6 @@ build dev node_modules -.appveyor.yml .drone.yml .editorconfig .gitignore diff --git a/tests/System/integration/administrator/components/com_users/Mfa.cy.js b/tests/System/integration/administrator/components/com_users/Mfa.cy.js index 040d92756781c..5c06f44af81b4 100644 --- a/tests/System/integration/administrator/components/com_users/Mfa.cy.js +++ b/tests/System/integration/administrator/components/com_users/Mfa.cy.js @@ -1,8 +1,8 @@ import { TOTP } from 'totp-generator'; -afterEach(() => cy.db_getUserId().then((uid) => cy.task('queryDB', `DELETE FROM #__user_mfa WHERE user_id = ${uid}`))); - describe('Test in backend that the user', () => { + afterEach(() => cy.db_getUserId().then((uid) => cy.task('queryDB', `DELETE FROM #__user_mfa WHERE user_id = ${uid}`))); + it('can login with Multi-factor Authentication (email)', () => { cy.doAdministratorLogin(); cy.visit('/administrator/index.php?option=com_users&view=users'); @@ -80,6 +80,44 @@ describe('Test in backend that the user', () => { cy.get('#com-users-methods-reset-message').contains('not enabled'); }); + it('can login with Multi-factor Authentication (passkey)', { browser: '!firefox' }, () => { + Cypress.automation('remote:debugger:protocol', { command: 'WebAuthn.enable', params: {} }).then(() => { + Cypress.automation('remote:debugger:protocol', { + command: 'WebAuthn.addVirtualAuthenticator', + params: { + options: { + protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, + }, + }, + }); + }); + cy.doAdministratorLogin(); + cy.visit('/administrator/index.php?option=com_users&view=users'); + cy.get('.header-profile:visible').click(); + cy.get('.header-profile a.dropdown-item').contains('Edit Account').click(); + cy.get('#myTab div[role="tablist"] button[aria-controls="multifactorauth"]').click(); + cy.get('.com-users-methods-list-method-name-webauthn a.com-users-methods-list-method-addnew').click(); + cy.get('#com-users-method-edit-title').clear().type('Test Passkey'); + cy.get('#toolbar-user-mfa-edit-save').click(); + cy.get('.com-users-methods-list-method-name-webauthn .com-users-methods-list-method-record').contains('Test Passkey'); + cy.clickToolbarButton('Cancel'); + cy.doAdministratorLogout(); + cy.get('#mod-login-username').type(Cypress.env('username')); + cy.get('#mod-login-password').type(Cypress.env('password')); + cy.get('#form-login').submit(); + cy.get('#users-mfa-title').contains('Passkey'); + cy.get('#toolbar-user-mfa-submit').click(); + cy.visit('/administrator/index.php?option=com_users&view=users'); + cy.get('.header-profile:visible').click(); + cy.get('.header-profile a.dropdown-item').contains('Edit Account').click(); + cy.get('#myTab div[role="tablist"] button[aria-controls="multifactorauth"]').click(); + cy.get('#com-users-methods-reset-message').contains('is enabled'); + cy.get('.com-users-methods-list-method-name-webauthn a.com-users-methods-list-method-record-delete').click(); + cy.on('window:confirm', (text) => expect(text).to.contains('Are you sure you want to delete?')); + cy.get('#com-users-methods-reset-message').contains('not enabled'); + cy.then(() => Cypress.automation('remote:debugger:protocol', { command: 'WebAuthn.disable', params: {} })); + }); + it('can login with Multi-factor Authentication (backup codes)', () => { cy.doAdministratorLogin(); cy.visit('/administrator/index.php?option=com_users&view=users'); diff --git a/tests/System/integration/install/Installation.cy.js b/tests/System/integration/install/Installation.cy.js index fd3f1ccc6088b..844ce5085097d 100644 --- a/tests/System/integration/install/Installation.cy.js +++ b/tests/System/integration/install/Installation.cy.js @@ -28,9 +28,6 @@ describe('Install Joomla', () => { cy.setErrorReportingToDevelopment(); cy.doAdministratorLogout(); - // Update to the correct secret for the API tests because of the bearer token - cy.config_setParameter('secret', 'tEstValue'); - // Setup mailing cy.config_setParameter('mailonline', true); cy.config_setParameter('mailer', 'smtp'); diff --git a/tests/System/integration/site/components/com_users/Mfa.cy.js b/tests/System/integration/site/components/com_users/Mfa.cy.js index 91790661a79ea..7712d712ab4d0 100644 --- a/tests/System/integration/site/components/com_users/Mfa.cy.js +++ b/tests/System/integration/site/components/com_users/Mfa.cy.js @@ -1,8 +1,8 @@ import { TOTP } from 'totp-generator'; -afterEach(() => cy.db_getUserId().then((uid) => cy.task('queryDB', `DELETE FROM #__user_mfa WHERE user_id = ${uid}`))); - describe('Test in frontend that the user', () => { + afterEach(() => cy.db_getUserId().then((uid) => cy.task('queryDB', `DELETE FROM #__user_mfa WHERE user_id = ${uid}`))); + it('can login with Multi-factor Authentication (email)', () => { cy.doFrontendLogin(); cy.visit('/index.php?option=com_users&view=profile&layout=edit'); @@ -66,6 +66,37 @@ describe('Test in frontend that the user', () => { cy.get('#com-users-methods-reset-message').contains('not enabled'); }); + it('can login with Multi-factor Authentication (passkey)', { browser: '!firefox' }, () => { + Cypress.automation('remote:debugger:protocol', { command: 'WebAuthn.enable', params: {} }).then(() => { + Cypress.automation('remote:debugger:protocol', { + command: 'WebAuthn.addVirtualAuthenticator', + params: { + options: { + protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, + }, + }, + }); + }); + cy.doFrontendLogin(); + cy.visit('/index.php?option=com_users&view=profile&layout=edit'); + cy.get('.com-users-methods-list-method-name-webauthn a.com-users-methods-list-method-addnew').click(); + cy.get('#com-users-method-edit-title').clear().type('Test Passkey'); + cy.get('#com-users-method-edit button.multifactorauth_webauthn_setup').click(); + cy.get('.com-users-methods-list-method-name-webauthn .com-users-methods-list-method-record').contains('Test Passkey'); + cy.doFrontendLogout(); + cy.get('form.mod-login input[name="username"]').type(Cypress.env('username')); + cy.get('form.mod-login input[name="password"]').type(Cypress.env('password')); + cy.get('form.mod-login').submit(); + cy.get('#users-mfa-title').contains('Passkey'); + cy.get('#users-mfa-captive-button-submit').click(); + cy.visit('/index.php?option=com_users&view=profile&layout=edit'); + cy.get('#com-users-methods-reset-message').contains('is enabled'); + cy.get('.com-users-methods-list-method-name-webauthn a.com-users-methods-list-method-record-delete').click(); + cy.on('window:confirm', (text) => expect(text).to.contains('Are you sure you want to delete?')); + cy.get('#com-users-methods-reset-message').contains('not enabled'); + cy.then(() => Cypress.automation('remote:debugger:protocol', { command: 'WebAuthn.disable', params: {} })); + }); + it('can login with Multi-factor Authentication (backup codes)', () => { cy.doFrontendLogin(); cy.visit('/index.php?option=com_users&view=profile&layout=edit'); diff --git a/tests/System/support/commands/api.mjs b/tests/System/support/commands/api.mjs index f2067c7c805a1..938e101168e28 100644 --- a/tests/System/support/commands/api.mjs +++ b/tests/System/support/commands/api.mjs @@ -15,30 +15,20 @@ Cypress.Commands.add('api_patch', (path, body) => cy.api_getBearerToken().then(( Cypress.Commands.add('api_delete', (path) => cy.api_getBearerToken().then((token) => cy.request({ method: 'DELETE', url: `/api/index.php/v1${path}`, headers: { Authorization: `Bearer ${token}` } }))); -Cypress.Commands.add('api_getBearerToken', () => cy.task('queryDB', "SELECT id FROM #__users WHERE username = 'api'").then((user) => { - if (user.length > 0) { - return 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='; - } - - return cy.db_createUser({ - id: 3, - name: 'API', - email: 'api@example.com', - username: 'api', - password: '123', - block: 0, - registerDate: '2000-01-01', - params: '{}', - group_id: 8, - }).then((id) => { - cy.task( - 'queryDB', - 'INSERT INTO #__user_profiles (user_id, profile_key, profile_value) VALUES ' - + `('${id}', 'joomlatoken.token', 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=')`, - ); - return cy.task( - 'queryDB', - `INSERT INTO #__user_profiles (user_id, profile_key, profile_value) VALUES ('${id}', 'joomlatoken.enabled', 1)`, - ); - }).then(() => 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ=='); -})); +Cypress.Commands.add('api_getBearerToken', () => { + cy.session('apiToken', () => { + cy.db_getUserId().then((uid) => { + cy.doAdministratorLogin(); + cy.visit(`/administrator/index.php?option=com_users&task=user.edit&id=${uid}#attrib-joomlatoken`); + cy.get('#fieldset-joomlatoken').then((fieldset) => { + if (fieldset.find('#jform_joomlatoken_reset1').length > 0) { + cy.get('#jform_joomlatoken_reset1').click(); + } + }); + cy.clickToolbarButton('Save'); + cy.get('#jform_joomlatoken_token').invoke('val').then((token) => { + window.localStorage.setItem('authToken', token); + }); + }); + }).then(() => window.localStorage.getItem('authToken')); +});