Replace custom Webpack builder with Angular DevKit builder, remove webpack.config.js, and add LoginComponent unit tests

This commit is contained in:
Milad Raeisi
2024-12-10 13:19:14 +04:00
parent e98fb20ced
commit d72d44d5de
6 changed files with 278 additions and 2868 deletions

View File

@@ -15,7 +15,7 @@
"prefix": "app", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-builders/custom-webpack:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "dist", "outputPath": "dist",
"index": "src/index.html", "index": "src/index.html",
@@ -89,7 +89,7 @@
"defaultConfiguration": "production" "defaultConfiguration": "production"
}, },
"serve": { "serve": {
"builder": "@angular-builders/custom-webpack:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "angor:build:production" "buildTarget": "angor:build:production"
@@ -101,10 +101,10 @@
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-builders/custom-webpack:extract-i18n" "builder": "@angular-devkit/build-angular:extract-i18n"
}, },
"test": { "test": {
"builder": "@angular-builders/custom-webpack:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": ["zone.js", "zone.js/testing"], "polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",

2926
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,10 +26,10 @@
class="mt-8" class="mt-8"
[appearance]="'outline'" [appearance]="'outline'"
[showIcon]="false" [showIcon]="false"
[type]="secAlert.type" [type]="secAlert().type"
[@shake]="secAlert.type === 'error'" [@shake]="secAlert().type === 'error'"
> >
{{ secAlert.message }} {{ secAlert().message }}
</angor-alert> </angor-alert>
@@ -154,10 +154,10 @@
class="mt-8" class="mt-8"
[appearance]="'outline'" [appearance]="'outline'"
[showIcon]="false" [showIcon]="false"
[type]="menemonicAlert.type" [type]="menemonicAlert().type"
[@shake]="menemonicAlert.type === 'error'" [@shake]="menemonicAlert().type === 'error'"
> >
{{ menemonicAlert.message }} {{ menemonicAlert().message }}
</angor-alert> </angor-alert>
<!-- Login form with Menemonic --> <!-- Login form with Menemonic -->
<form <form

View File

@@ -0,0 +1,109 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
import { Router } from '@angular/router';
import { SignerService } from 'app/services/signer.service';
import { StateService } from 'app/services/state.service';
import { NostrLoginService } from 'app/services/nostr-login.service';
import { ReactiveFormsModule } from '@angular/forms';
import { of, throwError } from 'rxjs';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let mockRouter: jasmine.SpyObj<Router>;
let mockSignerService: jasmine.SpyObj<SignerService>;
let mockStateService: jasmine.SpyObj<StateService>;
let mockNostrLoginService: jasmine.SpyObj<NostrLoginService>;
beforeEach(async () => {
mockRouter = jasmine.createSpyObj('Router', ['navigateByUrl']);
mockSignerService = jasmine.createSpyObj('SignerService', ['handleLoginWithKey', 'handleLoginWithMnemonic', 'handleLoginWithExtension', 'getPublicKey', 'setPublicKey']);
mockStateService = jasmine.createSpyObj('StateService', ['loadUserProfile']);
mockNostrLoginService = jasmine.createSpyObj('NostrLoginService', ['getPublicKeyObservable', 'launchLoginScreen', 'launchSignupScreen']);
await TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [ReactiveFormsModule],
providers: [
{ provide: Router, useValue: mockRouter },
{ provide: SignerService, useValue: mockSignerService },
{ provide: StateService, useValue: mockStateService },
{ provide: NostrLoginService, useValue: mockNostrLoginService },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
mockNostrLoginService.getPublicKeyObservable.and.returnValue(of('mockPublicKey'));
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize forms on init', () => {
expect(component.SecretKeyLoginForm).toBeDefined();
expect(component.MenemonicLoginForm).toBeDefined();
});
it('should handle login with secret key', () => {
component.SecretKeyLoginForm.setValue({ secretKey: 'mockSecretKey', password: 'mockPassword' });
mockSignerService.handleLoginWithKey.and.returnValue(true);
component.loginWithSecretKey();
expect(mockSignerService.handleLoginWithKey).toHaveBeenCalledWith('mockSecretKey', 'mockPassword');
expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('/home');
});
it('should handle login with mnemonic', () => {
component.MenemonicLoginForm.setValue({ menemonic: 'mockMnemonic', passphrase: 'mockPassphrase', password: 'mockPassword' });
mockSignerService.handleLoginWithMnemonic.and.returnValue(true);
component.loginWithMenemonic();
expect(mockSignerService.handleLoginWithMnemonic).toHaveBeenCalledWith('mockMnemonic', 'mockPassphrase', 'mockPassword');
expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('/home');
});
it('should handle login with Nostr extension', async () => {
mockSignerService.handleLoginWithExtension.and.returnValue(Promise.resolve(true));
await component.loginWithNostrExtension();
expect(mockSignerService.handleLoginWithExtension).toHaveBeenCalled();
expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('/home');
});
it('should handle login failure with secret key', () => {
component.SecretKeyLoginForm.setValue({ secretKey: 'mockSecretKey', password: 'mockPassword' });
mockSignerService.handleLoginWithKey.and.returnValue(false);
component.loginWithSecretKey();
expect(component.loading()).toBeFalse();
expect(component.showSecAlert()).toBeTrue();
});
it('should handle login failure with mnemonic', () => {
component.MenemonicLoginForm.setValue({ menemonic: 'mockMnemonic', passphrase: 'mockPassphrase', password: 'mockPassword' });
mockSignerService.handleLoginWithMnemonic.and.returnValue(false);
component.loginWithMenemonic();
expect(component.loading()).toBeFalse();
expect(component.showMenemonicAlert()).toBeTrue();
});
it('should handle error during login with Nostr extension', async () => {
mockSignerService.handleLoginWithExtension.and.returnValue(Promise.reject('error'));
await component.loginWithNostrExtension();
expect(component.loading()).toBeFalse();
});
});

View File

@@ -1,6 +1,6 @@
import { AngorAlertComponent, AngorAlertType } from '@angor/components/alert'; import { AngorAlertComponent, AngorAlertType } from '@angor/components/alert';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, inject, signal } from '@angular/core';
import { import {
FormBuilder, FormBuilder,
FormGroup, FormGroup,
@@ -40,37 +40,33 @@ import { Subscription } from 'rxjs';
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
SecretKeyLoginForm: FormGroup; SecretKeyLoginForm: FormGroup;
MenemonicLoginForm: FormGroup; MenemonicLoginForm: FormGroup;
secAlert = { type: 'error' as AngorAlertType, message: '' }; secAlert = signal({ type: 'error' as AngorAlertType, message: '' });
showSecAlert = false; showSecAlert = signal(false);
menemonicAlert = { type: 'error' as AngorAlertType, message: '' }; menemonicAlert = signal({ type: 'error' as AngorAlertType, message: '' });
showMenemonicAlert = false; showMenemonicAlert = signal(false);
loading = false; loading = signal(false);
isInstalledExtension = false; isInstalledExtension = signal(false);
privateKey: Uint8Array = new Uint8Array(); privateKey: Uint8Array = new Uint8Array();
publicKey: string = ''; publicKey = signal('');
npub: string = ''; npub = signal('');
nsec: string = ''; nsec = signal('');
useNostrLogin = true;
useNostrLogin = signal(true);
private subscription: Subscription; private subscription: Subscription;
constructor( private _formBuilder = inject(FormBuilder);
private _formBuilder: FormBuilder, private _router = inject(Router);
private _router: Router, private _signerService = inject(SignerService);
private _signerService: SignerService, private _stateService = inject(StateService);
private _stateService: StateService, private _nostrLoginService = inject(NostrLoginService);
private _nostrLoginService: NostrLoginService
) { }
ngOnInit(): void { ngOnInit(): void {
this.subscription = this._nostrLoginService.getPublicKeyObservable().subscribe({ this.subscription = this._nostrLoginService.getPublicKeyObservable().subscribe({
next: (pubkey: string) => { next: (pubkey: string) => {
this.publicKey = pubkey; this.publicKey.set(pubkey);
this._signerService.setPublicKey(pubkey); this._signerService.setPublicKey(pubkey);
this.initializeAppState(); this.initializeAppState();
this._router.navigateByUrl('/home'); this._router.navigateByUrl('/home');
@@ -106,7 +102,7 @@ export class LoginComponent implements OnInit {
this.MenemonicLoginForm = this._formBuilder.group({ this.MenemonicLoginForm = this._formBuilder.group({
menemonic: ['', [Validators.required, Validators.minLength(3)]], menemonic: ['', [Validators.required, Validators.minLength(3)]],
passphrase: [''], passphrase: [''],
password: [''], password: [''],
}); });
} }
@@ -120,9 +116,9 @@ export class LoginComponent implements OnInit {
globalContext.nostr && globalContext.nostr &&
typeof globalContext.nostr.signEvent === 'function' typeof globalContext.nostr.signEvent === 'function'
) { ) {
this.isInstalledExtension = true; this.isInstalledExtension.set(true);
} else { } else {
this.isInstalledExtension = false; this.isInstalledExtension.set(false);
} }
} }
@@ -134,8 +130,8 @@ export class LoginComponent implements OnInit {
const secretKey = this.SecretKeyLoginForm.get('secretKey')?.value; const secretKey = this.SecretKeyLoginForm.get('secretKey')?.value;
const password = this.SecretKeyLoginForm.get('password')?.value; const password = this.SecretKeyLoginForm.get('password')?.value;
this.loading = true; this.loading.set(true);
this.showSecAlert = false; this.showSecAlert.set(false);
try { try {
const success = this._signerService.handleLoginWithKey( const success = this._signerService.handleLoginWithKey(
@@ -152,12 +148,9 @@ export class LoginComponent implements OnInit {
} }
} catch (error) { } catch (error) {
// Handle login failure // Handle login failure
this.loading = false; this.loading.set(false);
this.secAlert.message = this.secAlert.update(alert => ({ ...alert, message: error instanceof Error ? error.message : 'An unexpected error occurred.' }));
error instanceof Error this.showSecAlert.set(true);
? error.message
: 'An unexpected error occurred.';
this.showSecAlert = true;
console.error('Login error: ', error); console.error('Login error: ', error);
} }
} }
@@ -172,8 +165,8 @@ export class LoginComponent implements OnInit {
this.MenemonicLoginForm.get('passphrase')?.value || ''; // Optional passphrase this.MenemonicLoginForm.get('passphrase')?.value || ''; // Optional passphrase
const password = this.MenemonicLoginForm.get('password')?.value; const password = this.MenemonicLoginForm.get('password')?.value;
this.loading = true; this.loading.set(true);
this.showMenemonicAlert = false; this.showMenemonicAlert.set(false);
const success = this._signerService.handleLoginWithMnemonic( const success = this._signerService.handleLoginWithMnemonic(
menemonic, menemonic,
@@ -185,9 +178,9 @@ export class LoginComponent implements OnInit {
this.initializeAppState(); this.initializeAppState();
this._router.navigateByUrl('/home'); this._router.navigateByUrl('/home');
} else { } else {
this.loading = false; this.loading.set(false);
this.menemonicAlert.message = 'Menemonic is missing or invalid.'; this.menemonicAlert.update(alert => ({ ...alert, message: 'Menemonic is missing or invalid.' }));
this.showMenemonicAlert = true; this.showMenemonicAlert.set(true);
} }
} }

View File

@@ -1,24 +0,0 @@
const webpack = require('webpack');
module.exports = {
resolve: {
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
// url: require.resolve('url/'),
url: false,
},
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new webpack.NormalModuleReplacementPlugin(
/node:crypto/,
require.resolve('crypto-browserify')
),
new webpack.DefinePlugin({
global: 'globalThis',
}),
],
};