add unit tests
This commit is contained in:
73
src/app/modules/presentation/select/select.component.spec.ts
Normal file
73
src/app/modules/presentation/select/select.component.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {GlobalSettingsService} from '../../../services/global-settings.service';
|
||||||
|
import {ShowService} from '../../shows/services/show.service';
|
||||||
|
import {SelectComponent} from './select.component';
|
||||||
|
|
||||||
|
describe('SelectComponent', () => {
|
||||||
|
let component: SelectComponent;
|
||||||
|
let fixture: ComponentFixture<SelectComponent>;
|
||||||
|
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||||
|
let globalSettingsServiceSpy: jasmine.SpyObj<GlobalSettingsService>;
|
||||||
|
let routerSpy: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['list$', 'update$']);
|
||||||
|
globalSettingsServiceSpy = jasmine.createSpyObj<GlobalSettingsService>('GlobalSettingsService', ['set']);
|
||||||
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
||||||
|
|
||||||
|
showServiceSpy.list$.and.returnValue(
|
||||||
|
of([
|
||||||
|
{id: 'older', date: {toDate: () => new Date('2025-12-15T00:00:00Z')}},
|
||||||
|
{id: 'recent-a', date: {toDate: () => new Date('2026-03-01T00:00:00Z')}},
|
||||||
|
{id: 'recent-b', date: {toDate: () => new Date('2026-02-20T00:00:00Z')}},
|
||||||
|
] as never)
|
||||||
|
);
|
||||||
|
showServiceSpy.update$.and.resolveTo();
|
||||||
|
globalSettingsServiceSpy.set.and.resolveTo();
|
||||||
|
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SelectComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ShowService, useValue: showServiceSpy},
|
||||||
|
{provide: GlobalSettingsService, useValue: globalSettingsServiceSpy},
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SelectComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should become visible on init', () => {
|
||||||
|
component.ngOnInit();
|
||||||
|
|
||||||
|
expect(component.visible).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose recent shows sorted descending by date', done => {
|
||||||
|
component.shows$.subscribe(shows => {
|
||||||
|
expect(showServiceSpy.list$).toHaveBeenCalledWith(true);
|
||||||
|
expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist the selected show, trigger presentation reset and navigate', async () => {
|
||||||
|
const show = {id: 'show-1'} as never;
|
||||||
|
component.visible = true;
|
||||||
|
|
||||||
|
await component.selectShow(show);
|
||||||
|
|
||||||
|
expect(component.visible).toBeFalse();
|
||||||
|
expect(globalSettingsServiceSpy.set).toHaveBeenCalledWith({currentShow: 'show-1'});
|
||||||
|
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {presentationSongId: 'title'});
|
||||||
|
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/presentation/remote');
|
||||||
|
});
|
||||||
|
});
|
||||||
82
src/app/modules/shows/edit/edit.component.spec.ts
Normal file
82
src/app/modules/shows/edit/edit.component.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {Timestamp} from '@angular/fire/firestore';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {ShowDataService} from '../services/show-data.service';
|
||||||
|
import {ShowService} from '../services/show.service';
|
||||||
|
import {EditComponent} from './edit.component';
|
||||||
|
|
||||||
|
describe('EditComponent', () => {
|
||||||
|
let component: EditComponent;
|
||||||
|
let fixture: ComponentFixture<EditComponent>;
|
||||||
|
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||||
|
let showDataServiceStub: Pick<ShowDataService, 'list$'>;
|
||||||
|
let routerSpy: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['read$', 'update$']);
|
||||||
|
showDataServiceStub = {list$: of([] as never)};
|
||||||
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
||||||
|
|
||||||
|
showServiceSpy.read$.and.returnValue(
|
||||||
|
of({
|
||||||
|
id: 'show-1',
|
||||||
|
showType: 'service-worship',
|
||||||
|
date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
|
||||||
|
} as never)
|
||||||
|
);
|
||||||
|
showServiceSpy.update$.and.resolveTo();
|
||||||
|
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [EditComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ShowService, useValue: showServiceSpy},
|
||||||
|
{provide: ShowDataService, useValue: showDataServiceStub},
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
{provide: ActivatedRoute, useValue: {params: of({showId: 'show-1'})}},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(EditComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the current show into the form on init', () => {
|
||||||
|
component.ngOnInit();
|
||||||
|
|
||||||
|
expect(showServiceSpy.read$).toHaveBeenCalledWith('show-1');
|
||||||
|
expect(component.form.value.id).toBe('show-1');
|
||||||
|
expect(component.form.value.showType).toBe('service-worship');
|
||||||
|
expect(component.form.value.date).toEqual(new Date('2026-03-10T00:00:00Z'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not save when the form is invalid', async () => {
|
||||||
|
component.form.setValue({id: null, date: null, showType: null});
|
||||||
|
|
||||||
|
await component.onSave();
|
||||||
|
|
||||||
|
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
|
expect(routerSpy.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the show and navigate back to the detail page', async () => {
|
||||||
|
const date = new Date('2026-03-11T00:00:00Z');
|
||||||
|
const firestoreTimestamp = {seconds: 1} as never;
|
||||||
|
spyOn(Timestamp, 'fromDate').and.returnValue(firestoreTimestamp);
|
||||||
|
component.form.setValue({id: 'show-1', date, showType: 'home-group'});
|
||||||
|
|
||||||
|
await component.onSave();
|
||||||
|
|
||||||
|
expect(Timestamp.fromDate).toHaveBeenCalledWith(date);
|
||||||
|
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {
|
||||||
|
date: firestoreTimestamp,
|
||||||
|
showType: 'home-group',
|
||||||
|
} as never);
|
||||||
|
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/shows/show-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {Packer} from 'docx';
|
||||||
import {DocxService} from './docx.service';
|
import {DocxService} from './docx.service';
|
||||||
|
|
||||||
describe('DocxService', () => {
|
describe('DocxService', () => {
|
||||||
@@ -13,4 +13,37 @@ describe('DocxService', () => {
|
|||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
void expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not try to save a document when the required data cannot be prepared', async () => {
|
||||||
|
const prepareDataSpy = spyOn<any>(service, 'prepareData').and.resolveTo(null);
|
||||||
|
const saveAsSpy = spyOn<any>(service, 'saveAs');
|
||||||
|
|
||||||
|
await service.create('show-1');
|
||||||
|
|
||||||
|
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
||||||
|
expect(saveAsSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build and save a docx file when all data is available', async () => {
|
||||||
|
const blob = new Blob(['docx']);
|
||||||
|
const prepareDataSpy = spyOn<any>(service, 'prepareData').and.resolveTo({
|
||||||
|
show: {
|
||||||
|
showType: 'service-worship',
|
||||||
|
date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
|
||||||
|
},
|
||||||
|
songs: [],
|
||||||
|
user: {name: 'Benjamin'},
|
||||||
|
config: {ccliLicenseId: '12345'},
|
||||||
|
});
|
||||||
|
const prepareNewDocumentSpy = spyOn<any>(service, 'prepareNewDocument').and.returnValue({doc: true});
|
||||||
|
const saveAsSpy = spyOn<any>(service, 'saveAs');
|
||||||
|
spyOn(Packer, 'toBlob').and.resolveTo(blob);
|
||||||
|
|
||||||
|
await service.create('show-1', {copyright: true});
|
||||||
|
|
||||||
|
expect(prepareDataSpy).toHaveBeenCalledWith('show-1');
|
||||||
|
expect(prepareNewDocumentSpy).toHaveBeenCalled();
|
||||||
|
expect(Packer.toBlob).toHaveBeenCalledWith({doc: true} as never);
|
||||||
|
expect(saveAsSpy).toHaveBeenCalledWith(blob, jasmine.stringMatching(/\.docx$/));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,45 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Storage} from '@angular/fire/storage';
|
import {Storage} from '@angular/fire/storage';
|
||||||
|
|
||||||
import {FileService} from './file.service';
|
|
||||||
import {FileDataService} from './file-data.service';
|
import {FileDataService} from './file-data.service';
|
||||||
|
import {FileService} from './file.service';
|
||||||
|
|
||||||
describe('FileService', () => {
|
describe('FileService', () => {
|
||||||
let service: FileService;
|
let service: FileService;
|
||||||
|
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['delete']);
|
||||||
|
fileDataServiceSpy.delete.and.resolveTo();
|
||||||
|
|
||||||
void TestBed.configureTestingModule({
|
void TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{provide: Storage, useValue: {}},
|
{provide: Storage, useValue: {app: 'test-storage'}},
|
||||||
{provide: FileDataService, useValue: {delete: () => Promise.resolve()}},
|
{provide: FileDataService, useValue: fileDataServiceSpy},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
service = TestBed.inject(FileService);
|
service = TestBed.inject(FileService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve download urls via AngularFire storage helpers', async () => {
|
||||||
|
const resolveSpy = spyOn<any>(service, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf');
|
||||||
|
|
||||||
|
await expectAsync(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).toBeResolvedTo('https://cdn.example/file.pdf');
|
||||||
|
|
||||||
|
expect(resolveSpy).toHaveBeenCalledWith('songs/song-1/file.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete the file from storage and metadata from firestore', async () => {
|
||||||
|
const deleteFromStorageSpy = spyOn<any>(service, 'deleteFromStorage').and.resolveTo();
|
||||||
|
|
||||||
|
service.delete('songs/song-1/file.pdf', 'song-1', 'file-1');
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(deleteFromStorageSpy).toHaveBeenCalledWith('songs/song-1/file.pdf');
|
||||||
|
expect(fileDataServiceSpy.delete).toHaveBeenCalledWith('song-1', 'file-1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
import {EnvironmentInjector, inject, Injectable, runInInjectionContext} from '@angular/core';
|
||||||
import {deleteObject, getDownloadURL, ref, Storage} from '@angular/fire/storage';
|
import {deleteObject, getDownloadURL, ref, Storage} from '@angular/fire/storage';
|
||||||
import {from, Observable} from 'rxjs';
|
import {from, Observable} from 'rxjs';
|
||||||
import {FileDataService} from './file-data.service';
|
import {FileDataService} from './file-data.service';
|
||||||
@@ -12,11 +12,19 @@ export class FileService {
|
|||||||
private environmentInjector = inject(EnvironmentInjector);
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public getDownloadUrl(path: string): Observable<string> {
|
public getDownloadUrl(path: string): Observable<string> {
|
||||||
return from(runInInjectionContext(this.environmentInjector, () => getDownloadURL(ref(this.storage, path))));
|
return from(runInInjectionContext(this.environmentInjector, () => this.resolveDownloadUrl(path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(path: string, songId: string, fileId: string): void {
|
public delete(path: string, songId: string, fileId: string): void {
|
||||||
void runInInjectionContext(this.environmentInjector, () => deleteObject(ref(this.storage, path)));
|
void runInInjectionContext(this.environmentInjector, () => this.deleteFromStorage(path));
|
||||||
void this.fileDataService.delete(songId, fileId);
|
void this.fileDataService.delete(songId, fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveDownloadUrl(path: string): Promise<string> {
|
||||||
|
return getDownloadURL(ref(this.storage, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteFromStorage(path: string): Promise<void> {
|
||||||
|
return deleteObject(ref(this.storage, path));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/app/modules/songs/services/song-list.resolver.spec.ts
Normal file
32
src/app/modules/songs/services/song-list.resolver.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {SongService} from './song.service';
|
||||||
|
import {SongListResolver} from './song-list.resolver';
|
||||||
|
|
||||||
|
describe('SongListResolver', () => {
|
||||||
|
let resolver: SongListResolver;
|
||||||
|
let songServiceSpy: jasmine.SpyObj<SongService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['list$']);
|
||||||
|
songServiceSpy.list$.and.returnValue(of([{id: 'song-1', title: 'Amazing Grace'}] as never));
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: SongService, useValue: songServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
|
resolver = TestBed.inject(SongListResolver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(resolver).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the first emitted song list from the service', done => {
|
||||||
|
resolver.resolve().subscribe(songs => {
|
||||||
|
expect(songServiceSpy.list$).toHaveBeenCalled();
|
||||||
|
expect(songs).toEqual([{id: 'song-1', title: 'Amazing Grace'}] as never);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,22 +1,54 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Storage} from '@angular/fire/storage';
|
import {Storage} from '@angular/fire/storage';
|
||||||
|
|
||||||
import {UploadService} from './upload.service';
|
|
||||||
import {FileDataService} from './file-data.service';
|
import {FileDataService} from './file-data.service';
|
||||||
|
import {Upload} from './upload';
|
||||||
|
import {UploadService} from './upload.service';
|
||||||
|
|
||||||
describe('UploadServiceService', () => {
|
describe('UploadService', () => {
|
||||||
beforeEach(
|
let service: UploadService;
|
||||||
() =>
|
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
|
||||||
void TestBed.configureTestingModule({
|
|
||||||
providers: [
|
beforeEach(() => {
|
||||||
{provide: Storage, useValue: {}},
|
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['set']);
|
||||||
{provide: FileDataService, useValue: {set: () => Promise.resolve('')}},
|
fileDataServiceSpy.set.and.resolveTo('file-1');
|
||||||
],
|
|
||||||
})
|
void TestBed.configureTestingModule({
|
||||||
);
|
providers: [
|
||||||
|
{provide: Storage, useValue: {app: 'test-storage'}},
|
||||||
|
{provide: FileDataService, useValue: fileDataServiceSpy},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(UploadService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: UploadService = TestBed.inject(UploadService);
|
expect(service).toBeTruthy();
|
||||||
void expect(service).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
it('should upload the file, update progress and persist file metadata on success', async () => {
|
||||||
|
const task = {
|
||||||
|
on: (event: string, progress: (snapshot: {bytesTransferred: number; totalBytes: number}) => void, error: () => void, success: () => void) => {
|
||||||
|
progress({bytesTransferred: 50, totalBytes: 100});
|
||||||
|
success();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uploadSpy = spyOn<any>(service, 'startUpload').and.returnValue(task as never);
|
||||||
|
const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'}));
|
||||||
|
|
||||||
|
service.pushUpload('song-1', upload);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(uploadSpy).toHaveBeenCalledWith('/attachments/song-1/test.pdf', upload.file);
|
||||||
|
expect(upload.progress).toBe(50);
|
||||||
|
expect(upload.path).toBe('/attachments/song-1');
|
||||||
|
expect(fileDataServiceSpy.set).toHaveBeenCalledWith(
|
||||||
|
'song-1',
|
||||||
|
jasmine.objectContaining({
|
||||||
|
name: 'test.pdf',
|
||||||
|
path: '/attachments/song-1',
|
||||||
|
createdAt: jasmine.any(Date),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ export class UploadService extends FileBase {
|
|||||||
const filePath = `${directory}/${upload.file.name}`;
|
const filePath = `${directory}/${upload.file.name}`;
|
||||||
upload.path = directory;
|
upload.path = directory;
|
||||||
|
|
||||||
const {task} = runInInjectionContext(this.environmentInjector, () => {
|
const task = runInInjectionContext(this.environmentInjector, () => this.startUpload(filePath, upload.file));
|
||||||
const storageRef = ref(this.storage, filePath);
|
|
||||||
return {
|
|
||||||
task: uploadBytesResumable(storageRef, upload.file),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
task.on(
|
task.on(
|
||||||
'state_changed',
|
'state_changed',
|
||||||
@@ -45,4 +40,9 @@ export class UploadService extends FileBase {
|
|||||||
};
|
};
|
||||||
await this.fileDataService.set(songId, file);
|
await this.fileDataService.set(songId, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startUpload(filePath: string, file: File) {
|
||||||
|
const storageRef = ref(this.storage, filePath);
|
||||||
|
return uploadBytesResumable(storageRef, file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {EditSongGuard} from './edit-song.guard';
|
import {EditSongGuard} from './edit-song.guard';
|
||||||
|
|
||||||
describe('EditSongGuard', () => {
|
describe('EditSongGuard', () => {
|
||||||
@@ -11,6 +10,22 @@ describe('EditSongGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(guard).toBeTruthy();
|
expect(guard).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when there is no nested editSongComponent', () => {
|
||||||
|
const result = guard.canDeactivate({editSongComponent: null} as never, {} as never, {} as never, {} as never);
|
||||||
|
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate to askForSave on the nested editSongComponent', async () => {
|
||||||
|
const nextState = {url: '/songs'} as never;
|
||||||
|
const askForSave = jasmine.createSpy('askForSave').and.resolveTo(true);
|
||||||
|
|
||||||
|
const result = await guard.canDeactivate({editSongComponent: {askForSave}} as never, {} as never, {} as never, nextState);
|
||||||
|
|
||||||
|
expect(askForSave).toHaveBeenCalledWith(nextState);
|
||||||
|
expect(result).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,74 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {EditService} from './edit.service';
|
import {EditService} from './edit.service';
|
||||||
|
|
||||||
describe('EditService', () => {
|
describe('EditService', () => {
|
||||||
beforeEach(() => void TestBed.configureTestingModule({}));
|
let service: EditService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
void TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(EditService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: EditService = TestBed.inject(EditService);
|
expect(service).toBeTruthy();
|
||||||
void expect(service).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
it('should create a form with all editable song fields populated', () => {
|
||||||
|
const form = service.createSongForm({
|
||||||
|
text: 'Line 1',
|
||||||
|
title: 'Amazing Grace',
|
||||||
|
comment: 'Comment',
|
||||||
|
flags: 'fast',
|
||||||
|
key: 'G',
|
||||||
|
tempo: 90,
|
||||||
|
type: 'Praise',
|
||||||
|
status: 'final',
|
||||||
|
legalType: 'allowed',
|
||||||
|
legalOwner: 'CCLI',
|
||||||
|
legalOwnerId: '123',
|
||||||
|
artist: 'Artist',
|
||||||
|
label: 'Label',
|
||||||
|
termsOfUse: 'Use it',
|
||||||
|
origin: 'Origin',
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(form.getRawValue()).toEqual({
|
||||||
|
text: 'Line 1',
|
||||||
|
title: 'Amazing Grace',
|
||||||
|
comment: 'Comment',
|
||||||
|
flags: 'fast',
|
||||||
|
key: 'G',
|
||||||
|
tempo: 90,
|
||||||
|
type: 'Praise',
|
||||||
|
status: 'final',
|
||||||
|
legalType: 'allowed',
|
||||||
|
legalOwner: 'CCLI',
|
||||||
|
legalOwnerId: '123',
|
||||||
|
artist: 'Artist',
|
||||||
|
label: 'Label',
|
||||||
|
termsOfUse: 'Use it',
|
||||||
|
origin: 'Origin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default the status control to draft when the song has no status', () => {
|
||||||
|
const form = service.createSongForm({
|
||||||
|
text: '',
|
||||||
|
title: 'Untitled',
|
||||||
|
comment: '',
|
||||||
|
flags: '',
|
||||||
|
key: 'C',
|
||||||
|
tempo: 80,
|
||||||
|
type: 'Misc',
|
||||||
|
legalType: 'open',
|
||||||
|
legalOwner: 'other',
|
||||||
|
legalOwnerId: '',
|
||||||
|
artist: '',
|
||||||
|
label: '',
|
||||||
|
termsOfUse: '',
|
||||||
|
origin: '',
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(form.get('status')?.value).toBe('draft');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {DbService} from './db.service';
|
||||||
import {ConfigService} from './config.service';
|
import {ConfigService} from './config.service';
|
||||||
|
|
||||||
describe('ConfigService', () => {
|
describe('ConfigService', () => {
|
||||||
let service: ConfigService;
|
let service: ConfigService;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
void TestBed.configureTestingModule({});
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc$']);
|
||||||
|
dbServiceSpy.doc$.and.returnValue(of({copyright: 'CCLI'}) as never);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
service = TestBed.inject(ConfigService);
|
service = TestBed.inject(ConfigService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the global config document once on creation', () => {
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/config');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose the shared config stream via get$', done => {
|
||||||
|
service.get$().subscribe(config => {
|
||||||
|
expect(config).toEqual({copyright: 'CCLI'} as never);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the current config via get()', async () => {
|
||||||
|
await expectAsync(service.get()).toBeResolvedTo({copyright: 'CCLI'} as never);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,46 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {DbService} from './db.service';
|
||||||
import {GlobalSettingsService} from './global-settings.service';
|
import {GlobalSettingsService} from './global-settings.service';
|
||||||
|
|
||||||
describe('GlobalSettingsService', () => {
|
describe('GlobalSettingsService', () => {
|
||||||
let service: GlobalSettingsService;
|
let service: GlobalSettingsService;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
let updateSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
void TestBed.configureTestingModule({});
|
updateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc$', 'doc']);
|
||||||
|
dbServiceSpy.doc$.and.returnValue(of({churchName: 'ICF'}) as never);
|
||||||
|
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
service = TestBed.inject(GlobalSettingsService);
|
service = TestBed.inject(GlobalSettingsService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the static global settings document once on creation', () => {
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/static');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose the shared settings stream via the getter', done => {
|
||||||
|
service.get$.subscribe(settings => {
|
||||||
|
expect(settings).toEqual({churchName: 'ICF'} as never);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the static global settings document', async () => {
|
||||||
|
await service.set({churchName: 'New Name'} as never);
|
||||||
|
|
||||||
|
expect(dbServiceSpy.doc).toHaveBeenCalledWith('global/static');
|
||||||
|
expect(updateSpy).toHaveBeenCalledWith({churchName: 'New Name'} as never);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {UserService} from '../user.service';
|
||||||
import {UserNameComponent} from './user-name.component';
|
import {UserNameComponent} from './user-name.component';
|
||||||
|
|
||||||
describe('UserNameComponent', () => {
|
describe('UserNameComponent', () => {
|
||||||
let component: UserNameComponent;
|
let component: UserNameComponent;
|
||||||
let fixture: ComponentFixture<UserNameComponent>;
|
let fixture: ComponentFixture<UserNameComponent>;
|
||||||
|
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']);
|
||||||
|
userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never));
|
||||||
|
|
||||||
void TestBed.configureTestingModule({
|
void TestBed.configureTestingModule({
|
||||||
imports: [UserNameComponent],
|
imports: [UserNameComponent],
|
||||||
|
providers: [{provide: UserService, useValue: userServiceSpy}],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -19,6 +25,27 @@ describe('UserNameComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
void expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the user name when the userId input changes', done => {
|
||||||
|
component.userId = 'user-1';
|
||||||
|
|
||||||
|
component.name$?.subscribe(name => {
|
||||||
|
expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('user-1');
|
||||||
|
expect(name).toBe('Benjamin');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map missing users to null names', done => {
|
||||||
|
userServiceSpy.getUserbyId$.and.returnValue(of(null));
|
||||||
|
|
||||||
|
component.userId = 'missing-user';
|
||||||
|
|
||||||
|
component.name$?.subscribe(name => {
|
||||||
|
expect(name).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
124
src/app/services/user/user-session.service.spec.ts
Normal file
124
src/app/services/user/user-session.service.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
|
import {Auth} from '@angular/fire/auth';
|
||||||
|
import {DbService} from '../db.service';
|
||||||
|
import {UserSessionService} from './user-session.service';
|
||||||
|
|
||||||
|
describe('UserSessionService', () => {
|
||||||
|
let service: UserSessionService;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
let routerSpy: jasmine.SpyObj<Router>;
|
||||||
|
let authStateSubject: BehaviorSubject<unknown>;
|
||||||
|
let createAuthStateSpy: jasmine.Spy;
|
||||||
|
let runInFirebaseContextSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authStateSubject = new BehaviorSubject<unknown>(null);
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc']);
|
||||||
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
|
||||||
|
dbServiceSpy.col$.and.returnValue(of([{id: 'user-1'}]) as never);
|
||||||
|
dbServiceSpy.doc$.and.callFake((path: string) => {
|
||||||
|
if (path === 'users/user-1') {
|
||||||
|
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters', songUsage: {}}) as never;
|
||||||
|
}
|
||||||
|
if (path === 'users/user-2') {
|
||||||
|
return of({id: 'user-2', name: 'Paula', role: 'user', chordMode: 'letters', songUsage: {}}) as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(null) as never;
|
||||||
|
});
|
||||||
|
dbServiceSpy.doc.and.returnValue({
|
||||||
|
update: jasmine.createSpy('update').and.resolveTo(),
|
||||||
|
set: jasmine.createSpy('set').and.resolveTo(),
|
||||||
|
} as never);
|
||||||
|
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||||
|
|
||||||
|
createAuthStateSpy = spyOn<any>(UserSessionService.prototype, 'createAuthState$').and.returnValue(authStateSubject.asObservable() as never);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: DbService, useValue: dbServiceSpy},
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
{provide: Auth, useValue: {}},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(UserSessionService);
|
||||||
|
runInFirebaseContextSpy = spyOn<any>(service, 'runInFirebaseContext');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
expect(createAuthStateSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive userId$ and loggedIn$ from authState', async () => {
|
||||||
|
authStateSubject.next({uid: 'user-1'});
|
||||||
|
|
||||||
|
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
|
||||||
|
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the current user document from auth state', async () => {
|
||||||
|
authStateSubject.next({uid: 'user-1'});
|
||||||
|
|
||||||
|
await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo(
|
||||||
|
jasmine.objectContaining({id: 'user-1', name: 'Benjamin'}) as never
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache user lookups by id', async () => {
|
||||||
|
const first$ = service.getUserbyId$('user-2');
|
||||||
|
const second$ = service.getUserbyId$('user-2');
|
||||||
|
|
||||||
|
expect(first$).toBe(second$);
|
||||||
|
await expectAsync(firstValueFrom(first$)).toBeResolvedTo(jasmine.objectContaining({id: 'user-2'}) as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should login and initialize songUsage when missing', async () => {
|
||||||
|
dbServiceSpy.doc$.and.callFake((path: string) => {
|
||||||
|
if (path === 'users/user-1') {
|
||||||
|
return of({id: 'user-1', name: 'Benjamin', role: 'leader', chordMode: 'letters'}) as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(null) as never;
|
||||||
|
});
|
||||||
|
const updateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
||||||
|
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}});
|
||||||
|
|
||||||
|
await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1');
|
||||||
|
|
||||||
|
expect(runInFirebaseContextSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updateSpy).toHaveBeenCalledWith({songUsage: {}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate logout and password reset to AngularFire auth APIs', async () => {
|
||||||
|
runInFirebaseContextSpy.and.resolveTo();
|
||||||
|
|
||||||
|
await service.logout();
|
||||||
|
await service.changePassword('mail@example.com');
|
||||||
|
|
||||||
|
expect(runInFirebaseContextSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new user document and navigate afterwards', async () => {
|
||||||
|
dbServiceSpy.doc$.and.callFake((path: string) => {
|
||||||
|
if (path === 'users/user-3') {
|
||||||
|
return of({id: 'user-3', name: 'New User', role: 'user', chordMode: 'onlyFirst', songUsage: {}}) as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(null) as never;
|
||||||
|
});
|
||||||
|
const setSpy = jasmine.createSpy('set').and.resolveTo();
|
||||||
|
dbServiceSpy.doc.and.returnValue({set: setSpy} as never);
|
||||||
|
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-3'}});
|
||||||
|
|
||||||
|
await service.createNewUser('mail@example.com', 'New User', 'secret');
|
||||||
|
|
||||||
|
expect(runInFirebaseContextSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setSpy).toHaveBeenCalledWith({name: 'New User', chordMode: 'onlyFirst', songUsage: {}});
|
||||||
|
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/brand/new-user');
|
||||||
|
});
|
||||||
|
});
|
||||||
76
src/app/services/user/user-song-usage.service.spec.ts
Normal file
76
src/app/services/user/user-song-usage.service.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {DbService} from '../db.service';
|
||||||
|
import {ShowDataService} from '../../modules/shows/services/show-data.service';
|
||||||
|
import {ShowSongDataService} from '../../modules/shows/services/show-song-data.service';
|
||||||
|
import {UserSessionService} from './user-session.service';
|
||||||
|
import {UserSongUsageService} from './user-song-usage.service';
|
||||||
|
|
||||||
|
describe('UserSongUsageService', () => {
|
||||||
|
let service: UserSongUsageService;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
||||||
|
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||||
|
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
|
||||||
|
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
|
||||||
|
user$: of({id: 'user-1', role: 'admin', songUsage: {}}) as never,
|
||||||
|
users$: of([{id: 'user-1'}, {id: 'user-2'}]) as never,
|
||||||
|
});
|
||||||
|
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['listRaw$']);
|
||||||
|
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['list$']);
|
||||||
|
|
||||||
|
sessionSpy.update$.and.resolveTo();
|
||||||
|
showDataServiceSpy.listRaw$.and.returnValue(of([{id: 'show-1', owner: 'user-1'}, {id: 'show-2', owner: 'user-2'}] as never));
|
||||||
|
showSongDataServiceSpy.list$.and.callFake((showId: string) =>
|
||||||
|
of(showId === 'show-1' ? ([{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] as never) : ([{songId: 'song-3'}] as never))
|
||||||
|
);
|
||||||
|
dbServiceSpy.doc.and.returnValue({update: jasmine.createSpy('update').and.resolveTo()} as never);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: DbService, useValue: dbServiceSpy},
|
||||||
|
{provide: UserSessionService, useValue: sessionSpy},
|
||||||
|
{provide: ShowDataService, useValue: showDataServiceSpy},
|
||||||
|
{provide: ShowSongDataService, useValue: showSongDataServiceSpy},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(UserSongUsageService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increment and decrement song usage for the current user', async () => {
|
||||||
|
const updateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
|
||||||
|
|
||||||
|
await service.incSongCount('song-1');
|
||||||
|
await service.decSongCount('song-2');
|
||||||
|
|
||||||
|
expect(dbServiceSpy.doc).toHaveBeenCalledWith('users/user-1');
|
||||||
|
expect(updateSpy.calls.argsFor(0)[0]).toEqual({'songUsage.song-1': jasmine.anything()});
|
||||||
|
expect(updateSpy.calls.argsFor(1)[0]).toEqual({'songUsage.song-2': jasmine.anything()});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rebuild song usage for all users based on owned show songs', async () => {
|
||||||
|
await expectAsync(service.rebuildSongUsage()).toBeResolvedTo({
|
||||||
|
usersProcessed: 2,
|
||||||
|
showsProcessed: 2,
|
||||||
|
showSongsProcessed: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sessionSpy.update$).toHaveBeenCalledWith('user-1', {songUsage: {'song-1': 2, 'song-2': 1}});
|
||||||
|
expect(sessionSpy.update$).toHaveBeenCalledWith('user-2', {songUsage: {'song-3': 1}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject song usage rebuilds for non-admin users', async () => {
|
||||||
|
Object.defineProperty(sessionSpy, 'user$', {value: of({id: 'user-1', role: 'leader'})});
|
||||||
|
|
||||||
|
await expectAsync(service.rebuildSongUsage()).toBeRejectedWithError('Admin role required to rebuild songUsage.');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,108 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
import {UserService} from './user.service';
|
import {UserService} from './user.service';
|
||||||
|
import {UserSessionService} from './user-session.service';
|
||||||
|
import {UserSongUsageService} from './user-song-usage.service';
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
beforeEach(() => void TestBed.configureTestingModule({}));
|
let service: UserService;
|
||||||
|
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
||||||
|
let songUsageSpy: jasmine.SpyObj<UserSongUsageService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionSpy = jasmine.createSpyObj<UserSessionService>(
|
||||||
|
'UserSessionService',
|
||||||
|
['currentUser', 'getUserbyId', 'getUserbyId$', 'login', 'loggedIn$', 'list$', 'logout', 'update$', 'changePassword', 'createNewUser'],
|
||||||
|
{
|
||||||
|
users$: of([{id: 'user-1'}]) as never,
|
||||||
|
userId$: of('user-1'),
|
||||||
|
user$: of({id: 'user-1'}) as never,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
songUsageSpy = jasmine.createSpyObj<UserSongUsageService>('UserSongUsageService', ['incSongCount', 'decSongCount', 'rebuildSongUsage']);
|
||||||
|
|
||||||
|
sessionSpy.currentUser.and.resolveTo({id: 'user-1'} as never);
|
||||||
|
sessionSpy.getUserbyId.and.resolveTo({id: 'user-2'} as never);
|
||||||
|
sessionSpy.getUserbyId$.and.returnValue(of({id: 'user-2'}) as never);
|
||||||
|
sessionSpy.login.and.resolveTo('user-1');
|
||||||
|
sessionSpy.loggedIn$.and.returnValue(of(true));
|
||||||
|
sessionSpy.list$.and.returnValue(of([{id: 'user-1'}]) as never);
|
||||||
|
sessionSpy.logout.and.resolveTo();
|
||||||
|
sessionSpy.update$.and.resolveTo();
|
||||||
|
sessionSpy.changePassword.and.resolveTo();
|
||||||
|
sessionSpy.createNewUser.and.resolveTo();
|
||||||
|
songUsageSpy.incSongCount.and.resolveTo();
|
||||||
|
songUsageSpy.decSongCount.and.resolveTo();
|
||||||
|
songUsageSpy.rebuildSongUsage.and.resolveTo({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: UserSessionService, useValue: sessionSpy},
|
||||||
|
{provide: UserSongUsageService, useValue: songUsageSpy},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(UserService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: UserService = TestBed.inject(UserService);
|
expect(service).toBeTruthy();
|
||||||
void expect(service).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
it('should expose the session streams directly', done => {
|
||||||
|
service.userId$.subscribe(userId => {
|
||||||
|
expect(userId).toBe('user-1');
|
||||||
|
service.user$.subscribe(user => {
|
||||||
|
expect(user).toEqual({id: 'user-1'} as never);
|
||||||
|
service.users$.subscribe(users => {
|
||||||
|
expect(users).toEqual([{id: 'user-1'}] as never);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate session operations to UserSessionService', async () => {
|
||||||
|
await expectAsync(service.currentUser()).toBeResolvedTo({id: 'user-1'} as never);
|
||||||
|
await expectAsync(service.getUserbyId('user-2')).toBeResolvedTo({id: 'user-2'} as never);
|
||||||
|
await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1');
|
||||||
|
await service.logout();
|
||||||
|
await service.update$('user-1', {name: 'Benjamin'} as never);
|
||||||
|
await service.changePassword('mail');
|
||||||
|
await service.createNewUser('mail', 'Benjamin', 'secret');
|
||||||
|
|
||||||
|
expect(sessionSpy.currentUser).toHaveBeenCalled();
|
||||||
|
expect(sessionSpy.getUserbyId).toHaveBeenCalledWith('user-2');
|
||||||
|
expect(sessionSpy.login).toHaveBeenCalledWith('mail', 'secret');
|
||||||
|
expect(sessionSpy.logout).toHaveBeenCalled();
|
||||||
|
expect(sessionSpy.update$).toHaveBeenCalledWith('user-1', {name: 'Benjamin'} as never);
|
||||||
|
expect(sessionSpy.changePassword).toHaveBeenCalledWith('mail');
|
||||||
|
expect(sessionSpy.createNewUser).toHaveBeenCalledWith('mail', 'Benjamin', 'secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate user lookup and loggedIn/list streams to UserSessionService', done => {
|
||||||
|
service.getUserbyId$('user-2').subscribe(user => {
|
||||||
|
expect(user).toEqual({id: 'user-2'} as never);
|
||||||
|
service.loggedIn$().subscribe(loggedIn => {
|
||||||
|
expect(loggedIn).toBeTrue();
|
||||||
|
service.list$().subscribe(users => {
|
||||||
|
expect(users).toEqual([{id: 'user-1'}] as never);
|
||||||
|
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
|
||||||
|
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
|
||||||
|
expect(sessionSpy.list$).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate song usage operations to UserSongUsageService', async () => {
|
||||||
|
await service.incSongCount('song-1');
|
||||||
|
await service.decSongCount('song-2');
|
||||||
|
await expectAsync(service.rebuildSongUsage()).toBeResolvedTo({usersProcessed: 1, showsProcessed: 2, showSongsProcessed: 3});
|
||||||
|
|
||||||
|
expect(songUsageSpy.incSongCount).toHaveBeenCalledWith('song-1');
|
||||||
|
expect(songUsageSpy.decSongCount).toHaveBeenCalledWith('song-2');
|
||||||
|
expect(songUsageSpy.rebuildSongUsage).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,92 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {UserService} from '../../services/user/user.service';
|
||||||
import {RoleGuard} from './role.guard';
|
import {RoleGuard} from './role.guard';
|
||||||
|
|
||||||
describe('RoleGuard', () => {
|
describe('RoleGuard', () => {
|
||||||
let guard: RoleGuard;
|
let guard: RoleGuard;
|
||||||
|
let routerSpy: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
void TestBed.configureTestingModule({});
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
|
routerSpy.createUrlTree.and.callFake(commands => ({commands}) as never);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
{provide: UserService, useValue: {user$: of(null)}},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(guard).toBeTruthy();
|
expect(guard).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when requiredRoles is missing', () => {
|
||||||
|
expect(() => guard.canActivate({data: {}} as never)).toThrowError('requiredRoles is not defined!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny access when there is no current user', done => {
|
||||||
|
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
||||||
|
expect(result).toBeFalse();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow admins regardless of requiredRoles', done => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
{provide: UserService, useValue: {user$: of({role: 'user;admin'})}},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
|
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow users with a matching required role', done => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
{provide: UserService, useValue: {user$: of({role: 'leader;user'})}},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
|
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect users without the required role to their role default route', done => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
|
routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never);
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: routerSpy},
|
||||||
|
{provide: UserService, useValue: {user$: of({role: 'presenter'})}},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
|
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
||||||
|
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['presentation']);
|
||||||
|
expect(result).toEqual({redirect: ['presentation']} as never);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user