diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts new file mode 100644 index 00000000000..82496ba752a --- /dev/null +++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts @@ -0,0 +1,269 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Observable, of } from 'rxjs'; +import { + Country, + GlobalMessageService, + GlobalMessageType, + I18nTestingModule, + Region, + RoutingService, + Title, + Translatable, +} from '@spartacus/core'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; +import { UserRegistrationOTPFormComponent } from './user-registration-otp-form.component'; +import { UserRegistrationFormService } from '../form'; +import createSpy = jasmine.createSpy; +import { Pipe, PipeTransform } from '@angular/core'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { + FormErrorsModule, + NgSelectA11yDirective, + SpinnerComponent, +} from '@spartacus/storefront'; +import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive'; + +class MockRoutingService implements Partial { + go = () => Promise.resolve(true); +} + +class MockGlobalMessageService implements Partial { + add(_: string | Translatable, __: GlobalMessageType, ___?: number): void {} +} + +class MockVerificationTokenFacade implements Partial { + createVerificationToken = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); +} + +const mockForm: FormGroup = new FormGroup({ + titleCode: new FormControl(), + firstName: new FormControl(), + lastName: new FormControl(), + companyName: new FormControl(), + email: new FormControl(), + country: new FormGroup({ + isocode: new FormControl(), + }), + region: new FormGroup({ + isocode: new FormControl(), + }), + town: new FormControl(), + line1: new FormControl(), + line2: new FormControl(), + postalCode: new FormControl(), + phoneNumber: new FormControl(), + message: new FormControl(), +}); + +const mockRegions: Region[] = [ + { + isocode: 'CA-ON', + name: 'Ontario', + }, + { + isocode: 'CA-QC', + name: 'Quebec', + }, +]; + +const mockTitles: Title[] = [ + { + code: '0002', + name: 'Mr.', + }, + { + code: '0001', + name: 'Mrs.', + }, +]; + +const mockCountries: Country[] = [ + { + isocode: 'CA', + name: 'Canada', + }, + { + isocode: 'PL', + name: 'Poland', + }, +]; + +class MockUserRegistrationFormService + implements Partial +{ + getTitles(): Observable { + return of(mockTitles); + } + + getCountries(): Observable { + return of(mockCountries); + } + + getRegions(): Observable { + return of(mockRegions); + } + + get form(): FormGroup { + return mockForm; + } +} + +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} +describe('UserRegistrationOTPFormComponent', () => { + let component: UserRegistrationOTPFormComponent; + let fixture: ComponentFixture; + let verificationTokenFacade: VerificationTokenFacade; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + NgSelectModule, + FormErrorsModule, + ], + declarations: [ + UserRegistrationOTPFormComponent, + MockUrlPipe, + NgSelectA11yDirective, + SpinnerComponent, + MockFeatureDirective, + ], + providers: [ + { provide: RoutingService, useClass: MockRoutingService }, + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { + provide: VerificationTokenFacade, + useClass: MockVerificationTokenFacade, + }, + { + provide: UserRegistrationFormService, + useClass: MockUserRegistrationFormService, + }, + ], + }); + verificationTokenFacade = TestBed.inject(VerificationTokenFacade); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserRegistrationOTPFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize form', () => { + expect(component.registerForm).toBeTruthy(); + expect(component.registerForm.get('email')).toBeTruthy(); + }); + + it('should not submit if form is invalid', () => { + component.registerForm.setValue({ + titleCode: '0001', + firstName: 'John', + lastName: 'Doe', + companyName: 'Company', + email: '', + country: { isocode: 'CA' }, + region: { isocode: 'CA-ON' }, + town: 'Townsville', + line1: '123 Main St', + line2: '', + postalCode: '12345', + phoneNumber: '1234567890', + message: '', + }); + component.onSubmit(); + expect( + verificationTokenFacade.createVerificationToken + ).not.toHaveBeenCalled(); + }); + + it('should submit form when valid', () => { + const verificationData = { + loginId: 'test@example.com', + purpose: 'REGISTRATION', + }; + component.registerForm.setValue({ + titleCode: '0001', + firstName: 'John', + lastName: 'Doe', + companyName: 'Company', + email: 'test@example.com', + country: { isocode: 'CA' }, + region: { isocode: 'CA-ON' }, + town: 'Townsville', + line1: '123 Main St', + line2: '', + postalCode: '12345', + phoneNumber: '1234567890', + message: '', + }); + component.onSubmit(); + expect( + verificationTokenFacade.createVerificationToken + ).toHaveBeenCalledWith(verificationData); + }); + + it('should mark all fields as touched if form is invalid', () => { + spyOn(component.registerForm, 'markAllAsTouched').and.callThrough(); + component.registerForm.patchValue({ + email: '', + }); + component.onSubmit(); + expect(component.registerForm.markAllAsTouched).toHaveBeenCalled(); + expect( + verificationTokenFacade.createVerificationToken + ).not.toHaveBeenCalled(); + }); + + it('should navigate to verifyTokenRegister route', () => { + const routingService = TestBed.inject(RoutingService); + spyOn(routingService, 'go').and.callThrough(); + const verificationToken = { tokenId: 'testToken', expiresIn: '300' }; + const formData = { + email: 'test@example.com', + titleCode: '0001', + firstName: 'John', + lastName: 'Doe', + companyName: 'Company', + country: { isocode: 'CA' }, + region: { isocode: 'CA-ON' }, + town: 'Townsville', + line1: '123 Main St', + line2: '', + postalCode: '12345', + phoneNumber: '1234567890', + message: '', + }; + component.registerForm.setValue(formData); + component['goToVerificationTokenForm'](verificationToken, { + loginId: 'test@example.com', + purpose: 'REGISTRATION', + }); + expect(routingService.go).toHaveBeenCalledWith( + { cxRoute: 'verifyTokenRegister' }, + { + state: { + form: formData, + loginId: 'test@example.com', + tokenId: 'testToken', + expiresIn: '300', + }, + } + ); + }); +}); diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts new file mode 100644 index 00000000000..f0ceddfd2d3 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts @@ -0,0 +1,183 @@ +import { TestBed } from '@angular/core/testing'; +import { FormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + AuthConfigService, + GlobalMessageService, + OAuthFlow, + RoutingService, + TranslationService, + GlobalMessageType, +} from '@spartacus/core'; +import { + OrganizationUserRegistration, + UserRegistrationFacade, +} from '@spartacus/organization/user-registration/root'; +import { of } from 'rxjs'; +import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; + +class MockGlobalMessageService implements Partial { + add() {} +} + +class MockOrganizationUserRegistrationFacade + implements Partial +{ + registerUser(userData: OrganizationUserRegistration) { + return of(userData); + } +} + +class MockRoutingService implements Partial { + go = () => Promise.resolve(true); +} + +class MockTranslationService implements Partial { + translate(value: string, options: any) { + return of(value + Object.values(options)); + } +} + +class MockAuthConfigService implements Partial { + getOAuthFlow() { + return OAuthFlow.ResourceOwnerPasswordFlow; + } +} + +describe('RegisterVerificationTokenFormComponentService', () => { + let service: RegisterVerificationTokenFormComponentService; + let routingService: RoutingService; + let globalMessageService: GlobalMessageService; + let userRegistrationFacade: UserRegistrationFacade; + let translationService: TranslationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + FormBuilder, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + { + provide: GlobalMessageService, + useClass: MockGlobalMessageService, + }, + { + provide: AuthConfigService, + useClass: MockAuthConfigService, + }, + { + provide: TranslationService, + useClass: MockTranslationService, + }, + { + provide: UserRegistrationFacade, + useClass: MockOrganizationUserRegistrationFacade, + }, + ], + }); + service = TestBed.inject(RegisterVerificationTokenFormComponentService); + routingService = TestBed.inject(RoutingService); + globalMessageService = TestBed.inject(GlobalMessageService); + userRegistrationFacade = TestBed.inject(UserRegistrationFacade); + translationService = TestBed.inject(TranslationService); + }); + + it('should inject service', () => { + expect(service).toBeTruthy(); + }); + + it('should get the form', () => { + expect(service.form).toBeInstanceOf(UntypedFormGroup); + }); + + it('should redirect to login page', () => { + spyOn(routingService, 'go').and.callThrough(); + + service.registerUser(service.form.value).subscribe().unsubscribe(); + + expect(routingService.go).toHaveBeenCalledWith({ + cxRoute: 'login', + }); + }); + + it('should display a success message after registration', () => { + spyOn(globalMessageService, 'add'); + + service.registerUser(service.form.value).subscribe().unsubscribe(); + + expect(globalMessageService.add).toHaveBeenCalledWith( + { + key: 'userRegistrationForm.successFormSubmitMessage', + params: {}, + }, + GlobalMessageType.MSG_TYPE_CONFIRMATION, + 10000 + ); + }); + + it('should call buildMessageContent and translate correctly', () => { + spyOn(translationService, 'translate').and.callThrough(); + + const formValue = { + phoneNumber: '123456789', + line1: 'Address Line 1', + line2: 'Address Line 2', + city: 'City', + region: { isocode: 'RegionCode' }, + postalCode: '12345', + country: { isocode: 'CountryCode' }, + companyName: 'Company', + message: 'Message content', + }; + + const formValueAfterBind = { + phoneNumber: '123456789', + addressLine: 'Address Line 1', + secondAddressLine: 'Address Line 2', + city: 'City', + state: 'RegionCode', + postalCode: '12345', + country: 'CountryCode', + companyName: 'Company', + message: 'Message content', + }; + + service['buildMessageContent'](formValue).subscribe((result) => { + expect(translationService.translate).toHaveBeenCalledWith( + 'userRegistrationForm.messageToApproverTemplate', + formValueAfterBind + ); + expect(result).toContain('123456789'); + }); + }); + + it('should call registerUser with correct data', () => { + spyOn(userRegistrationFacade, 'registerUser').and.callThrough(); + + service.form.setValue({ + tokenId: 'testTokenId', + tokenCode: 'testTokenCode', + }); + + const formValue = { + titleCode: 'mr', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '123456789', + addressLine: 'Address Line 1', + secondAddressLine: 'Address Line 2', + city: 'City', + region: { isocode: 'RegionCode' }, + postalCode: '12345', + country: { isocode: 'CountryCode' }, + companyName: 'Company', + message: 'Message content', + }; + + service.registerUser(formValue).subscribe(); + + expect(userRegistrationFacade.registerUser).toHaveBeenCalled(); + }); +}); diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts new file mode 100644 index 00000000000..61f8f32530c --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts @@ -0,0 +1,241 @@ +import { + ChangeDetectorRef, + DebugElement, + Pipe, + PipeTransform, +} from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { I18nTestingModule, RoutingService } from '@spartacus/core'; +import { + FormErrorsModule, + LaunchDialogService, + SpinnerModule, +} from '@spartacus/storefront'; +import { BehaviorSubject, of } from 'rxjs'; +import createSpy = jasmine.createSpy; +import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; +import { RegisterVerificationTokenFormComponent } from './verification-token-form.component'; +import { Store } from '@ngrx/store'; +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; +const isBusySubject = new BehaviorSubject(false); + +class MockFormComponentService + implements Partial +{ + form: UntypedFormGroup = new UntypedFormGroup({ + tokenId: new UntypedFormControl(), + tokenCode: new UntypedFormControl(), + }); + login = createSpy().and.stub(); + createVerificationToken = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); + displayMessage = createSpy('displayMessage').and.stub(); +} + +class MockRoutingService { + go = createSpy(); +} +class MockStore { + dispatch = jasmine.createSpy(); + select = jasmine.createSpy().and.returnValue(of({})); +} +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} + +class MockLaunchDialogService implements Partial { + openDialogAndSubscribe = createSpy().and.stub(); +} + +describe('RegisterVerificationTokenFormComponent', () => { + let component: RegisterVerificationTokenFormComponent; + let fixture: ComponentFixture; + let el: DebugElement; + let service: RegisterVerificationTokenFormComponentService; + let facade: VerificationTokenFacade; + let launchDialogService: LaunchDialogService; + let routineservice: RoutingService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + SpinnerModule, + ], + declarations: [RegisterVerificationTokenFormComponent, MockUrlPipe], + providers: [ + { provide: Store, useClass: MockStore }, + { + provide: RegisterVerificationTokenFormComponent, + useClass: MockFormComponentService, + }, + { + provide: LaunchDialogService, + useClass: MockLaunchDialogService, + }, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + ChangeDetectorRef, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterVerificationTokenFormComponent); + service = TestBed.inject(RegisterVerificationTokenFormComponentService); + facade = TestBed.inject(VerificationTokenFacade); + launchDialogService = TestBed.inject(LaunchDialogService); + routineservice = TestBed.inject(RoutingService); + component = fixture.componentInstance; + component.isUpdating$ = isBusySubject; + el = fixture.debugElement; + fixture.detectChanges(); + history.pushState( + { + tokenId: '', + password: 'pw4all', + loginId: 'test@sap.com', + }, + '' + ); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + describe('busy', () => { + it('should disable the submit button when form is disabled', () => { + component.form.disable(); + fixture.detectChanges(); + const submitBtn: HTMLButtonElement = el.query( + By.css('button') + ).nativeElement; + expect(submitBtn.disabled).toBeTruthy(); + }); + + it('should show the spinner', () => { + isBusySubject.next(true); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + + describe('idle', () => { + it('should enable the submit button', () => { + component.form.enable(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('button')); + expect(submitBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should not show the spinner', () => { + isBusySubject.next(false); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeNull(); + }); + }); + + describe('refresh with no tokenId/loginId', () => { + it('should navigate back to login page', () => { + spyOn(service, 'displayMessage'); + history.pushState( + { + tokenId: '', + loginId: '', + }, + '' + ); + component.ngOnInit(); + expect(routineservice.go).toHaveBeenCalledWith(['/login']); + expect(service.displayMessage).toHaveBeenCalledWith( + 'verificationTokenForm.needInputCredentials', + {} + ); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + spyOn(service, 'registerUser').and.returnValue( + of({ + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + titleCode: 'Mr', + password: 'Password123', + }) + ); + component.form.setValue({ + tokenId: 'tokenId', + tokenCode: '123', + }); + component.onSubmit(); + expect(service.registerUser).toHaveBeenCalledWith(component.registerData); + }); + + it('should display info dialog', () => { + component.openInfoDailog(); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); + + it('should display info dialog when keydown', () => { + const event = { + key: 'Enter', + preventDefault: () => {}, + }; + component.onOpenInfoDailogKeyDown(event as KeyboardEvent); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); + + it('should resend OTP', () => { + component.target = 'example@example.com'; + spyOn(component, 'startWaitTimeInterval'); + spyOn(service, 'displayMessage'); + spyOn(facade, 'createVerificationToken').and.returnValue( + of({ + tokenId: 'tokenId', + expiresIn: '300', + }) + ); + + component.resendOTP(); + + expect(component.isResendDisabled).toBe(true); + expect(component.waitTime).toBe(60); + expect(component.startWaitTimeInterval).toHaveBeenCalled(); + expect(facade.createVerificationToken).toHaveBeenCalledWith({ + loginId: 'example@example.com', + purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE, + }); + expect(service.displayMessage).toHaveBeenCalledWith( + 'verificationTokenForm.createVerificationToken', + { target: 'example@example.com' } + ); + }); + }); +});