Compare commits

5 Commits

36 changed files with 2492 additions and 167 deletions
+40 -6
View File
@@ -19,11 +19,33 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="1ac72a4a-52ad-4e70-9b15-c330b1ed3e7a" name="Changes" comment=""> <list default="true" id="1ac72a4a-52ad-4e70-9b15-c330b1ed3e7a" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/utilities/reset-password-validator.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Application/Application.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Application/Application.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Application/Services/AuthenticationService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/Application/Services/AuthenticationService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/package-lock.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/ClientApp/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/package.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/ClientApp/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/app.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/app.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/auth-service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/auth-service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/dark-mode.service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/dark-mode.service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/loading.service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/loading.service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/reset-password.service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/reset-password.service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/toast.service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/toast.service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user-store.service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user-store.service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user.service.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user.service.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/utilities/password-validator.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/utilities/password-validator.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/utilities/validate-form.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/utilities/validate-form.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/forget-password-popup/forget-password-popup.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/forget-password-popup/forget-password-popup.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/login/login.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/login/login.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/register/register.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/register/register.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/reset-password/reset-password.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/reset-password/reset-password.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/footer/footer.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/footer/footer.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/header/header.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/header/header.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -34,7 +56,7 @@
<option name="firstShow" value="false" /> <option name="firstShow" value="false" />
</component> </component>
<component name="EmbeddingIndexingInfo"> <component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="7" /> <option name="cachedIndexableFilesCount" value="1" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" /> <option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component> </component>
<component name="Git.Settings"> <component name="Git.Settings">
@@ -79,7 +101,8 @@
"cidr.known.project.marker": "true", "cidr.known.project.marker": "true",
"codeWithMe.voiceChat.enabledByDefault": "false", "codeWithMe.voiceChat.enabledByDefault": "false",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true", "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
"git-widget-placeholder": "feature/angular", "git-widget-placeholder": "feature/tests",
"git.auto.fetch.suggestion.counter": "1",
"junie.onboarding.icon.badge.shown": "true", "junie.onboarding.icon.badge.shown": "true",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
@@ -235,6 +258,8 @@
<workItem from="1773605341198" duration="3147000" /> <workItem from="1773605341198" duration="3147000" />
<workItem from="1773678089390" duration="1025000" /> <workItem from="1773678089390" duration="1025000" />
<workItem from="1777730558210" duration="1422000" /> <workItem from="1777730558210" duration="1422000" />
<workItem from="1778341026510" duration="1284000" />
<workItem from="1778343547356" duration="1390000" />
</task> </task>
<task id="LOCAL-00001" summary="updating template"> <task id="LOCAL-00001" summary="updating template">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -300,7 +325,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1773601314558</updated> <updated>1773601314558</updated>
</task> </task>
<option name="localTasksCounter" value="9" /> <task id="LOCAL-00009" summary="update angular and fix warnings">
<option name="closed" value="true" />
<created>1777739314669</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1777739314669</updated>
</task>
<option name="localTasksCounter" value="10" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -345,7 +378,8 @@
<MESSAGE value="update Angular core to new version" /> <MESSAGE value="update Angular core to new version" />
<MESSAGE value="fix dropdown" /> <MESSAGE value="fix dropdown" />
<MESSAGE value="update nuget packages to new version" /> <MESSAGE value="update nuget packages to new version" />
<option name="LAST_COMMIT_MESSAGE" value="update nuget packages to new version" /> <MESSAGE value="update angular and fix warnings" />
<option name="LAST_COMMIT_MESSAGE" value="update angular and fix warnings" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>
+3
View File
@@ -1,2 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=4adf65b1_002D2697_002D427f_002Db68a_002D93ae8ffb5baf/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Solution /&gt;
&lt;/SessionState&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hostingstartupassemblies/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=hostingstartupassemblies/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+922
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -38,8 +38,10 @@
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"puppeteer": "^24.43.0",
"typescript": "~5.9.3" "typescript": "~5.9.3"
} }
} }
+13 -7
View File
@@ -1,10 +1,23 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {AuthService} from './infrastructure/services/auth-service';
import {ActivatedRoute, provideRouter} from '@angular/router';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(async () => { beforeEach(async () => {
localStorage.clear();
const authSpy = jasmine.createSpyObj('AuthService', [
'getUsernameFromToken', 'getRoleFromToken', 'getEmailFromToken', 'getUserIdFromToken',
'isLoggedIn', 'getToken', 'signOut', 'decodedToken'
]);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AppComponent], imports: [AppComponent],
providers: [
provideRouter([]),
{provide: AuthService, useValue: authSpy},
{provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
]
}).compileComponents(); }).compileComponents();
}); });
@@ -19,11 +32,4 @@ describe('AppComponent', () => {
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app.title).toEqual('ClientApp'); expect(app.title).toEqual('ClientApp');
}); });
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, ClientApp');
});
}); });
@@ -1,22 +1,45 @@
import {TestBed} from '@angular/core/testing';
import {provideHttpClient} from '@angular/common/http';
import {AuthService} from './auth-service'; import {AuthService} from './auth-service';
import {TestBed} from "@angular/core/testing";
import {provideHttpClient} from "@angular/common/http";
import {ActivatedRoute} from "@angular/router";
import {of} from "rxjs";
describe('AuthService', () => { describe('AuthService', () => {
let service: AuthService; let service: AuthService;
beforeEach(() => { beforeEach(() => {
localStorage.clear();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [provideHttpClient(), providers: [provideHttpClient()]
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
]
}); });
service = TestBed.inject(AuthService); service = TestBed.inject(AuthService);
}) });
it('should create an instance', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should store and retrieve token', () => {
service.storeToken('test-token');
expect(service.getToken()).toBe('test-token');
});
it('should store and retrieve refresh token', () => {
service.storeRefreshToken('test-refresh');
expect(service.getRefreshToken()).toBe('test-refresh');
});
it('should be logged in after storing token', () => {
service.storeToken('some-token');
expect(service.isLoggedIn()).toBeTrue();
});
it('should not be logged in without token', () => {
expect(service.isLoggedIn()).toBeFalse();
});
it('should clear tokens on signOut', () => {
service.storeToken('test-token');
service.storeRefreshToken('test-refresh');
service.signOut();
expect(service.isLoggedIn()).toBeFalse();
});
}); });
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {DarkModeService} from './dark-mode.service'; import {DarkModeService} from './dark-mode.service';
describe('DarkModeService', () => { describe('DarkModeService', () => {
@@ -13,4 +12,16 @@ describe('DarkModeService', () => {
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should toggle from null to dark', () => {
service.darkModeSignal.set(null);
service.updateDarkMode();
expect(service.darkModeSignal()).toBe('dark');
});
it('should toggle from dark to null', () => {
service.darkModeSignal.set('dark');
service.updateDarkMode();
expect(service.darkModeSignal()).toBeNull();
});
}); });
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {LoadingService} from './loading.service'; import {LoadingService} from './loading.service';
describe('LoadingService', () => { describe('LoadingService', () => {
@@ -13,4 +12,26 @@ describe('LoadingService', () => {
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should start with loading false', () => {
expect(service.loading()).toBeFalse();
});
it('should set loading true on show()', () => {
service.show();
expect(service.loading()).toBeTrue();
});
it('should set loading false on hide()', () => {
service.show();
service.hide();
expect(service.loading()).toBeFalse();
});
it('should track per-item loading state', () => {
service.showItem('item-1');
expect(service.itemLoading()['item-1']).toBeTrue();
service.hideItem('item-1');
expect(service.itemLoading()['item-1']).toBeFalse();
});
}); });
@@ -1,23 +1,47 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {provideHttpClient} from '@angular/common/http';
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {ResetPasswordService} from './reset-password.service'; import {ResetPasswordService} from './reset-password.service';
import {provideHttpClient} from "@angular/common/http"; import {environment} from '../../../environments/environment.development';
import {ActivatedRoute} from "@angular/router";
import {of} from "rxjs";
describe('ResetPasswordService', () => { describe('ResetPasswordService', () => {
let service: ResetPasswordService; let service: ResetPasswordService;
let httpTesting: HttpTestingController;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } } provideHttpClient(),
provideHttpClientTesting(),
] ]
}); });
service = TestBed.inject(ResetPasswordService); service = TestBed.inject(ResetPasswordService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTesting.verify();
}); });
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should POST to send-reset-email on sendResetPasswordLink', () => {
service.sendResetPasswordLink('test@example.com').subscribe();
const req = httpTesting.expectOne(`${environment.baseUrl}auth/send-reset-email/test@example.com`);
expect(req.request.method).toBe('POST');
req.flush({value: 'Reset email sent successfully'});
});
it('should POST to reset-password on resetPassword', () => {
const resetObj = {email: 'test@example.com', emailToken: 'token', newPassword: 'NewPass1!', confirmPassword: 'NewPass1!'};
service.resetPassword(resetObj).subscribe();
const req = httpTesting.expectOne(`${environment.baseUrl}auth/reset-password`);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(resetObj);
req.flush({value: 'Password reset successfully'});
});
}); });
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {ToastService} from './toast.service'; import {ToastService} from './toast.service';
describe('ToastService', () => { describe('ToastService', () => {
@@ -13,4 +12,29 @@ describe('ToastService', () => {
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should start with empty toasts', () => {
expect(service.toasts.length).toBe(0);
});
it('should add a toast', () => {
service.show('Test message');
expect(service.toasts.length).toBe(1);
expect(service.toasts[0].message).toBe('Test message');
});
it('should add a toast with options', () => {
service.show('Warning', {classname: 'bg-warning', delay: 3000});
expect(service.toasts[0].classname).toBe('bg-warning');
expect(service.toasts[0].delay).toBe(3000);
});
it('should remove a toast', () => {
service.show('Message 1');
service.show('Message 2');
const toastToRemove = service.toasts[0];
service.remove(toastToRemove);
expect(service.toasts.length).toBe(1);
expect(service.toasts[0].message).toBe('Message 2');
});
}); });
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {UserStoreService} from './user-store.service'; import {UserStoreService} from './user-store.service';
describe('UserStoreService', () => { describe('UserStoreService', () => {
@@ -13,4 +12,35 @@ describe('UserStoreService', () => {
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should set and get username', (done) => {
service.setUsernameForStore('testuser');
service.getUsernameFromStore().subscribe(val => {
expect(val).toBe('testuser');
done();
});
});
it('should set and get role', (done) => {
service.setRoleForStore('Admin');
service.getRoleFromStore().subscribe(val => {
expect(val).toBe('Admin');
done();
});
});
it('should set and get email', (done) => {
service.setEmailForStore('test@example.com');
service.getEmailFromStore().subscribe(val => {
expect(val).toBe('test@example.com');
done();
});
});
it('should start with empty defaults', (done) => {
service.getUsernameFromStore().subscribe(val => {
expect(val).toBe('');
done();
});
});
}); });
@@ -1,23 +1,49 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {provideHttpClient} from '@angular/common/http';
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {UserService} from './user.service'; import {UserService} from './user.service';
import {provideHttpClient} from "@angular/common/http"; import {environment} from '../../../environments/environment.development';
import {ActivatedRoute} from "@angular/router";
import {of} from "rxjs";
describe('UserService', () => { describe('UserService', () => {
let service: UserService; let service: UserService;
let httpTesting: HttpTestingController;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } } provideHttpClient(),
provideHttpClientTesting(),
] ]
}); });
service = TestBed.inject(UserService); service = TestBed.inject(UserService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTesting.verify();
}); });
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should call GET with page params on getAllUsers', () => {
service.getAllUsers(1, 10).subscribe();
const req = httpTesting.expectOne(r =>
r.url === `${environment.baseUrl}user` &&
r.params.get('pageNumber') === '1' &&
r.params.get('pageSize') === '10'
);
expect(req.request.method).toBe('GET');
req.flush({value: {items: [], totalPages: 1}});
});
it('should call DELETE on deleteUser', () => {
service.deleteUser(5).subscribe();
const req = httpTesting.expectOne(`${environment.baseUrl}user/5`);
expect(req.request.method).toBe('DELETE');
req.flush({value: 'User deleted successfully'});
});
}); });
@@ -1,7 +1,47 @@
import {FormControl} from '@angular/forms';
import {PasswordValidator} from './password-validator'; import {PasswordValidator} from './password-validator';
describe('PasswordValidator', () => { describe('PasswordValidator', () => {
it('should create an instance', () => { it('should return null for a valid strong password', () => {
expect(new PasswordValidator()).toBeTruthy(); const control = new FormControl('ValidP@ss1');
const result = PasswordValidator.strong(control);
expect(result).toBeNull();
});
it('should return uppercase error when password has no uppercase', () => {
const control = new FormControl('lowercase1@');
const result = PasswordValidator.strong(control);
expect(result?.['uppercase']).toBeTrue();
});
it('should return lowercase error when password has no lowercase', () => {
const control = new FormControl('UPPERCASE1@');
const result = PasswordValidator.strong(control);
expect(result?.['lowercase']).toBeTrue();
});
it('should return number error when password has no digit', () => {
const control = new FormControl('NoDigit@aa');
const result = PasswordValidator.strong(control);
expect(result?.['number']).toBeTrue();
});
it('should return special error when password has no special character', () => {
const control = new FormControl('NoSpecialChar1');
const result = PasswordValidator.strong(control);
expect(result?.['special']).toBeTrue();
});
it('should return minlength error when password is too short', () => {
const control = new FormControl('Sh0rt!');
const result = PasswordValidator.strong(control);
expect(result?.['minlength']).toBeTrue();
});
it('should return multiple errors for a completely invalid password', () => {
const control = new FormControl('');
const result = PasswordValidator.strong(control);
expect(result).not.toBeNull();
expect(Object.keys(result!).length).toBeGreaterThan(1);
}); });
}); });
@@ -0,0 +1,23 @@
import {FormControl, FormGroup, ValidatorFn} from '@angular/forms';
import {ConfirmPasswordValidator} from './reset-password-validator';
describe('ConfirmPasswordValidator', () => {
const createForm = (password: string, confirm: string) => {
return new FormGroup({
password: new FormControl(password),
confirmPassword: new FormControl(confirm),
}, {validators: ConfirmPasswordValidator('password', 'confirmPassword') as ValidatorFn});
};
it('should set error when passwords do not match', () => {
const form = createForm('Pass123!', 'Different!');
form.updateValueAndValidity();
expect(form.get('confirmPassword')?.errors?.['ConfirmPasswordValidator']).toBeTrue();
});
it('should not set error when passwords match', () => {
const form = createForm('Pass123!', 'Pass123!');
form.updateValueAndValidity();
expect(form.get('confirmPassword')?.errors).toBeNull();
});
});
@@ -1,7 +1,30 @@
import {FormControl, FormGroup, Validators} from '@angular/forms';
import ValidateForm from './validate-form'; import ValidateForm from './validate-form';
describe('ValidateForm', () => { describe('ValidateForm', () => {
it('should create an instance', () => { it('should mark all controls as dirty', () => {
expect(new ValidateForm()).toBeTruthy(); const form = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', Validators.required),
});
ValidateForm.validateAllFormFields(form);
expect(form.get('name')?.dirty).toBeTrue();
expect(form.get('email')?.dirty).toBeTrue();
});
it('should recursively mark nested form groups', () => {
const form = new FormGroup({
user: new FormGroup({
name: new FormControl('', Validators.required),
}),
});
ValidateForm.validateAllFormFields(form);
const userGroup = form.get('user') as FormGroup;
expect(userGroup.get('name')?.dirty).toBeTrue();
});
it('should not throw for empty form group', () => {
const form = new FormGroup({});
expect(() => ValidateForm.validateAllFormFields(form)).not.toThrow();
}); });
}); });
@@ -1,24 +1,32 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ForgetPasswordPopupComponent} from './forget-password-popup.component'; import {ForgetPasswordPopupComponent} from './forget-password-popup.component';
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
import {provideHttpClient} from "@angular/common/http"; import {ResetPasswordService} from '../../../infrastructure/services/reset-password.service';
import {ActivatedRoute} from "@angular/router"; import {ToastService} from '../../../infrastructure/services/toast.service';
import {of} from "rxjs"; import {LoadingService} from '../../../infrastructure/services/loading.service';
import {of, throwError} from 'rxjs';
describe('ForgetPasswordPopupComponent', () => { describe('ForgetPasswordPopupComponent', () => {
let component: ForgetPasswordPopupComponent; let component: ForgetPasswordPopupComponent;
let fixture: ComponentFixture<ForgetPasswordPopupComponent>; let fixture: ComponentFixture<ForgetPasswordPopupComponent>;
let resetService: jasmine.SpyObj<ResetPasswordService>;
let toastService: jasmine.SpyObj<ToastService>;
let activeModal: jasmine.SpyObj<NgbActiveModal>;
beforeEach(async () => { beforeEach(async () => {
resetService = jasmine.createSpyObj('ResetPasswordService', ['sendResetPasswordLink']);
toastService = jasmine.createSpyObj('ToastService', ['show']);
activeModal = jasmine.createSpyObj('NgbActiveModal', ['close']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ForgetPasswordPopupComponent], imports: [ForgetPasswordPopupComponent],
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }, {provide: ResetPasswordService, useValue: resetService},
NgbActiveModal {provide: ToastService, useValue: toastService},
{provide: NgbActiveModal, useValue: activeModal},
LoadingService,
] ]
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ForgetPasswordPopupComponent); fixture = TestBed.createComponent(ForgetPasswordPopupComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -28,4 +36,39 @@ describe('ForgetPasswordPopupComponent', () => {
it('should create ForgetPasswordPopupComponent', () => { it('should create ForgetPasswordPopupComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should validate email format', () => {
expect(component.checkValidEmail('invalid')).toBeFalse();
expect(component.checkValidEmail('test@example.com')).toBeTrue();
expect(component.isValidEmail).toBeTrue();
});
it('should call sendResetPasswordLink on confirm with valid email', () => {
resetService.sendResetPasswordLink.and.returnValue(of({value: 'Email sent'}));
component.resetPasswordEmail = 'test@example.com';
component.isValidEmail = true;
component.confirmToSend();
expect(resetService.sendResetPasswordLink).toHaveBeenCalledWith('test@example.com');
});
it('should not call API when email is invalid', () => {
component.resetPasswordEmail = 'invalid-email';
component.confirmToSend();
expect(resetService.sendResetPasswordLink).not.toHaveBeenCalled();
});
it('should show error toast on failure', () => {
resetService.sendResetPasswordLink.and.returnValue(throwError(() => ({
error: {error: {message: 'User not found'}}
})));
component.resetPasswordEmail = 'test@example.com';
component.isValidEmail = true;
component.confirmToSend();
expect(toastService.show).toHaveBeenCalledWith('User not found', jasmine.any(Object));
});
}); });
@@ -1,23 +1,38 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component'; import {LoginComponent} from './login.component';
import {ActivatedRoute, provideRouter, Router} from "@angular/router"; import {AuthService} from '../../../infrastructure/services/auth-service';
import {Title} from "@angular/platform-browser"; import {ToastService} from '../../../infrastructure/services/toast.service';
import {of} from "rxjs"; import {UserStoreService} from '../../../infrastructure/services/user-store.service';
import {provideHttpClient} from "@angular/common/http"; import {LoadingService} from '../../../infrastructure/services/loading.service';
import {ActivatedRoute, Router} from '@angular/router';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {of, throwError} from 'rxjs';
describe('LoginComponent', () => { describe('LoginComponent', () => {
let component: LoginComponent; let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginComponent>;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
let toastService: jasmine.SpyObj<ToastService>;
beforeEach(async () => { beforeEach(async () => {
authService = jasmine.createSpyObj('AuthService', ['login', 'storeToken', 'storeRefreshToken', 'decodedToken']);
router = jasmine.createSpyObj('Router', ['navigate']);
toastService = jasmine.createSpyObj('ToastService', ['show']);
const modalService = jasmine.createSpyObj('NgbModal', ['open']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LoginComponent], imports: [LoginComponent],
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } } {provide: AuthService, useValue: authService},
{provide: Router, useValue: router},
{provide: ToastService, useValue: toastService},
{provide: NgbModal, useValue: modalService},
{provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
UserStoreService,
LoadingService,
] ]
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(LoginComponent); fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -27,4 +42,63 @@ describe('LoginComponent', () => {
it('should create LoginComponent', () => { it('should create LoginComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should have an invalid form when fields are empty', () => {
expect(component.loginForm.valid).toBeFalse();
});
it('should validate email pattern', () => {
const emailControl = component.loginForm.get('email');
emailControl?.setValue('not-an-email');
expect(emailControl?.valid).toBeFalse();
emailControl?.setValue('test@example.com');
expect(emailControl?.valid).toBeTrue();
});
it('should require password', () => {
const passwordControl = component.loginForm.get('password');
expect(passwordControl?.valid).toBeFalse();
passwordControl?.setValue('somepassword');
expect(passwordControl?.valid).toBeTrue();
});
it('should call login on submit when form is valid', () => {
const tokenPayload = {username: 'testuser', email: 'test@example.com', role: 'User'};
authService.login.and.returnValue(of({
value: {token: {accessToken: 'access', refreshToken: 'refresh'}}
}));
authService.decodedToken.and.returnValue(tokenPayload);
router.navigate.and.returnValue(Promise.resolve(true));
component.loginForm.setValue({
email: 'test@example.com',
password: 'Password1!',
});
component.onLogin();
expect(authService.login).toHaveBeenCalled();
expect(authService.storeToken).toHaveBeenCalledWith('access');
expect(authService.storeRefreshToken).toHaveBeenCalledWith('refresh');
});
it('should show error toast on login failure', () => {
authService.login.and.returnValue(throwError(() => ({
error: {error: {message: 'Invalid credentials'}}
})));
component.loginForm.setValue({
email: 'test@example.com',
password: 'wrong',
});
component.onLogin();
expect(toastService.show).toHaveBeenCalledWith('Invalid credentials', jasmine.any(Object));
});
it('should toggle password visibility', () => {
component.hideShowPassword();
expect(component.isText).toBeFalse();
component.hideShowPassword();
expect(component.isText).toBeTrue();
});
}); });
@@ -1,19 +1,31 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {RegisterComponent} from './register.component'; import {RegisterComponent} from './register.component';
import {HttpClient, provideHttpClient} from "@angular/common/http"; import {AuthService} from '../../../infrastructure/services/auth-service';
import {ActivatedRoute} from "@angular/router"; import {ToastService} from '../../../infrastructure/services/toast.service';
import {of} from "rxjs"; import {LoadingService} from '../../../infrastructure/services/loading.service';
import {ActivatedRoute, Router} from '@angular/router';
import {of, throwError} from 'rxjs';
describe('RegisterComponent', () => { describe('RegisterComponent', () => {
let component: RegisterComponent; let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>; let fixture: ComponentFixture<RegisterComponent>;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
let toastService: jasmine.SpyObj<ToastService>;
beforeEach(async () => { beforeEach(async () => {
authService = jasmine.createSpyObj('AuthService', ['register']);
router = jasmine.createSpyObj('Router', ['navigate']);
toastService = jasmine.createSpyObj('ToastService', ['show']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RegisterComponent], imports: [RegisterComponent],
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } } {provide: AuthService, useValue: authService},
{provide: Router, useValue: router},
{provide: ToastService, useValue: toastService},
{provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
LoadingService,
] ]
}).compileComponents(); }).compileComponents();
@@ -25,4 +37,68 @@ describe('RegisterComponent', () => {
it('should create RegisterComponent', () => { it('should create RegisterComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should have an invalid form when fields are empty', () => {
expect(component.registerForm.valid).toBeFalse();
});
it('should validate username pattern', () => {
const usernameControl = component.registerForm.get('username');
usernameControl?.setValue('invalid username!');
expect(usernameControl?.valid).toBeFalse();
usernameControl?.setValue('valid_user');
expect(usernameControl?.valid).toBeTrue();
});
it('should validate email pattern', () => {
const emailControl = component.registerForm.get('email');
emailControl?.setValue('invalid');
expect(emailControl?.valid).toBeFalse();
emailControl?.setValue('test@example.com');
expect(emailControl?.valid).toBeTrue();
});
it('should validate password strength', () => {
const passwordControl = component.registerForm.get('password');
passwordControl?.setValue('weak');
expect(passwordControl?.valid).toBeFalse();
passwordControl?.setValue('StrongP@ss1');
expect(passwordControl?.valid).toBeTrue();
});
it('should call register on submit when form is valid', () => {
authService.register.and.returnValue(of({isSuccess: true, value: 'Registered'}));
router.navigate.and.returnValue(Promise.resolve(true));
component.registerForm.setValue({
username: 'testuser',
email: 'test@example.com',
password: 'StrongP@ss1',
});
component.onSignUp();
expect(authService.register).toHaveBeenCalled();
});
it('should show error toast on registration failure', () => {
authService.register.and.returnValue(throwError(() => ({
error: {error: {message: 'Email already exists'}}
})));
component.registerForm.setValue({
username: 'testuser',
email: 'existing@example.com',
password: 'StrongP@ss1',
});
component.onSignUp();
expect(toastService.show).toHaveBeenCalledWith('Email already exists', jasmine.any(Object));
});
it('should toggle password visibility', () => {
component.hideShowPassword();
expect(component.isText).toBeFalse();
component.hideShowPassword();
expect(component.isText).toBeTrue();
});
}); });
@@ -1,32 +1,36 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ResetPasswordComponent} from './reset-password.component'; import {ResetPasswordComponent} from './reset-password.component';
import {provideHttpClient} from "@angular/common/http"; import {ResetPasswordService} from '../../../infrastructure/services/reset-password.service';
import {ActivatedRoute} from "@angular/router"; import {ToastService} from '../../../infrastructure/services/toast.service';
import {of} from "rxjs"; import {ActivatedRoute, Router} from '@angular/router';
import {ReactiveFormsModule} from "@angular/forms"; import {of, throwError} from 'rxjs';
describe('ResetPasswordComponent', () => { describe('ResetPasswordComponent', () => {
let component: ResetPasswordComponent; let component: ResetPasswordComponent;
let fixture: ComponentFixture<ResetPasswordComponent>; let fixture: ComponentFixture<ResetPasswordComponent>;
let resetService: jasmine.SpyObj<ResetPasswordService>;
let router: jasmine.SpyObj<Router>;
let toastService: jasmine.SpyObj<ToastService>;
beforeEach(async () => { beforeEach(async () => {
resetService = jasmine.createSpyObj('ResetPasswordService', ['resetPassword']);
router = jasmine.createSpyObj('Router', ['navigate']);
toastService = jasmine.createSpyObj('ToastService', ['show']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ResetPasswordComponent, ReactiveFormsModule], imports: [ResetPasswordComponent],
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }, {provide: ResetPasswordService, useValue: resetService},
{provide: Router, useValue: router},
{provide: ToastService, useValue: toastService},
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { useValue: {
queryParams: of({ queryParams: of({email: 'test@example.com', code: 'test-token'})
email: 'test@example.com',
code: 'test-token'
})
} }
} }
] ]
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ResetPasswordComponent); fixture = TestBed.createComponent(ResetPasswordComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -36,4 +40,53 @@ describe('ResetPasswordComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should read email and token from query params', () => {
expect(component.emailToReset).toBe('test@example.com');
expect(component.emailToken).toBe('test-token');
});
it('should have an invalid form when fields are empty', () => {
expect(component.resetForm.valid).toBeFalse();
});
it('should validate password strength', () => {
const passwordControl = component.resetForm.get('password');
passwordControl?.setValue('weak');
expect(passwordControl?.valid).toBeFalse();
passwordControl?.setValue('StrongP@ss1');
expect(passwordControl?.valid).toBeTrue();
});
it('should validate password confirmation match', () => {
component.resetForm.get('password')?.setValue('StrongP@ss1');
component.resetForm.get('confirmPassword')?.setValue('Different!');
expect(component.resetForm.valid).toBeFalse();
component.resetForm.get('confirmPassword')?.setValue('StrongP@ss1');
expect(component.resetForm.valid).toBeTrue();
});
it('should call resetPassword on submit when form is valid', () => {
resetService.resetPassword.and.returnValue(of({value: 'Password reset'}));
router.navigate.and.returnValue(Promise.resolve(true));
component.resetForm.get('password')?.setValue('NewStrongP@ss1');
component.resetForm.get('confirmPassword')?.setValue('NewStrongP@ss1');
component.reset();
expect(resetService.resetPassword).toHaveBeenCalled();
});
it('should show error alert on failure', () => {
spyOn(window, 'alert');
resetService.resetPassword.and.returnValue(throwError(() => ({
error: {error: {message: 'Invalid token'}}
})));
component.resetForm.get('password')?.setValue('NewStrongP@ss1');
component.resetForm.get('confirmPassword')?.setValue('NewStrongP@ss1');
component.reset();
expect(window.alert).toHaveBeenCalledWith('Invalid token');
});
}); });
@@ -1,6 +1,6 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AdminDashboardComponent} from './admin-dashboard.component'; import {AdminDashboardComponent} from './admin-dashboard.component';
import {ActivatedRoute} from '@angular/router';
describe('AdminDashboardComponent', () => { describe('AdminDashboardComponent', () => {
let component: AdminDashboardComponent; let component: AdminDashboardComponent;
@@ -8,9 +8,11 @@ describe('AdminDashboardComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AdminDashboardComponent] imports: [AdminDashboardComponent],
}) providers: [
.compileComponents(); {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
]
}).compileComponents();
fixture = TestBed.createComponent(AdminDashboardComponent); fixture = TestBed.createComponent(AdminDashboardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -1,9 +1,6 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FooterComponent} from './footer.component'; import {FooterComponent} from './footer.component';
import {provideHttpClient} from "@angular/common/http"; import {ActivatedRoute} from '@angular/router';
import {ActivatedRoute} from "@angular/router";
import {of} from "rxjs";
describe('FooterComponent', () => { describe('FooterComponent', () => {
let component: FooterComponent; let component: FooterComponent;
@@ -12,11 +9,10 @@ describe('FooterComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FooterComponent], imports: [FooterComponent],
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } } {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
] ]
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(FooterComponent); fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -26,4 +22,8 @@ describe('FooterComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should have a version', () => {
expect(component.version).toBeDefined();
});
}); });
@@ -1,22 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component'; import {HeaderComponent} from './header.component';
import {provideHttpClient} from "@angular/common/http"; import {AuthService} from '../../../infrastructure/services/auth-service';
import {ActivatedRoute} from "@angular/router"; import {ActivatedRoute} from '@angular/router';
import {of} from "rxjs";
describe('NavbarComponent', () => { describe('HeaderComponent', () => {
let component: HeaderComponent; let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>; let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => { beforeEach(async () => {
const authSpy = jasmine.createSpyObj('AuthService', [
'getUsernameFromToken', 'getRoleFromToken', 'getEmailFromToken', 'getUserIdFromToken',
'isLoggedIn', 'getToken', 'signOut', 'decodedToken'
]);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [HeaderComponent], imports: [HeaderComponent],
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } } {provide: AuthService, useValue: authSpy},
{provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
] ]
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(HeaderComponent); fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -1,16 +1,24 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {UserDashboardComponent} from './user-dashboard.component'; import {UserDashboardComponent} from './user-dashboard.component';
import {AuthService} from '../../../infrastructure/services/auth-service';
import {UserStoreService} from '../../../infrastructure/services/user-store.service';
import {of} from 'rxjs';
describe('UserDashboardComponent', () => { describe('UserDashboardComponent', () => {
let component: UserDashboardComponent; let component: UserDashboardComponent;
let fixture: ComponentFixture<UserDashboardComponent>; let fixture: ComponentFixture<UserDashboardComponent>;
beforeEach(async () => { beforeEach(async () => {
const authSpy = jasmine.createSpyObj('AuthService', [
'getUsernameFromToken', 'getEmailFromToken'
]);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserDashboardComponent] imports: [UserDashboardComponent],
}) providers: [
.compileComponents(); {provide: AuthService, useValue: authSpy},
]
}).compileComponents();
fixture = TestBed.createComponent(UserDashboardComponent); fixture = TestBed.createComponent(UserDashboardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -1,16 +1,38 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {UserTableComponent} from './user-table.component'; import {UserTableComponent} from './user-table.component';
import {UserService} from '../../../infrastructure/services/user.service';
import {ToastService} from '../../../infrastructure/services/toast.service';
import {of, throwError} from 'rxjs';
describe('UserTableComponent', () => { describe('UserTableComponent', () => {
let component: UserTableComponent; let component: UserTableComponent;
let fixture: ComponentFixture<UserTableComponent>; let fixture: ComponentFixture<UserTableComponent>;
let userService: jasmine.SpyObj<UserService>;
let toastService: jasmine.SpyObj<ToastService>;
const mockUsers = {
value: {
items: [
{id: 1, username: 'user1', email: 'user1@test.com', lastLogin: new Date(), UserRoles: [{role: {name: 'Admin'}}]},
{id: 2, username: 'user2', email: 'user2@test.com', lastLogin: new Date(), UserRoles: [{role: {name: 'User'}}]},
],
totalPages: 3,
}
};
beforeEach(async () => { beforeEach(async () => {
userService = jasmine.createSpyObj('UserService', ['getAllUsers', 'deleteUser']);
toastService = jasmine.createSpyObj('ToastService', ['show']);
userService.getAllUsers.and.returnValue(of(mockUsers));
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserTableComponent] imports: [UserTableComponent],
}) providers: [
.compileComponents(); {provide: UserService, useValue: userService},
{provide: ToastService, useValue: toastService},
]
}).compileComponents();
fixture = TestBed.createComponent(UserTableComponent); fixture = TestBed.createComponent(UserTableComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -20,4 +42,111 @@ describe('UserTableComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should load users on init', () => {
expect(userService.getAllUsers).toHaveBeenCalledWith(1, 10);
expect(component.users.length).toBe(2);
expect(component.totalPages).toBe(3);
});
it('should go to next page', () => {
component.pageNumber = 1;
component.totalPages = 3;
component.nextPage();
expect(component.pageNumber).toBe(2);
expect(userService.getAllUsers).toHaveBeenCalledWith(2, 10);
});
it('should not go beyond last page', () => {
component.pageNumber = 3;
component.totalPages = 3;
component.nextPage();
expect(component.pageNumber).toBe(3);
});
it('should go to previous page', () => {
component.pageNumber = 2;
component.previousPage();
expect(component.pageNumber).toBe(1);
expect(userService.getAllUsers).toHaveBeenCalledWith(1, 10);
});
it('should not go below first page', () => {
component.pageNumber = 1;
component.previousPage();
expect(component.pageNumber).toBe(1);
});
it('should go to specific page', () => {
component.goToPage(2);
expect(component.pageNumber).toBe(2);
expect(userService.getAllUsers).toHaveBeenCalledWith(2, 10);
});
it('should not go to invalid page', () => {
component.goToPage(0);
expect(component.pageNumber).toBe(1);
component.goToPage(999);
expect(component.pageNumber).toBe(1);
});
it('should sort users by column ascending', () => {
component.users = [
{id: 2, username: 'b_user'} as any,
{id: 1, username: 'a_user'} as any,
];
component.sort('username');
expect(component.users[0].username).toBe('a_user');
expect(component.users[1].username).toBe('b_user');
expect(component.sortDirection).toBe('asc');
});
it('should toggle sort direction on same column', () => {
component.users = [
{id: 1, username: 'a_user'} as any,
{id: 2, username: 'b_user'} as any,
];
component.sort('username');
component.sort('username');
expect(component.users[0].username).toBe('b_user');
expect(component.users[1].username).toBe('a_user');
expect(component.sortDirection).toBe('desc');
});
it('should delete user and remove from list', () => {
userService.deleteUser.and.returnValue(of({value: 'User deleted successfully'}));
component.users = [
{id: 1, username: 'user1'} as any,
{id: 2, username: 'user2'} as any,
];
component.deleteUser(1);
expect(userService.deleteUser).toHaveBeenCalledWith(1);
expect(component.users.length).toBe(1);
expect(component.users[0].id).toBe(2);
expect(toastService.show).toHaveBeenCalledWith('User deleted successfully', jasmine.any(Object));
});
it('should show error toast on delete failure', () => {
userService.deleteUser.and.returnValue(throwError(() => ({
error: {error: {message: 'User not found'}}
})));
component.deleteUser(99);
expect(toastService.show).toHaveBeenCalledWith('User not found', jasmine.any(Object));
});
it('should select user and handle confirm deletion', () => {
const user = {id: 1, username: 'user1'} as any;
userService.deleteUser.and.returnValue(of({value: 'Deleted'}));
component.users = [user];
component.selectUser(user);
expect(component.selectedUser).toBe(user);
component.handleConfirm();
expect(userService.deleteUser).toHaveBeenCalledWith(1);
});
}); });
@@ -1,17 +1,62 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {CanActivateFn} from '@angular/router'; import {Router} from '@angular/router';
import {adminGuard} from './admin.guard'; import {adminGuard} from './admin.guard';
import {AuthService} from '../../infrastructure/services/auth-service';
import {ToastService} from '../../infrastructure/services/toast.service';
import {UserStoreService} from '../../infrastructure/services/user-store.service';
describe('adminGuard', () => { describe('adminGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) => let authService: jasmine.SpyObj<AuthService>;
TestBed.runInInjectionContext(() => adminGuard(...guardParameters)); let router: jasmine.SpyObj<Router>;
let toastService: jasmine.SpyObj<ToastService>;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); authService = jasmine.createSpyObj('AuthService', ['getRoleFromToken']);
router = jasmine.createSpyObj('Router', ['navigate']);
toastService = jasmine.createSpyObj('ToastService', ['show']);
TestBed.configureTestingModule({
providers: [
UserStoreService,
{provide: AuthService, useValue: authService},
{provide: Router, useValue: router},
{provide: ToastService, useValue: toastService},
]
}); });
});
const executeGuard = () =>
TestBed.runInInjectionContext(() => adminGuard({} as any, {} as any));
it('should be created', () => { it('should be created', () => {
expect(executeGuard).toBeTruthy(); expect(executeGuard).toBeTruthy();
}); });
it('should return true for Admin role', (done) => {
const userStore = TestBed.inject(UserStoreService);
userStore.setRoleForStore('Admin');
(executeGuard() as any).subscribe((result: boolean) => {
expect(result).toBeTrue();
done();
});
});
it('should return true for SuperAdmin role', (done) => {
const userStore = TestBed.inject(UserStoreService);
userStore.setRoleForStore('SuperAdmin');
(executeGuard() as any).subscribe((result: boolean) => {
expect(result).toBeTrue();
done();
});
});
it('should return false and redirect for non-admin role', (done) => {
const userStore = TestBed.inject(UserStoreService);
userStore.setRoleForStore('User');
(executeGuard() as any).subscribe((result: boolean) => {
expect(result).toBeFalse();
expect(router.navigate).toHaveBeenCalledWith(['/unauthorized']);
done();
});
});
}); });
@@ -1,24 +1,45 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {Router} from '@angular/router';
import {AuthenticationGuard} from './authentication.guard'; import {AuthenticationGuard} from './authentication.guard';
import {provideHttpClient} from "@angular/common/http"; import {AuthService} from '../../infrastructure/services/auth-service';
import {ActivatedRoute} from "@angular/router"; import {ToastService} from '../../infrastructure/services/toast.service';
import {of} from "rxjs";
describe('AuthenticationGuard', () => { describe('AuthenticationGuard', () => {
let guard: AuthenticationGuard; let guard: AuthenticationGuard;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
beforeEach(() => { beforeEach(() => {
const authSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
routerSpy.navigate.and.returnValue(Promise.resolve(true));
const toastSpy = jasmine.createSpyObj('ToastService', ['show']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [provideHttpClient(), providers: [
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }, AuthenticationGuard,
{provide: AuthService, useValue: authSpy},
{provide: Router, useValue: routerSpy},
{provide: ToastService, useValue: toastSpy},
] ]
}); });
guard = TestBed.inject(AuthenticationGuard); guard = TestBed.inject(AuthenticationGuard);
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
}); });
it('should be created', () => { it('should be created', () => {
expect(guard).toBeTruthy(); expect(guard).toBeTruthy();
}); });
it('should return true when user is logged in', () => {
authService.isLoggedIn.and.returnValue(true);
expect(guard.canActivate()).toBeTrue();
});
it('should return false and redirect when user is not logged in', () => {
authService.isLoggedIn.and.returnValue(false);
expect(guard.canActivate()).toBeFalse();
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
}); });
@@ -1,17 +1,39 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import { CanActivateFn } from '@angular/router'; import {Router} from '@angular/router';
import {guestGuard} from './guest.guard'; import {guestGuard} from './guest.guard';
import {AuthService} from '../../infrastructure/services/auth-service';
describe('guestGuard', () => { describe('guestGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) => let authService: jasmine.SpyObj<AuthService>;
TestBed.runInInjectionContext(() => guestGuard(...guardParameters)); let router: jasmine.SpyObj<Router>;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); authService = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
router = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
providers: [
{provide: AuthService, useValue: authService},
{provide: Router, useValue: router},
]
}); });
});
const executeGuard = () =>
TestBed.runInInjectionContext(() => guestGuard({} as any, {} as any));
it('should be created', () => { it('should be created', () => {
expect(executeGuard).toBeTruthy(); expect(executeGuard).toBeTruthy();
}); });
it('should return true when user is not logged in', () => {
authService.isLoggedIn.and.returnValue(false);
expect(executeGuard()).toBeTrue();
});
it('should return false and redirect when user is logged in', () => {
authService.isLoggedIn.and.returnValue(true);
expect(executeGuard()).toBeFalse();
expect(router.navigate).toHaveBeenCalledWith(['/user-dashboard']);
});
}); });
@@ -1,17 +1,50 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {HttpInterceptorFn} from '@angular/common/http'; import {HttpClient, HttpInterceptorFn, provideHttpClient, withInterceptors} from '@angular/common/http';
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {tokenInterceptor} from './token.interceptor'; import {tokenInterceptor} from './token.interceptor';
import {AuthService} from '../../infrastructure/services/auth-service';
describe('tokenInterceptor', () => { describe('tokenInterceptor', () => {
const interceptor: HttpInterceptorFn = (req, next) => let authService: jasmine.SpyObj<AuthService>;
TestBed.runInInjectionContext(() => tokenInterceptor(req, next)); let httpClient: HttpClient;
let httpTesting: HttpTestingController;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); authService = jasmine.createSpyObj('AuthService', ['getToken', 'getRefreshToken', 'getUserIdFromToken',
'storeToken', 'storeRefreshToken', 'signOut', 'renewToken']);
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([tokenInterceptor])),
provideHttpClientTesting(),
{provide: AuthService, useValue: authService},
]
});
httpClient = TestBed.inject(HttpClient);
httpTesting = TestBed.inject(HttpTestingController);
}); });
it('should be created', () => { afterEach(() => {
expect(interceptor).toBeTruthy(); httpTesting.verify();
});
it('should add Authorization header when token exists', () => {
authService.getToken.and.returnValue('test-jwt-token');
httpClient.get('/api/test').subscribe();
const req = httpTesting.expectOne('/api/test');
expect(req.request.headers.get('Authorization')).toBe('Bearer test-jwt-token');
req.flush({});
});
it('should not add Authorization header when no token', () => {
authService.getToken.and.returnValue(null);
httpClient.get('/api/test').subscribe();
const req = httpTesting.expectOne('/api/test');
expect(req.request.headers.has('Authorization')).toBeFalse();
req.flush({});
}); });
}); });
@@ -1,9 +0,0 @@
namespace Application.FunctionalTest;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
@@ -18,10 +18,17 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="xunit.v3" Version="3.2.2" /> <PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="Xunit"/> <Using Include="Xunit"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Application\Application.csproj" />
<ProjectReference Include="..\..\src\Domain\Domain.csproj" />
<ProjectReference Include="..\..\src\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,285 @@
using Application.Common.Results;
using Application.DTOs;
using Application.Errors;
using Application.Interfaces;
using Application.Models;
using Application.Services;
using Application.Validators;
using Domain.Entities;
using Domain.Interface;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
namespace Application.UnitTest.Services;
public class AuthenticationServiceTests
{
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
private readonly IJwtService _jwtService = Substitute.For<IJwtService>();
private readonly IEmailService _emailService = Substitute.For<IEmailService>();
private readonly AuthenticationService _sut;
public AuthenticationServiceTests()
{
_sut = new AuthenticationService(
_unitOfWork,
_userRepository,
new LoginRequestValidator(),
new RegisterRequestValidator(),
_jwtService,
_emailService);
}
[Fact]
public async Task RegisterAsync_ShouldReturnSuccess_WhenRequestIsValid()
{
var request = new RegisterRequest("newuser", "new@example.com", "ValidP@ss1!");
_userRepository.GetUserByEmailAsync(request.Email).Returns((User?)null);
_userRepository.GetUserByUsernameAsync(request.Username).Returns((User?)null);
var result = await _sut.RegisterAsync(request);
Assert.True(result.IsSuccess);
await _userRepository.Received(1).AddAsync(Arg.Any<User>());
await _unitOfWork.Received(1).CommitAsync();
}
[Fact]
public async Task RegisterAsync_ShouldReturnFailure_WhenValidationFails()
{
var request = new RegisterRequest("", "", "");
var result = await _sut.RegisterAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(ErrorTypeConstant.ValidationError, result.Error.Code);
}
[Fact]
public async Task RegisterAsync_ShouldReturnFailure_WhenEmailAlreadyExists()
{
var request = new RegisterRequest("newuser", "existing@example.com", "ValidP@ss1!");
_userRepository.GetUserByEmailAsync(request.Email).Returns(new User
{
Username = "existing", Email = "existing@example.com", Password = "hash"
});
var result = await _sut.RegisterAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.EmailAlreadyExists, result.Error);
}
[Fact]
public async Task RegisterAsync_ShouldReturnFailure_WhenUsernameAlreadyExists()
{
var request = new RegisterRequest("existinguser", "new@example.com", "ValidP@ss1!");
_userRepository.GetUserByEmailAsync(request.Email).Returns((User?)null);
_userRepository.GetUserByUsernameAsync(request.Username).Returns(new User
{
Username = "existinguser", Email = "existing@example.com", Password = "hash"
});
var result = await _sut.RegisterAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.UsernameAlreadyExists, result.Error);
}
[Fact]
public async Task LoginAsync_ShouldReturnSuccess_WhenCredentialsAreValid()
{
var password = "ValidP@ss1!";
var user = new User
{
Id = 1,
Username = "testuser",
Email = "test@example.com",
Password = new PasswordHasher<User>().HashPassword(
new User { Username = "testuser", Email = "test@example.com", Password = password },
password)
};
var request = new LoginRequest(user.Email, password);
_userRepository.GetUserByEmailAsync(request.Email).Returns(user);
_jwtService.GenerateTokenAsync(user).Returns("access-token");
_jwtService.GenerateAndSaveRefreshTokenAsync(user).Returns("refresh-token");
var result = await _sut.LoginAsync(request);
Assert.True(result.IsSuccess);
_userRepository.Received(1).Update(user);
}
[Fact]
public async Task LoginAsync_ShouldReturnFailure_WhenValidationFails()
{
var request = new LoginRequest("", "");
var result = await _sut.LoginAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(ErrorTypeConstant.ValidationError, result.Error.Code);
}
[Fact]
public async Task LoginAsync_ShouldReturnFailure_WhenUserNotFound()
{
var request = new LoginRequest("unknown@example.com", "password123");
_userRepository.GetUserByEmailAsync(request.Email).Returns((User?)null);
var result = await _sut.LoginAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.UserNotFound, result.Error);
}
[Fact]
public async Task RefreshTokensAsync_ShouldReturnSuccess_WhenRefreshTokenIsValid()
{
var request = new RefreshTokenRequest { UserId = 1, RefreshToken = "valid-refresh-token" };
var user = new User { Id = 1, Username = "testuser", Email = "test@example.com", Password = "hash" };
_jwtService.ValidateRefreshTokenAsync(request.UserId, request.RefreshToken).Returns(user);
_jwtService.GenerateTokenAsync(user).Returns("new-access-token");
_jwtService.GenerateAndSaveRefreshTokenAsync(user).Returns("new-refresh-token");
var result = await _sut.RefreshTokensAsync(request);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task RefreshTokensAsync_ShouldReturnFailure_WhenUserNotFound()
{
var request = new RefreshTokenRequest { UserId = 99, RefreshToken = "invalid-token" };
_jwtService.ValidateRefreshTokenAsync(request.UserId, request.RefreshToken).Returns((User?)null);
var result = await _sut.RefreshTokensAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.UserNotFound, result.Error);
}
[Fact]
public async Task SendResetEmailAsync_ShouldReturnSuccess_WhenUserExists()
{
var email = "test@example.com";
var user = new User { Id = 1, Username = "testuser", Email = email, Password = "hash" };
_userRepository.GetUserByEmailAsync(email).Returns(user);
var result = await _sut.SendResetEmailAsync(email);
Assert.True(result.IsSuccess);
_emailService.Received(1).SendEmailAsync(Arg.Any<EmailRequest>());
_userRepository.Received(1).Update(user);
await _unitOfWork.Received(1).CommitAsync();
}
[Fact]
public async Task SendResetEmailAsync_ShouldReturnFailure_WhenUserNotFound()
{
var email = "unknown@example.com";
_userRepository.GetUserByEmailAsync(email).Returns((User?)null);
var result = await _sut.SendResetEmailAsync(email);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.UserNotFound, result.Error);
}
[Fact]
public async Task ResetPasswordAsync_ShouldReturnSuccess_WhenTokenIsValid()
{
var dto = new ResetPasswordDto
{
Email = "test@example.com",
EmailToken = "valid-token",
NewPassword = "NewValidP@ss1"
};
var user = new User
{
Id = 1,
Username = "testuser",
Email = dto.Email,
Password = "old-hash",
ResetPasswordToken = "valid-token",
ResetPasswordTokenExpiryTime = DateTime.UtcNow.AddHours(1)
};
_userRepository.GetUserByEmailAsync(dto.Email).Returns(user);
var result = await _sut.ResetPasswordAsync(dto);
Assert.True(result.IsSuccess);
_userRepository.Received(1).Update(user);
await _unitOfWork.Received(1).CommitAsync();
}
[Fact]
public async Task ResetPasswordAsync_ShouldReturnFailure_WhenUserNotFound()
{
var dto = new ResetPasswordDto
{
Email = "unknown@example.com",
EmailToken = "token",
NewPassword = "NewValidP@ss1"
};
_userRepository.GetUserByEmailAsync(dto.Email).Returns((User?)null);
var result = await _sut.ResetPasswordAsync(dto);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.UserNotFound, result.Error);
}
[Fact]
public async Task ResetPasswordAsync_ShouldReturnFailure_WhenTokenIsInvalid()
{
var dto = new ResetPasswordDto
{
Email = "test@example.com",
EmailToken = "wrong-token",
NewPassword = "NewValidP@ss1"
};
var user = new User
{
Id = 1,
Username = "testuser",
Email = dto.Email,
Password = "old-hash",
ResetPasswordToken = "valid-token",
ResetPasswordTokenExpiryTime = DateTime.UtcNow.AddHours(1)
};
_userRepository.GetUserByEmailAsync(dto.Email).Returns(user);
var result = await _sut.ResetPasswordAsync(dto);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.InvalidResetLink, result.Error);
}
[Fact]
public async Task ResetPasswordAsync_ShouldReturnFailure_WhenTokenIsExpired()
{
var dto = new ResetPasswordDto
{
Email = "test@example.com",
EmailToken = "valid-token",
NewPassword = "NewValidP@ss1"
};
var user = new User
{
Id = 1,
Username = "testuser",
Email = dto.Email,
Password = "old-hash",
ResetPasswordToken = "valid-token",
ResetPasswordTokenExpiryTime = DateTime.UtcNow.AddHours(-1)
};
_userRepository.GetUserByEmailAsync(dto.Email).Returns(user);
var result = await _sut.ResetPasswordAsync(dto);
Assert.True(result.IsFailure);
Assert.Equal(AuthError.InvalidResetLink, result.Error);
}
}
@@ -0,0 +1,222 @@
using Application.Common.Results;
using Application.DTOs;
using Application.Errors;
using Application.Models;
using Application.Services;
using Application.Validators;
using Domain.Entities;
using Domain.Interface;
using NSubstitute;
namespace Application.UnitTest.Services;
public class UserServiceTests
{
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
private readonly IUserRoleRepository _userRoleRepository = Substitute.For<IUserRoleRepository>();
private readonly UserService _sut;
public UserServiceTests()
{
_sut = new UserService(
_unitOfWork,
new UserUpdateRequestValidator(),
_userRepository,
_userRoleRepository);
}
[Fact]
public async Task GetAsync_ShouldReturnPagedResults()
{
var users = new List<User>
{
new() { Id = 1, Username = "user1", Email = "user1@test.com", Password = "hash",
UserRoles = [new UserRole { Role = new Role { Id = 1, Name = "Admin" } }] },
new() { Id = 2, Username = "user2", Email = "user2@test.com", Password = "hash",
UserRoles = [new UserRole { Role = new Role { Id = 2, Name = "User" } }] }
};
_userRepository.GetAllAsync().Returns(users);
var result = await _sut.GetAsync(1, 10);
Assert.True(result.IsSuccess);
Assert.Equal(2, result.Value.Items.Count);
Assert.Equal(2, result.Value.TotalCount);
}
[Fact]
public async Task UpdateAsync_ShouldReturnSuccess_WhenRequestIsValid()
{
var request = new UserUpdateRequest(1, "updateduser", "updated@test.com");
var user = new User { Id = 1, Username = "olduser", Email = "old@test.com", Password = "hash" };
_userRepository.GetByIdAsync(request.Id).Returns(user);
var result = await _sut.UpdateAsync(request);
Assert.True(result.IsSuccess);
Assert.Equal(request.Username, user.Username);
Assert.Equal(request.Email, user.Email);
_userRepository.Received(1).Update(user);
await _unitOfWork.Received(1).CommitAsync();
}
[Fact]
public async Task UpdateAsync_ShouldReturnFailure_WhenValidationFails()
{
var request = new UserUpdateRequest(0, "", "");
var result = await _sut.UpdateAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(ErrorTypeConstant.ValidationError, result.Error.Code);
}
[Fact]
public async Task UpdateAsync_ShouldReturnFailure_WhenUserNotFound()
{
var request = new UserUpdateRequest(99, "user", "user@test.com");
_userRepository.GetByIdAsync(request.Id).Returns((User?)null);
var result = await _sut.UpdateAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(UserError.UserNotFound, result.Error);
}
[Fact]
public async Task DeleteAsync_ShouldReturnSuccess_WhenUserExists()
{
var user = new User { Id = 2, Username = "user2", Email = "user2@test.com", Password = "hash" };
_userRepository.GetByIdAsync(2).Returns(user);
var result = await _sut.DeleteAsync(2, 1);
Assert.True(result.IsSuccess);
_userRepository.Received(1).Delete(user);
await _unitOfWork.Received(1).CommitAsync();
}
[Fact]
public async Task DeleteAsync_ShouldReturnFailure_WhenDeletingYourself()
{
var result = await _sut.DeleteAsync(1, 1);
Assert.True(result.IsFailure);
Assert.Equal(UserError.CannotDeleteYourself, result.Error);
}
[Fact]
public async Task DeleteAsync_ShouldReturnFailure_WhenUserNotFound()
{
_userRepository.GetByIdAsync(99).Returns((User?)null);
var result = await _sut.DeleteAsync(99, 1);
Assert.True(result.IsFailure);
Assert.Equal(UserError.UserNotFound, result.Error);
}
[Fact]
public async Task GetUserByIdAsync_ShouldReturnUser_WhenUserExists()
{
var user = new User
{
Id = 1, Username = "testuser", Email = "test@test.com", Password = "hash",
UserRoles = [new UserRole { Role = new Role { Id = 1, Name = "Admin" } }]
};
_userRepository.GetByIdAsync(1).Returns(user);
var result = await _sut.GetUserByIdAsync(1);
Assert.True(result.IsSuccess);
Assert.Equal(user.Id, result.Value.Id);
Assert.Equal(user.Username, result.Value.Username);
Assert.Equal(user.Email, result.Value.Email);
}
[Fact]
public async Task GetUserByIdAsync_ShouldReturnFailure_WhenUserNotFound()
{
_userRepository.GetByIdAsync(99).Returns((User?)null);
var result = await _sut.GetUserByIdAsync(99);
Assert.True(result.IsFailure);
Assert.Equal(UserError.UserNotFound, result.Error);
}
[Fact]
public async Task AssignRoleAsync_ShouldReturnSuccess_WhenUserDoesNotHaveRole()
{
var request = new AssingRoleRequest(1, 2);
_userRoleRepository.HasRoleAsync(request.UserId, request.RoleId).Returns(false);
_userRoleRepository.AddRoleAsync(request.UserId, request.RoleId).Returns(true);
var result = await _sut.AssignRoleAsync(request);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task AssignRoleAsync_ShouldReturnFailure_WhenUserAlreadyHasRole()
{
var request = new AssingRoleRequest(1, 2);
_userRoleRepository.HasRoleAsync(request.UserId, request.RoleId).Returns(true);
var result = await _sut.AssignRoleAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(UserError.UserAlreadyHasRole, result.Error);
}
[Fact]
public async Task AssignRoleAsync_ShouldReturnFailure_WhenAddRoleFails()
{
var request = new AssingRoleRequest(1, 2);
_userRoleRepository.HasRoleAsync(request.UserId, request.RoleId).Returns(false);
_userRoleRepository.AddRoleAsync(request.UserId, request.RoleId).Returns(false);
var result = await _sut.AssignRoleAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(UserError.FailedToAssignRole, result.Error);
}
[Fact]
public async Task RevokeRoleAsync_ShouldReturnSuccess_WhenUserHasRole()
{
var request = new AssingRoleRequest(1, 2);
_userRoleRepository.HasRoleAsync(request.UserId, request.RoleId).Returns(true);
_userRoleRepository.RemoveRoleAsync(request.UserId, request.RoleId).Returns(true);
var result = await _sut.RevokeRoleAsync(request);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task RevokeRoleAsync_ShouldReturnFailure_WhenUserHasNoRole()
{
var request = new AssingRoleRequest(1, 2);
_userRoleRepository.HasRoleAsync(request.UserId, request.RoleId).Returns(false);
var result = await _sut.RevokeRoleAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(UserError.UserHasNoRole, result.Error);
}
[Fact]
public async Task RevokeRoleAsync_ShouldReturnFailure_WhenRemoveRoleFails()
{
var request = new AssingRoleRequest(1, 2);
_userRoleRepository.HasRoleAsync(request.UserId, request.RoleId).Returns(true);
_userRoleRepository.RemoveRoleAsync(request.UserId, request.RoleId).Returns(false);
var result = await _sut.RevokeRoleAsync(request);
Assert.True(result.IsFailure);
Assert.Equal(UserError.FailedToRevokeRole, result.Error);
}
}
-9
View File
@@ -1,9 +0,0 @@
namespace Application.UnitTest;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
@@ -0,0 +1,33 @@
using Application.Models;
using Application.Validators;
namespace Application.UnitTest.Validators;
public class LoginRequestValidatorTests
{
private readonly LoginRequestValidator _sut = new();
[Fact]
public void Validate_ShouldPass_WhenAllFieldsAreValid()
{
var request = new LoginRequest("test@example.com", "password123");
var result = _sut.Validate(request);
Assert.True(result.IsValid);
}
[Theory]
[InlineData("", "password123", "Email")]
[InlineData("not-an-email", "password123", "Email")]
[InlineData("test@example.com", "", "Password")]
public void Validate_ShouldFail_WhenFieldIsInvalid(string email, string password, string expectedProperty)
{
var request = new LoginRequest(email, password);
var result = _sut.Validate(request);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.PropertyName == expectedProperty);
}
}
@@ -0,0 +1,41 @@
using Application.Models;
using Application.Validators;
namespace Application.UnitTest.Validators;
public class RegisterRequestValidatorTests
{
private readonly RegisterRequestValidator _sut = new();
[Fact]
public void Validate_ShouldPass_WhenAllFieldsAreValid()
{
var request = new RegisterRequest("ValidUser", "test@example.com", "ValidP@ss1");
var result = _sut.Validate(request);
Assert.True(result.IsValid);
}
[Theory]
[InlineData("ValidUser", "", "ValidP@ss1", "Email")]
[InlineData("ValidUser", "invalid-email", "ValidP@ss1", "Email")]
[InlineData("ValidUser", "test@example.com", "", "Password")]
[InlineData("ValidUser", "test@example.com", "short1A@", "Password")]
[InlineData("ValidUser", "test@example.com", "nouppercase1@", "Password")]
[InlineData("ValidUser", "test@example.com", "NOLOWERCASE1@", "Password")]
[InlineData("ValidUser", "test@example.com", "NoDigit@aa", "Password")]
[InlineData("ValidUser", "test@example.com", "NoSpecialChar1", "Password")]
[InlineData("", "test@example.com", "ValidP@ss1", "Username")]
[InlineData("ab", "test@example.com", "ValidP@ss1", "Username")]
[InlineData("user@name", "test@example.com", "ValidP@ss1", "Username")]
public void Validate_ShouldFail_WhenFieldIsInvalid(string username, string email, string password, string expectedProperty)
{
var request = new RegisterRequest(username, email, password);
var result = _sut.Validate(request);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.PropertyName == expectedProperty);
}
}
@@ -0,0 +1,34 @@
using Application.Models;
using Application.Validators;
namespace Application.UnitTest.Validators;
public class UserUpdateRequestValidatorTests
{
private readonly UserUpdateRequestValidator _sut = new();
[Fact]
public void Validate_ShouldPass_WhenAllFieldsAreValid()
{
var request = new UserUpdateRequest(1, "ValidUser", "test@example.com");
var result = _sut.Validate(request);
Assert.True(result.IsValid);
}
[Theory]
[InlineData(0, "ValidUser", "test@example.com", "Id")]
[InlineData(1, "", "test@example.com", "Username")]
[InlineData(1, "ValidUser", "", "Email")]
[InlineData(1, "ValidUser", "invalid-email", "Email")]
public void Validate_ShouldFail_WhenFieldIsInvalid(int id, string username, string email, string expectedProperty)
{
var request = new UserUpdateRequest(id, username, email);
var result = _sut.Validate(request);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.PropertyName == expectedProperty);
}
}