From 095380686597710eb13262a1705842376346db8e Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 16 Jun 2022 18:43:41 +0530 Subject: [PATCH] script insert and test cases with dynamic site key --- package.json | 2 +- src/app/core/core.module.ts | 5 -- .../data/google-recaptcha.service.spec.ts | 32 --------- src/app/core/data/google-recaptcha.service.ts | 22 ------ .../google-recaptcha.module.ts | 20 ++++++ .../google-recaptcha.service.spec.ts | 47 +++++++++++++ .../google-recaptcha.service.ts | 70 +++++++++++++++++++ .../register-email-form.component.spec.ts | 20 +++--- .../register-email-form.component.ts | 31 ++++---- .../register-email-form.module.ts | 2 + tsconfig.app.json | 2 +- tsconfig.server.json | 3 +- tsconfig.spec.json | 3 +- yarn.lock | 10 +-- 14 files changed, 172 insertions(+), 97 deletions(-) delete mode 100644 src/app/core/data/google-recaptcha.service.spec.ts delete mode 100644 src/app/core/data/google-recaptcha.service.ts create mode 100644 src/app/core/google-recaptcha/google-recaptcha.module.ts create mode 100644 src/app/core/google-recaptcha/google-recaptcha.service.spec.ts create mode 100644 src/app/core/google-recaptcha/google-recaptcha.service.ts diff --git a/package.json b/package.json index 7c81547574..31cc8d7b23 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@nguniversal/express-engine": "^13.0.2", "@ngx-translate/core": "^13.0.0", "@nicky-lenaers/ngx-scroll-to": "^9.0.0", + "@types/grecaptcha": "^3.0.4", "angular-idle-preload": "3.0.0", "angulartics2": "^12.0.0", "axios": "^0.27.2", @@ -110,7 +111,6 @@ "moment": "^2.29.4", "morgan": "^1.10.0", "ng-mocks": "^13.1.1", - "ng-recaptcha": "^9.0.0", "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.3", "ngx-infinite-scroll": "^10.0.1", diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 1010840d76..b16930e819 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -178,8 +178,6 @@ import { OrcidHistoryDataService } from './orcid/orcid-history-data.service'; import { OrcidQueue } from './orcid/model/orcid-queue.model'; import { OrcidHistory } from './orcid/model/orcid-history.model'; import { OrcidAuthService } from './orcid/orcid-auth.service'; -import { GoogleRecaptchaService } from './data/google-recaptcha.service'; -import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY } from 'ng-recaptcha'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -195,7 +193,6 @@ export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => const IMPORTS = [ CommonModule, - RecaptchaV3Module, StoreModule.forFeature('core', coreReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(coreEffects) ]; @@ -312,8 +309,6 @@ const PROVIDERS = [ OrcidAuthService, OrcidQueueService, OrcidHistoryDataService, - GoogleRecaptchaService, - { provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.recaptchaSiteKey } ]; /** diff --git a/src/app/core/data/google-recaptcha.service.spec.ts b/src/app/core/data/google-recaptcha.service.spec.ts deleted file mode 100644 index bc13e3e494..0000000000 --- a/src/app/core/data/google-recaptcha.service.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GoogleRecaptchaService } from './google-recaptcha.service'; -import { of as observableOf } from 'rxjs'; - -describe('GoogleRecaptchaService', () => { - let service: GoogleRecaptchaService; - - let reCaptchaV3Service; - - function init() { - reCaptchaV3Service = jasmine.createSpyObj('reCaptchaV3Service', { - execute: observableOf('googleRecaptchaToken') - }); - service = new GoogleRecaptchaService(reCaptchaV3Service); - } - - beforeEach(() => { - init(); - }); - - describe('getRecaptchaToken', () => { - let result; - - beforeEach(() => { - result = service.getRecaptchaToken('test'); - }); - - it('should send a Request with action', () => { - expect(reCaptchaV3Service.execute).toHaveBeenCalledWith('test'); - }); - - }); -}); diff --git a/src/app/core/data/google-recaptcha.service.ts b/src/app/core/data/google-recaptcha.service.ts deleted file mode 100644 index edf559d501..0000000000 --- a/src/app/core/data/google-recaptcha.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ReCaptchaV3Service } from 'ng-recaptcha'; - -/** - * A GoogleRecaptchaService used to send action and get a token from REST - */ -@Injectable() -export class GoogleRecaptchaService { - - constructor( - private recaptchaV3Service: ReCaptchaV3Service - ) {} - - /** - * Returns an observable of string - * @param action action is the process type in which used to protect multiple spam REST calls - */ - public getRecaptchaToken (action) { - return this.recaptchaV3Service.execute(action); - } - -} diff --git a/src/app/core/google-recaptcha/google-recaptcha.module.ts b/src/app/core/google-recaptcha/google-recaptcha.module.ts new file mode 100644 index 0000000000..e2acba0e93 --- /dev/null +++ b/src/app/core/google-recaptcha/google-recaptcha.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../../shared/shared.module'; + +import { GoogleRecaptchaService } from './google-recaptcha.service'; + +const PROVIDERS = [ + GoogleRecaptchaService +]; + +@NgModule({ + imports: [ SharedModule ], + providers: [ + ...PROVIDERS + ] +}) + +/** + * This module handles google recaptcha functionalities + */ +export class GoogleRecaptchaModule {} diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts new file mode 100644 index 0000000000..3698306763 --- /dev/null +++ b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts @@ -0,0 +1,47 @@ +import { GoogleRecaptchaService } from './google-recaptcha.service'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; + +describe('GoogleRecaptchaService', () => { + let service: GoogleRecaptchaService; + + let rendererFactory2; + let configurationDataService; + let spy: jasmine.Spy; + let scriptElementMock: any; + const innerHTMLTestValue = 'mock-script-inner-html'; + const document = { documentElement: { lang: 'en' } } as Document; + scriptElementMock = { + set innerHTML(newVal) { /* noop */ }, + get innerHTML() { return innerHTMLTestValue; } + }; + + function init() { + rendererFactory2 = jasmine.createSpyObj('rendererFactory2', { + createRenderer: observableOf('googleRecaptchaToken'), + createElement: scriptElementMock + }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] }) + }); + service = new GoogleRecaptchaService(document, rendererFactory2, configurationDataService); + } + + beforeEach(() => { + init(); + }); + + describe('getRecaptchaToken', () => { + let result; + + beforeEach(() => { + spy = spyOn(service, 'getRecaptchaToken').and.stub(); + }); + + it('should send a Request with action', () => { + service.getRecaptchaToken('test'); + expect(spy).toHaveBeenCalledWith('test'); + }); + + }); +}); diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts new file mode 100644 index 0000000000..168e385597 --- /dev/null +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { DOCUMENT } from '@angular/common'; +import { ConfigurationDataService } from '../data/configuration-data.service'; +import { RemoteData } from '../data/remote-data'; + +/** + * A GoogleRecaptchaService used to send action and get a token from REST + */ +@Injectable() +export class GoogleRecaptchaService { + + private renderer: Renderer2; + captchaSiteKey: string; + + constructor( + @Inject(DOCUMENT) private _document: Document, + rendererFactory: RendererFactory2, + private configService: ConfigurationDataService, + ) { + this.renderer = rendererFactory.createRenderer(null, null); + this.configService.findByPropertyName('google.recaptcha.key').pipe( + getFirstCompletedRemoteData(), + ).subscribe((res: RemoteData) => { + if (res.hasSucceeded && isNotEmpty(res?.payload?.values[0])) { + this.captchaSiteKey = res?.payload?.values[0]; + this.loadScript(this.buildCaptchaUrl(res?.payload?.values[0])); + } + }); + } + + /** + * Returns an observable of string + * @param action action is the process type in which used to protect multiple spam REST calls + */ + public async getRecaptchaToken (action) { + return await grecaptcha.execute(this.captchaSiteKey, {action: action}); + } + + /** + * Return the google captcha ur with google captchas api key + * + * @param key contains a secret key of a google captchas + * @returns string which has google captcha url with google captchas key + */ + buildCaptchaUrl(key: string) { + return `https://www.google.com/recaptcha/api.js?render=${key}`; + } + + /** + * Append the google captchas script to the document + * + * @param url contains a script url which will be loaded into page + * @returns A promise + */ + private loadScript(url) { + return new Promise((resolve, reject) => { + const script = this.renderer.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.text = ``; + script.onload = resolve; + script.onerror = reject; + this.renderer.appendChild(this._document.head, script); + }); + } + +} diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 2d80b6d594..330933426a 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -1,4 +1,4 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { waitForAsync, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; import { RestResponse } from '../core/cache/response.models'; import { CommonModule } from '@angular/common'; @@ -15,7 +15,7 @@ import { NotificationsServiceStub } from '../shared/testing/notifications-servic import { RegisterEmailFormComponent } from './register-email-form.component'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; -import { GoogleRecaptchaService } from '../core/data/google-recaptcha.service'; +import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; describe('RegisterEmailComponent', () => { @@ -31,7 +31,7 @@ describe('RegisterEmailComponent', () => { }); const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', { - getRecaptchaToken: jasmine.createSpy('getRecaptchaToken') + getRecaptchaToken: Promise.resolve('googleRecaptchaToken') }); const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] }); @@ -64,7 +64,6 @@ describe('RegisterEmailComponent', () => { fixture = TestBed.createComponent(RegisterEmailFormComponent); comp = fixture.componentInstance; configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$); - googleRecaptchaService.getRecaptchaToken.and.returnValue(observableOf('googleRecaptchaToken')); fixture.detectChanges(); }); @@ -108,29 +107,30 @@ describe('RegisterEmailComponent', () => { }); }); describe('register with google recaptcha', () => { - beforeEach(waitForAsync(() => { + beforeEach(fakeAsync(() => { configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$); comp.ngOnInit(); fixture.detectChanges(); })); - it('should send a registration to the service and on success display a message and return to home', () => { + it('should send a registration to the service and on success display a message and return to home', fakeAsync(() => { comp.form.patchValue({email: 'valid@email.org'}); - comp.register(); + tick(); expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); expect(notificationsService.success).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith(['/home']); - }); - it('should send a registration to the service and on error display a message', () => { + })); + it('should send a registration to the service and on error display a message', fakeAsync(() => { (epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request'))); comp.form.patchValue({email: 'valid@email.org'}); comp.register(); + tick(); expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); expect(notificationsService.error).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); - }); + })); }); }); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 1752865248..c53fbd993d 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -6,12 +6,12 @@ import { Router } from '@angular/router'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { Registration } from '../core/shared/registration.model'; import { RemoteData } from '../core/data/remote-data'; -import { GoogleRecaptchaService } from '../core/data/google-recaptcha.service'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { isNotEmpty } from '../shared/empty.util'; import { map } from 'rxjs/operators'; +import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; @Component({ selector: 'ds-register-email-form', @@ -71,19 +71,18 @@ export class RegisterEmailFormComponent implements OnInit { /** * Register an email address */ - register() { + async register() { if (!this.form.invalid) { if (this.registrationVerification) { - this.googleRecaptchaService.getRecaptchaToken('register_email').subscribe(captcha => { - if (isNotEmpty(captcha)) { - this.registeration(captcha); - } else { - this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), - this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); - } - }); + const token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); + if (isNotEmpty(token)) { + this.registeration(token); + } else { + this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), + this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); + } } else { - this.registeration(null); + this.registeration(); } } } @@ -91,12 +90,14 @@ export class RegisterEmailFormComponent implements OnInit { /** * Register an email address */ - registeration(captchaToken) { - let a = this.epersonRegistrationService.registerEmail(this.email.value); + registeration(captchaToken = null) { + let registerEmail$; if (captchaToken) { - a = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); + registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); + } else { + registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value); } - a.subscribe((response: RemoteData) => { + registerEmail$.subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value})); diff --git a/src/app/register-email-form/register-email-form.module.ts b/src/app/register-email-form/register-email-form.module.ts index f19e869beb..18c2830ff4 100644 --- a/src/app/register-email-form/register-email-form.module.ts +++ b/src/app/register-email-form/register-email-form.module.ts @@ -2,11 +2,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { RegisterEmailFormComponent } from './register-email-form.component'; +import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; @NgModule({ imports: [ CommonModule, SharedModule, + GoogleRecaptchaModule ], declarations: [ RegisterEmailFormComponent, diff --git a/tsconfig.app.json b/tsconfig.app.json index f67e8b25d8..c89834db4a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": ["grecaptcha"] }, "files": [ "src/main.browser.ts", diff --git a/tsconfig.server.json b/tsconfig.server.json index 76be339fca..d2fb2c9d40 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -4,7 +4,8 @@ "outDir": "./out-tsc/app-server", "target": "es2016", "types": [ - "node" + "node", + "grecaptcha" ] }, "files": [ diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 05d55a31f8..0d92677545 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,7 +5,8 @@ "outDir": "./out-tsc/spec", "types": [ "jasmine", - "node" + "node", + "grecaptcha" ] }, "files": [ diff --git a/yarn.lock b/yarn.lock index 7780a764b6..1c262f83d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2247,7 +2247,7 @@ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7" integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ== -"@types/grecaptcha@^3.0.3": +"@types/grecaptcha@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/grecaptcha/-/grecaptcha-3.0.4.tgz#3de601f3b0cd0298faf052dd5bd62aff64c2be2e" integrity sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg== @@ -8863,14 +8863,6 @@ ng-mocks@^13.1.1: resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-13.1.1.tgz#e967dacc420c2ecec71826c5f24e0120186fad0b" integrity sha512-LYi/1ccDwHKLwi4/hvUsmxBDeQ+n8BTdg5f1ujCDCO7OM9OVnqkQcRlBHHK+5iFtEn/aqa2QG3xU/qwIhnaL4w== -ng-recaptcha@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/ng-recaptcha/-/ng-recaptcha-9.0.0.tgz#11b88f820cfca366d363fffd0f451490f2db2b04" - integrity sha512-39YfJh1+p6gvfsGUhC8cmjhFZ1TtQ1OJES5SUgnanPL2aQuwrSX4WyTFh2liFn1dQqtGUVd5O4EhbcexB7AECQ== - dependencies: - "@types/grecaptcha" "^3.0.3" - tslib "^2.2.0" - ng2-file-upload@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.4.0.tgz#8dea28d573234c52af474ad2a4001b335271e5c4"