diff --git a/mon-pix/app/components/authentication/password-reset-demand/password-reset-demand-form.gjs b/mon-pix/app/components/authentication/password-reset-demand/password-reset-demand-form.gjs index b9e7eeee5ab..9021f6c859f 100644 --- a/mon-pix/app/components/authentication/password-reset-demand/password-reset-demand-form.gjs +++ b/mon-pix/app/components/authentication/password-reset-demand/password-reset-demand-form.gjs @@ -15,6 +15,7 @@ import PasswordResetDemandReceivedInfo from './password-reset-demand-received-in export default class PasswordResetDemandForm extends Component { @service errors; + @service requestManager; @tracked globalError = this.errors.hasErrors && this.errors.shift(); @tracked isLoading = false; @@ -47,22 +48,19 @@ export default class PasswordResetDemandForm extends Component { this.isPasswordResetDemandReceived = false; try { - const response = await window.fetch(`${ENV.APP.API_HOST}/api/password-reset-demands`, { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + await this.requestManager.request({ + url: `${ENV.APP.API_HOST}/api/password-reset-demands`, method: 'POST', body: JSON.stringify({ email }), }); - if (!response.ok && response.status != 404) { - throw new Error(`Response status: ${response.status}`); - } - this.isPasswordResetDemandReceived = true; } catch (error) { - this.globalError = 'common.api-error-messages.internal-server-error'; + if (error.status === 404) { + this.isPasswordResetDemandReceived = true; + } else { + this.globalError = 'common.api-error-messages.internal-server-error'; + } } finally { this.isLoading = false; } diff --git a/mon-pix/app/services/request-manager-handlers/app-info-handler.js b/mon-pix/app/services/request-manager-handlers/app-info-handler.js new file mode 100644 index 00000000000..6f60b703bf3 --- /dev/null +++ b/mon-pix/app/services/request-manager-handlers/app-info-handler.js @@ -0,0 +1,15 @@ +import ENV from 'mon-pix/config/environment'; + +/** + * Request manager handler adding application info in request headers. + * See: https://github.com/emberjs/data/blob/main/guides/requests/examples/1-auth.md + */ +export default class AppInfoHandler { + request(context, next) { + const headers = new Headers(context.request.headers); + + headers.append('X-App-Version', ENV.APP.APP_VERSION); + + return next(Object.assign({}, context.request, { headers })); + } +} diff --git a/mon-pix/app/services/request-manager-handlers/auth-handler.js b/mon-pix/app/services/request-manager-handlers/auth-handler.js new file mode 100644 index 00000000000..226b1cbacf8 --- /dev/null +++ b/mon-pix/app/services/request-manager-handlers/auth-handler.js @@ -0,0 +1,20 @@ +import { inject as service } from '@ember/service'; + +/** + * Request manager handler adding authentication credentials in the request. + * See: https://github.com/emberjs/data/blob/main/guides/requests/examples/1-auth.md + */ +export default class AuthHandler { + @service session; + + request(context, next) { + const headers = new Headers(context.request.headers); + + const { isAuthenticated, data } = this.session; + if (isAuthenticated) { + headers.append('Authorization', `Bearer ${data.authenticated.access_token}`); + } + + return next(Object.assign({}, context.request, { headers })); + } +} diff --git a/mon-pix/app/services/request-manager-handlers/json-handler.js b/mon-pix/app/services/request-manager-handlers/json-handler.js new file mode 100644 index 00000000000..35636438429 --- /dev/null +++ b/mon-pix/app/services/request-manager-handlers/json-handler.js @@ -0,0 +1,14 @@ +/** + * Request manager handler to manage JSON request. + * See: https://github.com/emberjs/data/blob/main/guides/requests/examples/1-auth.md + */ +export default class JsonHandler { + request(context, next) { + const headers = new Headers(context.request.headers); + + headers.append('Accept', 'application/json'); + headers.append('Content-Type', 'application/json'); + + return next(Object.assign({}, context.request, { headers })); + } +} diff --git a/mon-pix/app/services/request-manager-handlers/locale-handler.js b/mon-pix/app/services/request-manager-handlers/locale-handler.js new file mode 100644 index 00000000000..1123fca229a --- /dev/null +++ b/mon-pix/app/services/request-manager-handlers/locale-handler.js @@ -0,0 +1,25 @@ +import { inject as service } from '@ember/service'; + +const FRENCH_FRANCE_LOCALE = 'fr-fr'; + +/** + * Request manager handler adding user locale in request header. + * See: https://github.com/emberjs/data/blob/main/guides/requests/examples/1-auth.md + */ +export default class LocaleHandler { + @service currentDomain; + @service intl; + + request(context, next) { + const headers = new Headers(context.request.headers); + + headers.append('Accept-Language', this._locale); + + return next(Object.assign({}, context.request, { headers })); + } + + get _locale() { + if (this.currentDomain.isFranceDomain) return FRENCH_FRANCE_LOCALE; + return this.intl.primaryLocale; + } +} diff --git a/mon-pix/app/services/request-manager.js b/mon-pix/app/services/request-manager.js new file mode 100644 index 00000000000..828725d7a98 --- /dev/null +++ b/mon-pix/app/services/request-manager.js @@ -0,0 +1,32 @@ +import { getOwner, setOwner } from '@ember/application'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; + +import AppInfoHandler from './request-manager-handlers/app-info-handler.js'; +import AuthHandler from './request-manager-handlers/auth-handler.js'; +import JsonHandler from './request-manager-handlers/json-handler.js'; +import LocaleHandler from './request-manager-handlers/locale-handler.js'; + +/** + * Request manager preconfigured for authenticated or not HTTP requests. + * see: https://api.emberjs.com/ember-data/release/modules/@ember-data%2Frequest + */ +export default class RequestManagerService extends RequestManager { + constructor(createArgs) { + super(createArgs); + + const authHandler = new AuthHandler(); + setOwner(authHandler, getOwner(this)); + + const localHandler = new LocaleHandler(); + setOwner(localHandler, getOwner(this)); + + const appInfoHandler = new AppInfoHandler(); + setOwner(appInfoHandler, getOwner(this)); + + const jsonHandler = new JsonHandler(); + setOwner(jsonHandler, getOwner(this)); + + this.use([authHandler, localHandler, appInfoHandler, jsonHandler, Fetch]); + } +} diff --git a/mon-pix/tests/integration/components/authentication/password-reset-demand/password-reset-demand-form-test.gjs b/mon-pix/tests/integration/components/authentication/password-reset-demand/password-reset-demand-form-test.gjs index aedba90906e..ad546fb2469 100644 --- a/mon-pix/tests/integration/components/authentication/password-reset-demand/password-reset-demand-form-test.gjs +++ b/mon-pix/tests/integration/components/authentication/password-reset-demand/password-reset-demand-form-test.gjs @@ -21,6 +21,13 @@ const I18N_KEYS = { module('Integration | Component | Authentication | PasswordResetDemand | password-reset-demand-form', function (hooks) { setupIntlRenderingTest(hooks); + let requestManagerService; + + hooks.beforeEach(function () { + requestManagerService = this.owner.lookup('service:requestManager'); + sinon.stub(requestManagerService, 'request'); + }); + test('it displays all elements of component successfully', async function (assert) { // given const screen = await render(); @@ -78,11 +85,7 @@ module('Integration | Component | Authentication | PasswordResetDemand | passwor module('when the password-reset-demand is successful', function () { test('it displays a "password reset demand received" info (without any error message)', async function (assert) { // given - window.fetch.resolves( - fetchMock({ - status: 201, - }), - ); + requestManagerService.request.resolves({ response: { ok: true, status: 201 } }); const email = 'someone@example.net'; const locale = ENGLISH_INTERNATIONAL_LOCALE; @@ -127,14 +130,7 @@ module('Integration | Component | Authentication | PasswordResetDemand | passwor module('when there is no corresponding user account', function () { test('it displays a "password reset demand received" info (without any error message to avoid email enumeration)', async function (assert) { // given - window.fetch.resolves( - fetchMock({ - status: 404, - body: { - errors: [{ title: 'Not Found' }], - }, - }), - ); + requestManagerService.request.rejects({ status: 404 }); const email = 'someone@example.net'; const screen = await render(); @@ -157,11 +153,7 @@ module('Integration | Component | Authentication | PasswordResetDemand | passwor module('when there is an unknown error', function () { test('it displays an "unknown error" error message', async function (assert) { // given - window.fetch.resolves( - fetchMock({ - status: 500, - }), - ); + requestManagerService.request.rejects({ status: 500 }); const email = 'someone@example.net'; const screen = await render(); @@ -195,12 +187,3 @@ module('Integration | Component | Authentication | PasswordResetDemand | passwor }); }); }); - -function fetchMock({ body, status }) { - return new window.Response(JSON.stringify(body), { - status, - headers: { - 'Content-type': 'application/json', - }, - }); -} diff --git a/mon-pix/tests/unit/services/request-manager-test.js b/mon-pix/tests/unit/services/request-manager-test.js new file mode 100644 index 00000000000..ba6523cd405 --- /dev/null +++ b/mon-pix/tests/unit/services/request-manager-test.js @@ -0,0 +1,101 @@ +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +module('Unit | Service | request-manager', function (hooks) { + setupTest(hooks); + let requestManagerService; + let sessionService; + let currentDomainService; + let intlService; + + hooks.beforeEach(function () { + sinon.stub(window, 'fetch'); + + requestManagerService = this.owner.lookup('service:requestManager'); + + sessionService = this.owner.lookup('service:session'); + sinon.stub(sessionService, 'isAuthenticated').value(false); + sinon.stub(sessionService, 'data').value(null); + + currentDomainService = this.owner.lookup('service:currentDomain'); + sinon.stub(currentDomainService, 'isFranceDomain').value(false); + + intlService = this.owner.lookup('service:intl'); + sinon.stub(intlService, 'primaryLocale').value('fr'); + }); + + hooks.afterEach(function () { + sinon.restore(); + }); + + module('request()', function () { + test('it requests successfully with default headers', async function (assert) { + // given + window.fetch.resolves(responseMock({ status: 200, data: { foo: 'bar' } })); + + // when + const result = await requestManagerService.request({ url: '/test', method: 'GET' }); + + // then + assert.strictEqual(result.response.status, 200); + assert.deepEqual(result.content, { foo: 'bar' }); + + const [url, { headers }] = window.fetch.getCall(0).args; + assert.strictEqual(url, '/test'); + assert.strictEqual(headers.get('Accept-Language'), 'fr'); + assert.strictEqual(headers.get('X-App-Version'), 'development'); + assert.strictEqual(headers.get('Accept'), 'application/json'); + assert.strictEqual(headers.get('Content-Type'), 'application/json'); + }); + + module('when user is authenticated', function () { + test('it sets the Authorization header with the access token', async function (assert) { + // given + window.fetch.resolves(responseMock({ status: 200, data: { foo: 'bar' } })); + sinon.stub(sessionService, 'isAuthenticated').value(true); + sinon.stub(sessionService, 'data').value({ authenticated: { access_token: 'baz' } }); + + // when + await requestManagerService.request({ url: '/test', method: 'GET' }); + + // then + const [url, { headers }] = window.fetch.getCall(0).args; + assert.strictEqual(url, '/test'); + assert.strictEqual(headers.get('Authorization'), 'Bearer baz'); + }); + }); + + module('when it is on France domain', function () { + test('it sets the header Accept-Language to fr-fr', async function (assert) { + // given + window.fetch.resolves(responseMock({ status: 200, data: { foo: 'bar' } })); + sinon.stub(currentDomainService, 'isFranceDomain').value(true); + + // when + await requestManagerService.request({ url: '/test', method: 'GET' }); + + // then + const [url, { headers }] = window.fetch.getCall(0).args; + assert.strictEqual(url, '/test'); + assert.strictEqual(headers.get('Accept-Language'), 'fr-fr'); + }); + }); + + module('when an error occured on HTTP call', function () { + test('it throws an exception with error details', async function (assert) { + // given + window.fetch.resolves(responseMock({ status: 400, data: { error: 'KO' } })); + + // when + assert.rejects(requestManagerService.request({ url: '/test', method: 'GET' }), function (err) { + return err.status === 400 && err.content.error === 'KO'; + }); + }); + }); + }); +}); + +function responseMock({ status, data }) { + return new window.Response(JSON.stringify(data), { status }); +}