diff --git a/mon-pix/app/components/authentication/signin-form.gjs b/mon-pix/app/components/authentication/signin-form.gjs index 1e11f662cab..c767617b8d2 100644 --- a/mon-pix/app/components/authentication/signin-form.gjs +++ b/mon-pix/app/components/authentication/signin-form.gjs @@ -38,6 +38,10 @@ export default class SigninForm extends Component { } } + get isFormDisabled() { + return !this.login || !this.password; + } + @action updateLogin(event) { this.login = event.target.value?.trim(); @@ -148,7 +152,7 @@ export default class SigninForm extends Component { - + {{t "pages.sign-in.actions.submit"}} diff --git a/mon-pix/tests/acceptance/authentication-test.js b/mon-pix/tests/acceptance/authentication-test.js deleted file mode 100644 index 924155c2cfc..00000000000 --- a/mon-pix/tests/acceptance/authentication-test.js +++ /dev/null @@ -1,71 +0,0 @@ -import { visit } from '@1024pix/ember-testing-library'; -import { click, currentURL, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { t } from 'ember-intl/test-support'; -import { setupApplicationTest } from 'ember-qunit'; -import { module, test } from 'qunit'; - -import { authenticateByEmail, authenticateByUsername } from '../helpers/authentication'; -import setupIntl from '../helpers/setup-intl'; - -module('Acceptance | Authentication', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - setupIntl(hooks); - - let user; - - hooks.beforeEach(function () { - user = server.create('user', 'withEmail'); - }); - - module('Success cases', function () { - module('Accessing to the default page page while disconnected', function () { - test('should redirect to the connexion page', async function (assert) { - // when - await visit('/'); - - // then - assert.strictEqual(currentURL(), '/connexion'); - }); - }); - - module('Log-in phase', function () { - test('should redirect to /accueil after connexion', async function (assert) { - // when - await authenticateByEmail(user); - - // then - assert.strictEqual(currentURL(), '/accueil'); - }); - }); - }); - - module('Error case', function () { - test('should stay in /connexion, when authentication failed', async function (assert) { - // given - const screen = await visit('/connexion'); - await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail ou identifiant' }), 'anyone@pix.world'); - await fillIn(screen.getByLabelText('Mot de passe'), 'Pix20!!'); - - // when - await click(screen.getByRole('button', { name: t('pages.sign-in.actions.submit') })); - - // then - assert.strictEqual(currentURL(), '/connexion'); - }); - - module('when user should change password', function () { - test('should redirect to /update-expired-password', async function (assert) { - // given - user = server.create('user', 'withUsername', 'shouldChangePassword'); - - // when - await authenticateByUsername(user); - - // then - assert.strictEqual(currentURL(), '/mise-a-jour-mot-de-passe-expire'); - }); - }); - }); -}); diff --git a/mon-pix/tests/acceptance/authentication/authentication-redirections-test.js b/mon-pix/tests/acceptance/authentication/authentication-redirections-test.js new file mode 100644 index 00000000000..9102e0dfd05 --- /dev/null +++ b/mon-pix/tests/acceptance/authentication/authentication-redirections-test.js @@ -0,0 +1,139 @@ +import { visit } from '@1024pix/ember-testing-library'; +import { click, currentURL, fillIn } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import { authenticateByEmail, authenticateByUsername } from '../../helpers/authentication'; +import setupIntl from '../../helpers/setup-intl'; + +module('Acceptance | Authentication redirections', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + let user; + + hooks.beforeEach(function () { + user = server.create('user', 'withEmail'); + }); + + module('when "New authentication design" feature toggle is disabled', function (hooks) { + hooks.beforeEach(function () { + server.create('feature-toggle', { + id: 0, + isNewAuthenticationDesignEnabled: false, + }); + }); + + module('Success cases', function () { + module('Accessing to the default page page while disconnected', function () { + test('should redirect to the connexion page', async function (assert) { + // when + await visit('/'); + + // then + assert.strictEqual(currentURL(), '/connexion'); + }); + }); + + module('Log-in phase', function () { + test('should redirect to /accueil after connexion', async function (assert) { + // when + await authenticateByEmail(user); + + // then + assert.strictEqual(currentURL(), '/accueil'); + }); + }); + }); + + module('Error case', function () { + test('should stay in /connexion, when authentication failed', async function (assert) { + // given + const screen = await visit('/connexion'); + await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail ou identifiant' }), 'anyone@pix.world'); + await fillIn(screen.getByLabelText('Mot de passe'), 'Pix20!!'); + + // when + await click(screen.getByRole('button', { name: t('pages.sign-in.actions.submit') })); + + // then + assert.strictEqual(currentURL(), '/connexion'); + }); + + module('when user should change password', function () { + test('should redirect to /update-expired-password', async function (assert) { + // given + user = server.create('user', 'withUsername', 'shouldChangePassword'); + + // when + await authenticateByUsername(user); + + // then + assert.strictEqual(currentURL(), '/mise-a-jour-mot-de-passe-expire'); + }); + }); + }); + }); + + module('when "New authentication design" feature toggle is enabled', function (hooks) { + hooks.beforeEach(function () { + server.create('feature-toggle', { + id: 0, + isNewAuthenticationDesignEnabled: true, + }); + }); + + module('Success cases', function () { + module('Accessing to the default page page while disconnected', function () { + test('should redirect to the connexion page', async function (assert) { + // when + await visit('/'); + + // then + assert.strictEqual(currentURL(), '/connexion'); + }); + }); + + module('Log-in phase', function () { + test('should redirect to /accueil after connexion', async function (assert) { + // when + await authenticateByEmail(user); + + // then + assert.strictEqual(currentURL(), '/accueil'); + }); + }); + }); + + module('Error case', function () { + test('should stay in /connexion, when authentication failed', async function (assert) { + // given + const screen = await visit('/connexion'); + await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail ou identifiant' }), 'anyone@pix.world'); + await fillIn(screen.getByLabelText('Mot de passe'), 'Pix20!!'); + + // when + await click(screen.getByRole('button', { name: t('pages.sign-in.actions.submit') })); + + // then + assert.strictEqual(currentURL(), '/connexion'); + }); + + module('when user should change password', function () { + test('should redirect to /update-expired-password', async function (assert) { + // given + user = server.create('user', 'withUsername', 'shouldChangePassword'); + + // when + await authenticateByUsername(user); + + // then + assert.strictEqual(currentURL(), '/mise-a-jour-mot-de-passe-expire'); + }); + }); + }); + }); +}); diff --git a/mon-pix/tests/acceptance/authentication/login-test.js b/mon-pix/tests/acceptance/authentication/login-test.js index 003caf28e45..7e2a96be3dd 100644 --- a/mon-pix/tests/acceptance/authentication/login-test.js +++ b/mon-pix/tests/acceptance/authentication/login-test.js @@ -12,7 +12,7 @@ module('Acceptance | Login', function (hooks) { setupMirage(hooks); setupIntl(hooks); - module('when feature toggle is false', function (hooks) { + module('when "New authentication design" feature toggle is disabled', function (hooks) { hooks.beforeEach(function () { server.create('feature-toggle', { id: 0, @@ -78,20 +78,19 @@ module('Acceptance | Login', function (hooks) { }); }); - module('when feature toggle is true', function (hooks) { + module('when "New authentication design" feature toggle is enabled', function (hooks) { hooks.beforeEach(function () { - server.create('feature-toggle', { - id: 0, - isNewAuthenticationDesignEnabled: true, - }); + server.create('feature-toggle', { id: 0, isNewAuthenticationDesignEnabled: true }); }); - test('displays the new layout with a footer', async function (assert) { + test('displays the authentication layout with a footer', async function (assert) { // when const screen = await visit('/connexion'); + // then assert.dom(screen.getByRole('contentinfo')).exists(); }); + module('when current url does not contain french tld (.fr)', function () { module('when accessing the login page with "English" as selected language', function () { module('when the user select "Français" as his language', function () { @@ -104,7 +103,7 @@ module('Acceptance | Login', function (hooks) { // then assert.strictEqual(currentURL(), '/connexion'); - assert.dom(screen.getByRole('heading', { name: t('pages.sign-in.title'), level: 1 })).exists(); + assert.dom(screen.getByRole('heading', { name: t('pages.sign-in.first-title'), level: 1 })).exists(); assert.dom(screen.getByRole('button', { name: 'Sélectionnez une langue' })).exists(); }); }); diff --git a/mon-pix/tests/integration/components/authentication/signin-form-test.gjs b/mon-pix/tests/integration/components/authentication/signin-form-test.gjs new file mode 100644 index 00000000000..07ba0444606 --- /dev/null +++ b/mon-pix/tests/integration/components/authentication/signin-form-test.gjs @@ -0,0 +1,244 @@ +import { clickByName, fillByLabel, render } from '@1024pix/ember-testing-library'; +import { t } from 'ember-intl/test-support'; +import SigninForm from 'mon-pix/components/authentication/signin-form'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../helpers/setup-intl-rendering'; + +const I18N_KEYS = { + emailInput: 'pages.sign-in.fields.login.label', + passwordInput: 'pages.sign-in.fields.password.label', + submitButton: 'pages.sign-in.actions.submit', +}; + +module('Integration | Component | Authentication | SigninForm', function (hooks) { + setupIntlRenderingTest(hooks); + + let screen; + let storeService; + let routerService; + let sessionService; + + hooks.beforeEach(async function () { + storeService = this.owner.lookup('service:store'); + routerService = this.owner.lookup('service:router'); + sessionService = this.owner.lookup('service:session'); + sinon.stub(sessionService, 'authenticateUser'); + + screen = await render(); + }); + + test('it signs in with valid credentials', async function (assert) { + //given + sessionService.authenticateUser.resolves(); + await fillByLabel(t(I18N_KEYS.emailInput), ' pix@example.net '); + await fillByLabel(t(I18N_KEYS.passwordInput), 'JeMeLoggue1024'); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + assert.ok(sessionService.authenticateUser.calledWith('pix@example.net', 'JeMeLoggue1024')); + }); + + module('When there are spaces in email', function () { + test('it signs in with email trimmed', async function (assert) { + // given + sessionService.authenticateUser.resolves(); + await fillByLabel(t(I18N_KEYS.emailInput), ' pix@example.net '); + await fillByLabel(t(I18N_KEYS.passwordInput), 'JeMeLoggue1024'); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + assert.ok(sessionService.authenticateUser.calledWith('pix@example.net', 'JeMeLoggue1024')); + }); + }); + + module('Rendering', function () { + test('[a11y] it displays a message that all inputs are required', async function (assert) { + // then + assert.dom(screen.getByText(t('common.form.mandatory-all-fields'))).exists(); + }); + + test('it displays a required inputs for email and password fields', async function (assert) { + // then + assert.dom(screen.getByRole('textbox', { name: t(I18N_KEYS.emailInput) })).hasAttribute('required'); + assert.dom(screen.getByLabelText(t(I18N_KEYS.passwordInput))).hasAttribute('required'); + }); + + test('it displays a disabled submission button', async function (assert) { + // then + assert.dom(screen.getByRole('button', { name: t(I18N_KEYS.submitButton) })).hasAttribute('disabled'); + }); + + test('it displays a link to password reset', async function (assert) { + // then + assert.dom(screen.getByRole('link', { name: t('pages.sign-in.forgotten-password') })).exists(); + }); + }); + + module('When a business error occurred', function (hooks) { + hooks.beforeEach(async function () { + await fillByLabel(t(I18N_KEYS.emailInput), 'pix@example.net'); + await fillByLabel(t(I18N_KEYS.passwordInput), 'JeMeLoggue1024'); + }); + + test('it displays error message for invalid local', async function (assert) { + // given + sessionService.authenticateUser.rejects( + _buildApiReponseError({ errorCode: 'INVALID_LOCALE_FORMAT', meta: { locale: 'foo' } }), + ); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('pages.sign-up.errors.invalid-locale-format', { invalidLocale: 'foo' }); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + + test('it displays error message for local not supported', async function (assert) { + // given + sessionService.authenticateUser.rejects( + _buildApiReponseError({ errorCode: 'LOCALE_NOT_SUPPORTED', meta: { locale: 'foo' } }), + ); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('pages.sign-up.errors.locale-not-supported', { localeNotSupported: 'foo' }); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + + test('it displays error message for a user with a temporary password', async function (assert) { + // given + sinon.stub(storeService, 'createRecord'); + sinon.stub(routerService, 'replaceWith'); + const passwordResetToken = 'reset-token'; + sessionService.authenticateUser.rejects( + _buildApiReponseError({ errorCode: 'SHOULD_CHANGE_PASSWORD', meta: passwordResetToken }), + ); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + assert.ok(storeService.createRecord.calledWith('reset-expired-password-demand', { passwordResetToken })); + assert.ok(routerService.replaceWith.calledWith('update-expired-password')); + }); + + test('it displays error message for a user with a temporary blocked account', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ errorCode: 'USER_IS_TEMPORARY_BLOCKED' })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = screen.getByText( + (content) => + content.startsWith( + 'Vous avez effectué trop de tentatives de connexion. Réessayez plus tard ou cliquez sur', + ) && content.endsWith('pour le réinitialiser.'), + ); + assert.dom(errorMessage).exists(); + + const errorMessageLink = screen.getByRole('link', { name: 'mot de passe oublié' }); + assert.dom(errorMessageLink).hasAttribute('href', '/mot-de-passe-oublie'); + }); + + test('it displays error message for a user with a blocked account', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ errorCode: 'USER_IS_BLOCKED' })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = screen.getByText((content) => + content.startsWith( + 'Votre compte est bloqué car vous avez effectué trop de tentatives de connexion. Pour le débloquer,', + ), + ); + assert.dom(errorMessage).exists(); + + const errorMessageLink = screen.getByRole('link', { name: 'contactez-nous' }); + assert.dom(errorMessageLink).hasAttribute('href', 'https://support.pix.org/support/tickets/new'); + }); + }); + + module('When a http error occurred', function (hooks) { + hooks.beforeEach(async function () { + await fillByLabel(t(I18N_KEYS.emailInput), 'pix@example.net'); + await fillByLabel(t(I18N_KEYS.passwordInput), 'JeMeLoggue1024'); + }); + + test('it displays error message for 400 HTTP status code', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ status: 400 })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('common.api-error-messages.bad-request-error'); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + + test('it displays error message for 401 HTTP status code', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ status: 401 })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('common.api-error-messages.login-unauthorized-error'); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + + test('it displays error message for 422 HTTP status code', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ status: 422 })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('common.api-error-messages.bad-request-error'); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + + test('it displays error message for 504 HTTP status code', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ status: 504 })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('common.api-error-messages.gateway-timeout-error'); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + + test('it displays error message for other HTTP status code', async function (assert) { + // given + sessionService.authenticateUser.rejects(_buildApiReponseError({ status: 500 })); + + // when + await clickByName(t(I18N_KEYS.submitButton)); + + // then + const errorMessage = t('common.api-error-messages.internal-server-error'); + assert.dom(screen.getByText(errorMessage)).exists(); + }); + }); +}); + +function _buildApiReponseError({ status = 400, errorCode, meta }) { + return { status, responseJSON: { errors: [{ code: errorCode, meta }] } }; +}