migrate angular 21 tests
This commit is contained in:
75
src/app/modules/guest/guest-show-data.service.spec.ts
Normal file
75
src/app/modules/guest/guest-show-data.service.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {DbService} from 'src/app/services/db.service';
|
||||||
|
import {GuestShowDataService} from './guest-show-data.service';
|
||||||
|
|
||||||
|
describe('GuestShowDataService', () => {
|
||||||
|
let service: GuestShowDataService;
|
||||||
|
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docSpy: jasmine.Spy;
|
||||||
|
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
||||||
|
let colSpy: jasmine.Spy;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||||
|
docSpy = jasmine.createSpy('doc').and.returnValue({
|
||||||
|
update: docUpdateSpy,
|
||||||
|
delete: docDeleteSpy,
|
||||||
|
});
|
||||||
|
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'guest-2'});
|
||||||
|
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||||
|
dbServiceSpy.col$.and.returnValue(of([{id: 'guest-1'}]) as never);
|
||||||
|
dbServiceSpy.doc$.and.returnValue(of({id: 'guest-1'}) as never);
|
||||||
|
dbServiceSpy.doc.and.callFake(docSpy);
|
||||||
|
dbServiceSpy.col.and.callFake(colSpy);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(GuestShowDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the guest collection for list$', () => {
|
||||||
|
service.list$.subscribe();
|
||||||
|
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('guest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read a single guest show by id', () => {
|
||||||
|
service.read$('guest-7').subscribe();
|
||||||
|
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('guest/guest-7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a guest show document', async () => {
|
||||||
|
await service.update$('guest-7', {published: true} as never);
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('guest/guest-7');
|
||||||
|
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(updatePayload).toEqual({published: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a guest show and return the created id', async () => {
|
||||||
|
await expectAsync(service.add({published: false} as never)).toBeResolvedTo('guest-2');
|
||||||
|
|
||||||
|
expect(colSpy).toHaveBeenCalledWith('guest');
|
||||||
|
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(addPayload).toEqual({published: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a guest show document', async () => {
|
||||||
|
await service.delete('guest-7');
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('guest/guest-7');
|
||||||
|
expect(docDeleteSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
62
src/app/modules/guest/guest-show.service.spec.ts
Normal file
62
src/app/modules/guest/guest-show.service.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {GuestShowDataService} from './guest-show-data.service';
|
||||||
|
import {GuestShowService} from './guest-show.service';
|
||||||
|
import {ShowService} from '../shows/services/show.service';
|
||||||
|
|
||||||
|
describe('GuestShowService', () => {
|
||||||
|
let service: GuestShowService;
|
||||||
|
let guestShowDataServiceSpy: jasmine.SpyObj<GuestShowDataService>;
|
||||||
|
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
guestShowDataServiceSpy = jasmine.createSpyObj<GuestShowDataService>('GuestShowDataService', ['add', 'update$']);
|
||||||
|
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['update$']);
|
||||||
|
guestShowDataServiceSpy.add.and.resolveTo('share-1');
|
||||||
|
guestShowDataServiceSpy.update$.and.resolveTo();
|
||||||
|
showServiceSpy.update$.and.resolveTo();
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: GuestShowDataService, useValue: guestShowDataServiceSpy},
|
||||||
|
{provide: ShowService, useValue: showServiceSpy},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(GuestShowService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new guest share, persist the generated shareId on the show and return the share url', async () => {
|
||||||
|
const show = {id: 'show-1', showType: 'service-worship', date: new Date(), shareId: ''} as any;
|
||||||
|
const songs = [{id: 'song-1'}] as any;
|
||||||
|
const expectedUrl = window.location.protocol + '//' + window.location.host + '/guest/share-1';
|
||||||
|
|
||||||
|
await expectAsync(service.share(show, songs)).toBeResolvedTo(expectedUrl);
|
||||||
|
|
||||||
|
expect(guestShowDataServiceSpy.add).toHaveBeenCalledWith({
|
||||||
|
showType: 'service-worship',
|
||||||
|
date: show.date,
|
||||||
|
songs,
|
||||||
|
});
|
||||||
|
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {shareId: 'share-1'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update an existing share and reuse its id in the returned url', async () => {
|
||||||
|
const show = {id: 'show-1', showType: 'service-worship', date: new Date(), shareId: 'share-9'} as any;
|
||||||
|
const songs = [{id: 'song-1'}] as any;
|
||||||
|
const expectedUrl = window.location.protocol + '//' + window.location.host + '/guest/share-9';
|
||||||
|
|
||||||
|
await expectAsync(service.share(show, songs)).toBeResolvedTo(expectedUrl);
|
||||||
|
|
||||||
|
expect(guestShowDataServiceSpy.update$).toHaveBeenCalledWith('share-9', {
|
||||||
|
showType: 'service-worship',
|
||||||
|
date: show.date,
|
||||||
|
songs,
|
||||||
|
});
|
||||||
|
expect(guestShowDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
|
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,14 +17,15 @@ export class GuestShowService {
|
|||||||
date: show.date,
|
date: show.date,
|
||||||
songs: songs,
|
songs: songs,
|
||||||
};
|
};
|
||||||
|
let shareId = show.shareId;
|
||||||
|
|
||||||
if (!show.shareId) {
|
if (!show.shareId) {
|
||||||
const shareId = await this.guestShowDataService.add(data);
|
shareId = await this.guestShowDataService.add(data);
|
||||||
await this.showService.update$(show.id, {shareId});
|
await this.showService.update$(show.id, {shareId});
|
||||||
} else {
|
} else {
|
||||||
await this.guestShowDataService.update$(show.shareId, data);
|
await this.guestShowDataService.update$(show.shareId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.location.protocol + '//' + window.location.host + '/guest/' + show.shareId;
|
return window.location.protocol + '//' + window.location.host + '/guest/' + shareId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {ShareDialogComponent} from './share-dialog.component';
|
import {ShareDialogComponent} from './share-dialog.component';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
describe('ShareDialogComponent', () => {
|
describe('ShareDialogComponent', () => {
|
||||||
let component: ShareDialogComponent;
|
let component: ShareDialogComponent;
|
||||||
let fixture: ComponentFixture<ShareDialogComponent>;
|
let fixture: ComponentFixture<ShareDialogComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
spyOn(QRCode, 'toDataURL').and.resolveTo('data:image/jpeg;base64,test');
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ShareDialogComponent],
|
imports: [ShareDialogComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -1,12 +1,115 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {firstValueFrom, of, Subject} from 'rxjs';
|
||||||
|
import {skip, take} from 'rxjs/operators';
|
||||||
|
import {DbService} from '../../../services/db.service';
|
||||||
import {ShowDataService} from './show-data.service';
|
import {ShowDataService} from './show-data.service';
|
||||||
|
|
||||||
describe('ShowDataService', () => {
|
describe('ShowDataService', () => {
|
||||||
beforeEach(() => void TestBed.configureTestingModule({}));
|
let service: ShowDataService;
|
||||||
|
let shows$: Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>;
|
||||||
|
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docSpy: jasmine.Spy;
|
||||||
|
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
||||||
|
let colSpy: jasmine.Spy;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
shows$ = new Subject<Array<{id: string; date: {toMillis: () => number}; archived?: boolean}>>();
|
||||||
|
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
docSpy = jasmine.createSpy('doc').and.returnValue({update: docUpdateSpy});
|
||||||
|
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'show-3'});
|
||||||
|
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||||
|
dbServiceSpy.col$.and.returnValue(shows$.asObservable());
|
||||||
|
dbServiceSpy.doc$.and.returnValue(of(null));
|
||||||
|
dbServiceSpy.doc.and.callFake(docSpy);
|
||||||
|
dbServiceSpy.col.and.callFake(colSpy);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(ShowDataService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: ShowDataService = TestBed.inject(ShowDataService);
|
expect(service).toBeTruthy();
|
||||||
void expect(service).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
it('should load the raw show list from the shows collection on creation', () => {
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort the list by ascending show date', async () => {
|
||||||
|
const listPromise = firstValueFrom(service.list$.pipe(take(1)));
|
||||||
|
|
||||||
|
shows$.next([
|
||||||
|
{id: 'show-2', date: {toMillis: () => 200}},
|
||||||
|
{id: 'show-1', date: {toMillis: () => 100}},
|
||||||
|
{id: 'show-3', date: {toMillis: () => 300}},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await listPromise;
|
||||||
|
|
||||||
|
expect(list.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replay the latest sorted list to late subscribers', async () => {
|
||||||
|
const initialSubscription = service.list$.subscribe();
|
||||||
|
|
||||||
|
shows$.next([
|
||||||
|
{id: 'show-2', date: {toMillis: () => 200}},
|
||||||
|
{id: 'show-1', date: {toMillis: () => 100}},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const replayedList = await firstValueFrom(service.list$.pipe(take(1)));
|
||||||
|
|
||||||
|
expect(replayedList.map(show => show.id)).toEqual(['show-1', 'show-2']);
|
||||||
|
|
||||||
|
initialSubscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose the raw list without sorting via listRaw$', () => {
|
||||||
|
service.listRaw$().subscribe();
|
||||||
|
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should request only published recent shows and filter archived entries', async () => {
|
||||||
|
const publicShows$ = of([
|
||||||
|
{id: 'show-1', archived: false},
|
||||||
|
{id: 'show-2', archived: true},
|
||||||
|
{id: 'show-3'},
|
||||||
|
]);
|
||||||
|
dbServiceSpy.col$.and.returnValue(publicShows$ as never);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.listPublicSince$(3));
|
||||||
|
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows', jasmine.any(Array));
|
||||||
|
const [, queryConstraints] = dbServiceSpy.col$.calls.mostRecent().args as [string, unknown[]];
|
||||||
|
expect(queryConstraints.length).toBe(3);
|
||||||
|
expect(result.map(show => show.id)).toEqual(['show-1', 'show-3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read a single show by id', () => {
|
||||||
|
service.read$('show-7').subscribe();
|
||||||
|
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('shows/show-7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a show at the expected document path', async () => {
|
||||||
|
await service.update('show-8', {archived: true});
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('shows/show-8');
|
||||||
|
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(updatePayload).toEqual({archived: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a show to the shows collection and return the new id', async () => {
|
||||||
|
await expectAsync(service.add({published: true})).toBeResolvedTo('show-3');
|
||||||
|
|
||||||
|
expect(colSpy).toHaveBeenCalledWith('shows');
|
||||||
|
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(addPayload).toEqual({published: true});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Observable} from 'rxjs';
|
|||||||
import {DbService} from '../../../services/db.service';
|
import {DbService} from '../../../services/db.service';
|
||||||
import {Show} from './show';
|
import {Show} from './show';
|
||||||
import {map, shareReplay} from 'rxjs/operators';
|
import {map, shareReplay} from 'rxjs/operators';
|
||||||
import {orderBy, QueryConstraint, Timestamp, where} from '@angular/fire/firestore';
|
import {orderBy, QueryConstraint, Timestamp, where} from 'firebase/firestore';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
|||||||
@@ -1,16 +1,98 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {DbService} from '../../../services/db.service';
|
||||||
import {ShowSongDataService} from './show-song-data.service';
|
import {ShowSongDataService} from './show-song-data.service';
|
||||||
|
|
||||||
describe('ShowSongDataService', () => {
|
describe('ShowSongDataService', () => {
|
||||||
let service: ShowSongDataService;
|
let service: ShowSongDataService;
|
||||||
|
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docSpy: jasmine.Spy;
|
||||||
|
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
||||||
|
let colSpy: jasmine.Spy;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
void TestBed.configureTestingModule({});
|
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||||
|
docSpy = jasmine.createSpy('doc').and.returnValue({
|
||||||
|
update: docUpdateSpy,
|
||||||
|
delete: docDeleteSpy,
|
||||||
|
});
|
||||||
|
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'show-song-3'});
|
||||||
|
colSpy = jasmine.createSpy('col').and.returnValue({add: colAddSpy});
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||||
|
dbServiceSpy.col$.and.callFake(() => of([{id: 'show-song-1'}]) as never);
|
||||||
|
dbServiceSpy.doc$.and.returnValue(of({id: 'show-song-1'}) as never);
|
||||||
|
dbServiceSpy.doc.and.callFake(docSpy);
|
||||||
|
dbServiceSpy.col.and.callFake(colSpy);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
service = TestBed.inject(ShowSongDataService);
|
service = TestBed.inject(ShowSongDataService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache the list observable per show id when no query constraints are passed', () => {
|
||||||
|
const first = service.list$('show-1');
|
||||||
|
const second = service.list$('show-1');
|
||||||
|
|
||||||
|
expect(first).toBe(second);
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows/show-1/songs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reuse the cache when query constraints are passed', () => {
|
||||||
|
const constraints = [{}] as never[];
|
||||||
|
|
||||||
|
const first = service.list$('show-1', constraints as never);
|
||||||
|
const second = service.list$('show-1', constraints as never);
|
||||||
|
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledTimes(2);
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows/show-1/songs', constraints as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep separate caches for different shows', () => {
|
||||||
|
service.list$('show-1');
|
||||||
|
service.list$('show-2');
|
||||||
|
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledTimes(2);
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows/show-1/songs');
|
||||||
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('shows/show-2/songs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read a single show song by nested document path', () => {
|
||||||
|
service.read$('show-4', 'song-5').subscribe();
|
||||||
|
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('shows/show-4/songs/song-5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a nested show song document', async () => {
|
||||||
|
await service.update$('show-4', 'song-5', {title: 'Updated'} as never);
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('shows/show-4/songs/song-5');
|
||||||
|
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(updatePayload).toEqual({title: 'Updated'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a nested show song document', async () => {
|
||||||
|
await service.delete('show-4', 'song-5');
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('shows/show-4/songs/song-5');
|
||||||
|
expect(docDeleteSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a song to the nested show songs collection and return the id', async () => {
|
||||||
|
await expectAsync(service.add('show-4', {songId: 'song-5'} as never)).toBeResolvedTo('show-song-3');
|
||||||
|
|
||||||
|
expect(colSpy).toHaveBeenCalledWith('shows/show-4/songs');
|
||||||
|
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(addPayload).toEqual({songId: 'song-5'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,131 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
|
import {SongDataService} from '../../songs/services/song-data.service';
|
||||||
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
import {ShowService} from './show.service';
|
||||||
|
import {ShowSongDataService} from './show-song-data.service';
|
||||||
import {ShowSongService} from './show-song.service';
|
import {ShowSongService} from './show-song.service';
|
||||||
|
|
||||||
describe('ShowSongService', () => {
|
describe('ShowSongService', () => {
|
||||||
let service: ShowSongService;
|
let service: ShowSongService;
|
||||||
|
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
||||||
|
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
||||||
|
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||||
|
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||||
|
let user$: BehaviorSubject<any>;
|
||||||
|
const song = {id: 'song-1', key: 'G', title: 'Amazing Grace'} as any;
|
||||||
|
const showSong = {id: 'show-song-1', songId: 'song-1'} as any;
|
||||||
|
const show = {id: 'show-1', order: ['show-song-1', 'show-song-2']} as any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
void TestBed.configureTestingModule({});
|
user$ = new BehaviorSubject<any>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'letters', songUsage: {}});
|
||||||
|
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['add', 'read$', 'list$', 'delete', 'update$']);
|
||||||
|
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$']);
|
||||||
|
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
||||||
|
user$: user$.asObservable(),
|
||||||
|
});
|
||||||
|
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['read$', 'update$']);
|
||||||
|
|
||||||
|
showSongDataServiceSpy.add.and.resolveTo('show-song-2');
|
||||||
|
showSongDataServiceSpy.read$.and.returnValue(of(showSong));
|
||||||
|
showSongDataServiceSpy.list$.and.returnValue(of([showSong]));
|
||||||
|
showSongDataServiceSpy.delete.and.resolveTo();
|
||||||
|
showSongDataServiceSpy.update$.and.resolveTo();
|
||||||
|
songDataServiceSpy.read$.and.returnValue(of(song));
|
||||||
|
userServiceSpy.incSongCount.and.resolveTo();
|
||||||
|
userServiceSpy.decSongCount.and.resolveTo();
|
||||||
|
showServiceSpy.read$.and.returnValue(of(show));
|
||||||
|
showServiceSpy.update$.and.resolveTo();
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: ShowSongDataService, useValue: showSongDataServiceSpy},
|
||||||
|
{provide: SongDataService, useValue: songDataServiceSpy},
|
||||||
|
{provide: UserService, useValue: userServiceSpy},
|
||||||
|
{provide: ShowService, useValue: showServiceSpy},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
service = TestBed.inject(ShowSongService);
|
service = TestBed.inject(ShowSongService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
void expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a show song from the song and current user', async () => {
|
||||||
|
await expectAsync(service.new$('show-1', 'song-1', true)).toBeResolvedTo('show-song-2');
|
||||||
|
|
||||||
|
expect(userServiceSpy.incSongCount).toHaveBeenCalledWith('song-1');
|
||||||
|
expect(showSongDataServiceSpy.add).toHaveBeenCalledWith('show-1', {
|
||||||
|
...song,
|
||||||
|
songId: 'song-1',
|
||||||
|
key: 'G',
|
||||||
|
keyOriginal: 'G',
|
||||||
|
chordMode: 'letters',
|
||||||
|
addedLive: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when the song is missing', async () => {
|
||||||
|
songDataServiceSpy.read$.and.returnValue(of(null));
|
||||||
|
|
||||||
|
await expectAsync(service.new$('show-1', 'song-1')).toBeResolvedTo(null);
|
||||||
|
|
||||||
|
expect(showSongDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
|
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when the current user is missing', async () => {
|
||||||
|
user$.next(null);
|
||||||
|
|
||||||
|
await expectAsync(service.new$('show-1', 'song-1')).toBeResolvedTo(null);
|
||||||
|
|
||||||
|
expect(showSongDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
|
expect(userServiceSpy.incSongCount).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate reads to the data service', async () => {
|
||||||
|
await expectAsync(service.read('show-1', 'show-song-1')).toBeResolvedTo(showSong);
|
||||||
|
expect(showSongDataServiceSpy.read$).toHaveBeenCalledWith('show-1', 'show-song-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate list access to the data service', async () => {
|
||||||
|
await expectAsync(service.list('show-1')).toBeResolvedTo([showSong]);
|
||||||
|
expect(showSongDataServiceSpy.list$).toHaveBeenCalledWith('show-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a show song, update the order and decrement song usage', async () => {
|
||||||
|
await service.delete$('show-1', 'show-song-1', 0);
|
||||||
|
|
||||||
|
expect(showSongDataServiceSpy.delete).toHaveBeenCalledWith('show-1', 'show-song-1');
|
||||||
|
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {order: ['show-song-2']});
|
||||||
|
expect(userServiceSpy.decSongCount).toHaveBeenCalledWith('song-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop delete when the show is missing', async () => {
|
||||||
|
showServiceSpy.read$.and.returnValue(of(null));
|
||||||
|
|
||||||
|
await service.delete$('show-1', 'show-song-1', 0);
|
||||||
|
|
||||||
|
expect(showSongDataServiceSpy.delete).not.toHaveBeenCalled();
|
||||||
|
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
|
expect(userServiceSpy.decSongCount).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop delete when the show song is missing', async () => {
|
||||||
|
showSongDataServiceSpy.read$.and.returnValue(of(null));
|
||||||
|
|
||||||
|
await service.delete$('show-1', 'show-song-1', 0);
|
||||||
|
|
||||||
|
expect(showSongDataServiceSpy.delete).not.toHaveBeenCalled();
|
||||||
|
expect(showServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
|
expect(userServiceSpy.decSongCount).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate updates to the data service', async () => {
|
||||||
|
await service.update$('show-1', 'show-song-1', {title: 'Updated'} as never);
|
||||||
|
|
||||||
|
expect(showSongDataServiceSpy.update$).toHaveBeenCalledWith('show-1', 'show-song-1', {title: 'Updated'} as never);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,102 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
import {ShowService} from './show.service';
|
|
||||||
import {ShowDataService} from './show-data.service';
|
import {ShowDataService} from './show-data.service';
|
||||||
|
import {ShowService} from './show.service';
|
||||||
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
|
||||||
describe('ShowService', () => {
|
describe('ShowService', () => {
|
||||||
const mockShowDataService = {add: Promise.resolve(null)};
|
let service: ShowService;
|
||||||
beforeEach(
|
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||||
() =>
|
let user$: BehaviorSubject<unknown>;
|
||||||
void TestBed.configureTestingModule({
|
const shows = [
|
||||||
providers: [{provide: ShowDataService, useValue: mockShowDataService}],
|
{id: 'show-1', owner: 'user-1', published: false, archived: false},
|
||||||
})
|
{id: 'show-2', owner: 'other-user', published: true, archived: false},
|
||||||
);
|
{id: 'show-3', owner: 'user-1', published: true, archived: true},
|
||||||
|
] as never;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
||||||
|
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], {
|
||||||
|
list$: of(shows),
|
||||||
|
});
|
||||||
|
showDataServiceSpy.read$.and.returnValue(of(shows[0]));
|
||||||
|
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
|
||||||
|
showDataServiceSpy.update.and.resolveTo();
|
||||||
|
showDataServiceSpy.add.and.resolveTo('new-show-id');
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{provide: ShowDataService, useValue: showDataServiceSpy},
|
||||||
|
{provide: UserService, useValue: {user$: user$.asObservable()}},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(ShowService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list published shows and own drafts, but exclude archived ones', done => {
|
||||||
|
service.list$().subscribe(result => {
|
||||||
|
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out private drafts when publishedOnly is true', done => {
|
||||||
|
service.list$(true).subscribe(result => {
|
||||||
|
expect(result.map(show => show.id)).toEqual(['show-2']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate public listing to the data service', done => {
|
||||||
|
service.listPublicSince$(6).subscribe(result => {
|
||||||
|
expect(result).toEqual([shows[1]]);
|
||||||
|
expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate reads to the data service', done => {
|
||||||
|
service.read$('show-1').subscribe(result => {
|
||||||
|
expect(result).toEqual(shows[0]);
|
||||||
|
expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate updates to the data service', async () => {
|
||||||
|
await service.update$('show-1', {published: true});
|
||||||
|
|
||||||
|
expect(showDataServiceSpy.update).toHaveBeenCalledWith('show-1', {published: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when creating a show without showType', async () => {
|
||||||
|
await expectAsync(service.new$({published: true})).toBeResolvedTo(null);
|
||||||
|
|
||||||
|
expect(showDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no user is available for show creation', async () => {
|
||||||
|
user$.next(null);
|
||||||
|
|
||||||
|
await expectAsync(service.new$({showType: 'misc-public'})).toBeResolvedTo(null);
|
||||||
|
|
||||||
|
expect(showDataServiceSpy.add).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
ShowService.SHOW_TYPE_PUBLIC.forEach(type => {
|
ShowService.SHOW_TYPE_PUBLIC.forEach(type => {
|
||||||
it('should calc public flag for ' + type, async () => {
|
it('should calc public flag for ' + type, async () => {
|
||||||
const service: ShowService = TestBed.inject(ShowService);
|
|
||||||
const addSpy = spyOn(TestBed.inject(ShowDataService), 'add').and.returnValue(Promise.resolve('id'));
|
|
||||||
|
|
||||||
const id = await service.new$({showType: type});
|
const id = await service.new$({showType: type});
|
||||||
|
|
||||||
void expect(id).toBe('id');
|
expect(id).toBe('new-show-id');
|
||||||
void expect(addSpy).toHaveBeenCalledWith({
|
expect(showDataServiceSpy.add).toHaveBeenCalledWith({
|
||||||
showType: type,
|
showType: type,
|
||||||
|
owner: 'user-1',
|
||||||
|
order: [],
|
||||||
public: true,
|
public: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -29,14 +104,13 @@ describe('ShowService', () => {
|
|||||||
|
|
||||||
ShowService.SHOW_TYPE_PRIVATE.forEach(type => {
|
ShowService.SHOW_TYPE_PRIVATE.forEach(type => {
|
||||||
it('should calc private flag for ' + type, async () => {
|
it('should calc private flag for ' + type, async () => {
|
||||||
const service: ShowService = TestBed.inject(ShowService);
|
|
||||||
const addSpy = spyOn(TestBed.inject(ShowDataService), 'add').and.returnValue(Promise.resolve('id'));
|
|
||||||
|
|
||||||
const id = await service.new$({showType: type});
|
const id = await service.new$({showType: type});
|
||||||
|
|
||||||
void expect(id).toBe('id');
|
expect(id).toBe('new-show-id');
|
||||||
void expect(addSpy).toHaveBeenCalledWith({
|
expect(showDataServiceSpy.add).toHaveBeenCalledWith({
|
||||||
showType: type,
|
showType: type,
|
||||||
|
owner: 'user-1',
|
||||||
|
order: [],
|
||||||
public: false,
|
public: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,68 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
import {DbService} from '../../../services/db.service';
|
||||||
import {FileDataService} from './file-data.service';
|
import {FileDataService} from './file-data.service';
|
||||||
|
|
||||||
describe('FileDataService', () => {
|
describe('FileDataService', () => {
|
||||||
beforeEach(() => void TestBed.configureTestingModule({}));
|
let service: FileDataService;
|
||||||
|
let filesCollectionValueChangesSpy: jasmine.Spy;
|
||||||
|
let filesCollectionAddSpy: jasmine.Spy;
|
||||||
|
let songDocCollectionSpy: jasmine.Spy;
|
||||||
|
let songDocSpy: jasmine.Spy;
|
||||||
|
let fileDeleteSpy: jasmine.Spy;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
filesCollectionValueChangesSpy = jasmine.createSpy('valueChanges').and.returnValue(of([{id: 'file-1', name: 'plan.pdf'}]));
|
||||||
|
filesCollectionAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'file-2'});
|
||||||
|
songDocCollectionSpy = jasmine.createSpy('collection').and.returnValue({
|
||||||
|
add: filesCollectionAddSpy,
|
||||||
|
valueChanges: filesCollectionValueChangesSpy,
|
||||||
|
});
|
||||||
|
songDocSpy = jasmine.createSpy('songDoc').and.callFake((path: string) => {
|
||||||
|
if (path.includes('/files/')) {
|
||||||
|
return {delete: fileDeleteSpy};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {collection: songDocCollectionSpy};
|
||||||
|
});
|
||||||
|
fileDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
|
||||||
|
dbServiceSpy.doc.and.callFake(songDocSpy);
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(FileDataService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: FileDataService = TestBed.inject(FileDataService);
|
expect(service).toBeTruthy();
|
||||||
void expect(service).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
it('should add files to the song files subcollection and return the generated id', async () => {
|
||||||
|
const file = {name: 'setlist.pdf', path: 'songs/song-1/files', createdAt: new Date()};
|
||||||
|
|
||||||
|
await expectAsync(service.set('song-1', file)).toBeResolvedTo('file-2');
|
||||||
|
|
||||||
|
expect(songDocSpy).toHaveBeenCalledWith('songs/song-1');
|
||||||
|
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
||||||
|
expect(filesCollectionAddSpy).toHaveBeenCalledWith(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete files from the nested file document path', async () => {
|
||||||
|
await service.delete('song-4', 'file-9');
|
||||||
|
|
||||||
|
expect(songDocSpy).toHaveBeenCalledWith('songs/song-4/files/file-9');
|
||||||
|
expect(fileDeleteSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read files from the files subcollection with the id field attached', () => {
|
||||||
|
service.read$('song-3').subscribe();
|
||||||
|
|
||||||
|
expect(songDocSpy).toHaveBeenCalledWith('songs/song-3');
|
||||||
|
expect(songDocCollectionSpy).toHaveBeenCalledWith('files');
|
||||||
|
expect(filesCollectionValueChangesSpy).toHaveBeenCalledWith({idField: 'id'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Injectable, inject} from '@angular/core';
|
import {EnvironmentInjector, Injectable, inject, 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';
|
||||||
@@ -9,13 +9,14 @@ import {FileDataService} from './file-data.service';
|
|||||||
export class FileService {
|
export class FileService {
|
||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
private fileDataService = inject(FileDataService);
|
private fileDataService = inject(FileDataService);
|
||||||
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public getDownloadUrl(path: string): Observable<string> {
|
public getDownloadUrl(path: string): Observable<string> {
|
||||||
return from(getDownloadURL(ref(this.storage, path)));
|
return from(runInInjectionContext(this.environmentInjector, () => getDownloadURL(ref(this.storage, path))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(path: string, songId: string, fileId: string): void {
|
public delete(path: string, songId: string, fileId: string): void {
|
||||||
void deleteObject(ref(this.storage, path));
|
void runInInjectionContext(this.environmentInjector, () => deleteObject(ref(this.storage, path)));
|
||||||
void this.fileDataService.delete(songId, fileId);
|
void this.fileDataService.delete(songId, fileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,104 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {firstValueFrom, Subject} from 'rxjs';
|
||||||
import {SongDataService} from './song-data.service';
|
import {skip, take, toArray} from 'rxjs/operators';
|
||||||
import {firstValueFrom, of} from 'rxjs';
|
|
||||||
import {DbService} from '../../../services/db.service';
|
import {DbService} from '../../../services/db.service';
|
||||||
|
import {SongDataService} from './song-data.service';
|
||||||
|
|
||||||
describe('SongDataService', () => {
|
describe('SongDataService', () => {
|
||||||
const songs = [{title: 'title1'}];
|
let service: SongDataService;
|
||||||
|
let songs$: Subject<Array<{id: string; title: string}>>;
|
||||||
|
let docUpdateSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docDeleteSpy: jasmine.Spy<() => Promise<void>>;
|
||||||
|
let docSpy: jasmine.Spy;
|
||||||
|
let colAddSpy: jasmine.Spy<() => Promise<{id: string}>>;
|
||||||
|
let colSpy: jasmine.Spy;
|
||||||
|
let dbServiceSpy: jasmine.SpyObj<DbService>;
|
||||||
|
|
||||||
const mockDbService = {
|
beforeEach(() => {
|
||||||
col$: () => of(songs),
|
songs$ = new Subject<Array<{id: string; title: string}>>();
|
||||||
};
|
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
|
||||||
|
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
|
||||||
|
docSpy = jasmine.createSpy('doc').and.callFake(() => ({
|
||||||
|
update: docUpdateSpy,
|
||||||
|
delete: docDeleteSpy,
|
||||||
|
}));
|
||||||
|
colAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'song-3'});
|
||||||
|
colSpy = jasmine.createSpy('col').and.returnValue({
|
||||||
|
add: colAddSpy,
|
||||||
|
});
|
||||||
|
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['col$', 'doc$', 'doc', 'col']);
|
||||||
|
dbServiceSpy.col$.and.returnValue(songs$.asObservable());
|
||||||
|
dbServiceSpy.doc$.and.returnValue(songs$.asObservable() as never);
|
||||||
|
dbServiceSpy.doc.and.callFake(docSpy);
|
||||||
|
dbServiceSpy.col.and.callFake(colSpy);
|
||||||
|
|
||||||
beforeEach(
|
void TestBed.configureTestingModule({
|
||||||
() =>
|
providers: [{provide: DbService, useValue: dbServiceSpy}],
|
||||||
void TestBed.configureTestingModule({
|
});
|
||||||
providers: [{provide: DbService, useValue: mockDbService}],
|
|
||||||
})
|
service = TestBed.inject(SongDataService);
|
||||||
);
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: SongDataService = TestBed.inject(SongDataService);
|
expect(service).toBeTruthy();
|
||||||
void expect(service).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list songs', async () => {
|
it('should load songs from the songs collection once on creation', () => {
|
||||||
const service: SongDataService = TestBed.inject(SongDataService);
|
expect(dbServiceSpy.col$).toHaveBeenCalledTimes(1);
|
||||||
const list = await firstValueFrom(service.list$);
|
expect(dbServiceSpy.col$).toHaveBeenCalledWith('songs');
|
||||||
void expect(list[0].title).toEqual('title1');
|
});
|
||||||
|
|
||||||
|
it('should emit an empty list first and then the loaded songs', async () => {
|
||||||
|
const emissionsPromise = firstValueFrom(service.list$.pipe(take(2), toArray()));
|
||||||
|
|
||||||
|
songs$.next([{id: 'song-1', title: 'Amazing Grace'}]);
|
||||||
|
|
||||||
|
const emissions = await emissionsPromise;
|
||||||
|
|
||||||
|
expect(emissions[0]).toEqual([]);
|
||||||
|
expect(emissions[1].map(song => song.id)).toEqual(['song-1']);
|
||||||
|
expect(emissions[1].map(song => song.title)).toEqual(['Amazing Grace']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replay the latest songs to late subscribers', async () => {
|
||||||
|
const initialSubscription = service.list$.subscribe();
|
||||||
|
|
||||||
|
songs$.next([{id: 'song-2', title: 'How Great'}]);
|
||||||
|
|
||||||
|
const latestSongs = await firstValueFrom(service.list$.pipe(take(1)));
|
||||||
|
|
||||||
|
expect(latestSongs.map(song => song.id)).toEqual(['song-2']);
|
||||||
|
expect(latestSongs.map(song => song.title)).toEqual(['How Great']);
|
||||||
|
|
||||||
|
initialSubscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read a single song by id', () => {
|
||||||
|
service.read$('song-7');
|
||||||
|
|
||||||
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('songs/song-7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a song at the expected document path', async () => {
|
||||||
|
await service.update$('song-8', {title: 'Updated'});
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('songs/song-8');
|
||||||
|
const [updatePayload] = docUpdateSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(updatePayload).toEqual({title: 'Updated'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a song to the songs collection and return the new id', async () => {
|
||||||
|
await expectAsync(service.add({title: 'New Song'})).toBeResolvedTo('song-3');
|
||||||
|
|
||||||
|
expect(colSpy).toHaveBeenCalledWith('songs');
|
||||||
|
const [addPayload] = colAddSpy.calls.mostRecent().args as unknown as [Record<string, unknown>];
|
||||||
|
expect(addPayload).toEqual({title: 'New Song'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a song at the expected document path', async () => {
|
||||||
|
await service.delete('song-9');
|
||||||
|
|
||||||
|
expect(docSpy).toHaveBeenCalledWith('songs/song-9');
|
||||||
|
expect(docDeleteSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,32 +1,100 @@
|
|||||||
import {TestBed, waitForAsync} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {SongService} from './song.service';
|
|
||||||
import {SongDataService} from './song-data.service';
|
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
|
import {SongDataService} from './song-data.service';
|
||||||
|
import {SongService} from './song.service';
|
||||||
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
import {Timestamp} from '@angular/fire/firestore';
|
||||||
|
|
||||||
describe('SongService', () => {
|
describe('SongService', () => {
|
||||||
const songs = [{title: 'title1'}];
|
let service: SongService;
|
||||||
|
let songDataServiceSpy: jasmine.SpyObj<SongDataService>;
|
||||||
|
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||||
|
const song = {
|
||||||
|
id: 'song-1',
|
||||||
|
title: 'Amazing Grace',
|
||||||
|
edits: [],
|
||||||
|
} as never;
|
||||||
|
|
||||||
const mockSongDataService = {
|
beforeEach(() => {
|
||||||
list: () => of(songs),
|
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$', 'update$', 'add', 'delete'], {
|
||||||
};
|
list$: of([song]),
|
||||||
|
});
|
||||||
|
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['currentUser']);
|
||||||
|
|
||||||
beforeEach(
|
songDataServiceSpy.read$.and.returnValue(of(song));
|
||||||
() =>
|
songDataServiceSpy.update$.and.resolveTo();
|
||||||
void TestBed.configureTestingModule({
|
songDataServiceSpy.add.and.resolveTo('song-2');
|
||||||
providers: [{provide: SongDataService, useValue: mockSongDataService}],
|
songDataServiceSpy.delete.and.resolveTo();
|
||||||
})
|
userServiceSpy.currentUser.and.resolveTo({name: 'Benjamin'} as never);
|
||||||
);
|
|
||||||
|
|
||||||
it('should be created', () => {
|
void TestBed.configureTestingModule({
|
||||||
const service: SongService = TestBed.inject(SongService);
|
providers: [
|
||||||
void expect(service).toBeTruthy();
|
{provide: SongDataService, useValue: songDataServiceSpy},
|
||||||
|
{provide: UserService, useValue: userServiceSpy},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(SongService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list songs', waitForAsync(() => {
|
it('should be created', () => {
|
||||||
const service: SongService = TestBed.inject(SongService);
|
expect(service).toBeTruthy();
|
||||||
service.list$().subscribe(s => {
|
});
|
||||||
void expect(s[0].title).toEqual('title1');
|
|
||||||
|
it('should list songs from the data service', done => {
|
||||||
|
service.list$().subscribe(songs => {
|
||||||
|
expect(songs).toEqual([song]);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
it('should delegate reads to the data service', async () => {
|
||||||
|
await expectAsync(service.read('song-1')).toBeResolvedTo(song);
|
||||||
|
expect(songDataServiceSpy.read$).toHaveBeenCalledWith('song-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append an edit with the current user when updating a song', async () => {
|
||||||
|
const timestamp = {seconds: 1} as never;
|
||||||
|
spyOn(Timestamp, 'now').and.returnValue(timestamp);
|
||||||
|
|
||||||
|
await service.update$('song-1', {title: 'Updated'});
|
||||||
|
|
||||||
|
expect(songDataServiceSpy.update$).toHaveBeenCalled();
|
||||||
|
const [, payload] = songDataServiceSpy.update$.calls.mostRecent().args as unknown as [string, Record<string, unknown>];
|
||||||
|
expect(payload.title).toBe('Updated');
|
||||||
|
expect(payload.edits).toEqual([{username: 'Benjamin', timestamp}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update when the song does not exist', async () => {
|
||||||
|
songDataServiceSpy.read$.and.returnValue(of(null));
|
||||||
|
|
||||||
|
await service.update$('missing-song', {title: 'Updated'});
|
||||||
|
|
||||||
|
expect(songDataServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update when no current user is available', async () => {
|
||||||
|
userServiceSpy.currentUser.and.resolveTo(null);
|
||||||
|
|
||||||
|
await service.update$('song-1', {title: 'Updated'});
|
||||||
|
|
||||||
|
expect(songDataServiceSpy.update$).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new songs with the expected defaults', async () => {
|
||||||
|
await expectAsync(service.new(42, 'New Song')).toBeResolvedTo('song-2');
|
||||||
|
|
||||||
|
expect(songDataServiceSpy.add).toHaveBeenCalledWith({
|
||||||
|
number: 42,
|
||||||
|
title: 'New Song',
|
||||||
|
status: 'draft',
|
||||||
|
legalType: 'open',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete songs via the data service', async () => {
|
||||||
|
await service.delete('song-1');
|
||||||
|
|
||||||
|
expect(songDataServiceSpy.delete).toHaveBeenCalledWith('song-1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Cool bridge without any chords
|
|||||||
void expect(sections[1].lines[2].type).toBe(LineType.chord);
|
void expect(sections[1].lines[2].type).toBe(LineType.chord);
|
||||||
void expect(sections[1].lines[2].text).toBe(' a d e f g a h c b');
|
void expect(sections[1].lines[2].text).toBe(' a d e f g a h c b');
|
||||||
void expect(sections[2].lines[0].type).toBe(LineType.chord);
|
void expect(sections[2].lines[0].type).toBe(LineType.chord);
|
||||||
void expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e');
|
void expect(sections[2].lines[0].text).toBe('c c♯ d♭ c7 cmaj7 c/e');
|
||||||
|
|
||||||
// c c# db c7 cmaj7 c/e
|
// c c# db c7 cmaj7 c/e
|
||||||
void expect(sections[2].lines[0].chords).toEqual([
|
void expect(sections[2].lines[0].chords).toEqual([
|
||||||
@@ -93,7 +93,7 @@ g# F# E g# F# E
|
|||||||
text`;
|
text`;
|
||||||
const sections = service.parse(text, null);
|
const sections = service.parse(text, null);
|
||||||
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||||
void expect(sections[0].lines[0].text).toBe('g# F# E g# F# E');
|
void expect(sections[0].lines[0].text).toBe('g♯ F♯ E g♯ F♯ E');
|
||||||
void expect(sections[0].lines[1].type).toBe(LineType.text);
|
void expect(sections[0].lines[1].type).toBe(LineType.text);
|
||||||
void expect(sections[0].lines[1].text).toBe('text');
|
void expect(sections[0].lines[1].text).toBe('text');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('TransposeService', () => {
|
|||||||
|
|
||||||
void expect(distance).toBe(1);
|
void expect(distance).toBe(1);
|
||||||
void expect(map?.['H']).toBe('C');
|
void expect(map?.['H']).toBe('C');
|
||||||
void expect(map?.['B']).toBe('C#');
|
void expect(map?.['B']).toBe('H');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render unknown chords as X', () => {
|
it('should render unknown chords as X', () => {
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ type ScaleVariants = [string[], string[]];
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class TransposeService {
|
export class TransposeService {
|
||||||
|
private readonly enharmonicAliases: Array<[string, string]> = [
|
||||||
|
['C#', 'Db'],
|
||||||
|
['D#', 'Eb'],
|
||||||
|
['F#', 'Gb'],
|
||||||
|
['G#', 'Ab'],
|
||||||
|
['A#', 'B'],
|
||||||
|
['c#', 'db'],
|
||||||
|
['d#', 'eb'],
|
||||||
|
['f#', 'gb'],
|
||||||
|
['g#', 'ab'],
|
||||||
|
['a#', 'b'],
|
||||||
|
];
|
||||||
|
|
||||||
private readonly keyToSemitone: Record<string, number> = {
|
private readonly keyToSemitone: Record<string, number> = {
|
||||||
C: 0,
|
C: 0,
|
||||||
'C#': 1,
|
'C#': 1,
|
||||||
@@ -105,6 +118,15 @@ export class TransposeService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.enharmonicAliases.forEach(([left, right]) => {
|
||||||
|
if (map[left] && !map[right]) {
|
||||||
|
map[right] = map[left];
|
||||||
|
}
|
||||||
|
if (map[right] && !map[left]) {
|
||||||
|
map[left] = map[right];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.mapCache.set(cacheKey, map);
|
this.mapCache.set(cacheKey, map);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Injectable, inject} from '@angular/core';
|
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||||
import {Upload} from './upload';
|
import {Upload} from './upload';
|
||||||
import {FileDataService} from './file-data.service';
|
import {FileDataService} from './file-data.service';
|
||||||
import {ref, Storage, uploadBytesResumable} from '@angular/fire/storage';
|
import {ref, Storage, uploadBytesResumable} from '@angular/fire/storage';
|
||||||
@@ -11,14 +11,19 @@ import {FileServer} from './fileServer';
|
|||||||
export class UploadService extends FileBase {
|
export class UploadService extends FileBase {
|
||||||
private fileDataService = inject(FileDataService);
|
private fileDataService = inject(FileDataService);
|
||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public pushUpload(songId: string, upload: Upload): void {
|
public pushUpload(songId: string, upload: Upload): void {
|
||||||
const directory = this.directory(songId);
|
const directory = this.directory(songId);
|
||||||
const filePath = `${directory}/${upload.file.name}`;
|
const filePath = `${directory}/${upload.file.name}`;
|
||||||
upload.path = directory;
|
upload.path = directory;
|
||||||
|
|
||||||
const storageRef = ref(this.storage, filePath);
|
const {task} = runInInjectionContext(this.environmentInjector, () => {
|
||||||
const task = uploadBytesResumable(storageRef, upload.file);
|
const storageRef = ref(this.storage, filePath);
|
||||||
|
return {
|
||||||
|
task: uploadBytesResumable(storageRef, upload.file),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
task.on(
|
task.on(
|
||||||
'state_changed',
|
'state_changed',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe('SongListComponent', () => {
|
|||||||
const songs = [{title: 'title1'}];
|
const songs = [{title: 'title1'}];
|
||||||
|
|
||||||
const mockSongService = {
|
const mockSongService = {
|
||||||
list: () => of(songs),
|
list$: () => of(songs),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe('SaveDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
void TestBed.configureTestingModule({
|
void TestBed.configureTestingModule({
|
||||||
declarations: [SaveDialogComponent],
|
imports: [SaveDialogComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Component, Input, inject} from '@angular/core';
|
import {Component, EnvironmentInjector, Input, inject, runInInjectionContext} from '@angular/core';
|
||||||
import {File} from '../../services/file';
|
import {File} from '../../services/file';
|
||||||
import {getDownloadURL, ref, Storage} from '@angular/fire/storage';
|
import {getDownloadURL, ref, Storage} from '@angular/fire/storage';
|
||||||
import {from, Observable} from 'rxjs';
|
import {from, Observable} from 'rxjs';
|
||||||
@@ -12,13 +12,14 @@ import {AsyncPipe} from '@angular/common';
|
|||||||
})
|
})
|
||||||
export class FileComponent {
|
export class FileComponent {
|
||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public url$: Observable<string> | null = null;
|
public url$: Observable<string> | null = null;
|
||||||
public name = '';
|
public name = '';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public set file(file: File) {
|
public set file(file: File) {
|
||||||
this.url$ = from(getDownloadURL(ref(this.storage, file.path + '/' + file.name)));
|
this.url$ = from(runInInjectionContext(this.environmentInjector, () => getDownloadURL(ref(this.storage, file.path + '/' + file.name))));
|
||||||
this.name = file.name;
|
this.name = file.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Injectable, inject} from '@angular/core';
|
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
addDoc,
|
addDoc,
|
||||||
collection,
|
collection,
|
||||||
@@ -25,7 +25,8 @@ type DocumentPredicate<T> = string | DbDocument<T>;
|
|||||||
export class DbCollection<T> {
|
export class DbCollection<T> {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly fs: Firestore,
|
private readonly fs: Firestore,
|
||||||
private readonly path: string
|
private readonly path: string,
|
||||||
|
private readonly environmentInjector: EnvironmentInjector
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public add(data: Partial<T>): Promise<DocumentReference<T>> {
|
public add(data: Partial<T>): Promise<DocumentReference<T>> {
|
||||||
@@ -33,7 +34,7 @@ export class DbCollection<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public valueChanges(options?: {idField?: string}): Observable<T[]> {
|
public valueChanges(options?: {idField?: string}): Observable<T[]> {
|
||||||
return collectionData(this.ref, options as {idField?: never}) as Observable<T[]>;
|
return runInInjectionContext(this.environmentInjector, () => collectionData(this.ref, options as {idField?: never}) as Observable<T[]>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get ref(): CollectionReference<DocumentData> {
|
private get ref(): CollectionReference<DocumentData> {
|
||||||
@@ -44,7 +45,8 @@ export class DbCollection<T> {
|
|||||||
export class DbDocument<T> {
|
export class DbDocument<T> {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly fs: Firestore,
|
private readonly fs: Firestore,
|
||||||
private readonly path: string
|
private readonly path: string,
|
||||||
|
private readonly environmentInjector: EnvironmentInjector
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public set(data: Partial<T>): Promise<void> {
|
public set(data: Partial<T>): Promise<void> {
|
||||||
@@ -60,11 +62,14 @@ export class DbDocument<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public collection<U>(subPath: string): DbCollection<U> {
|
public collection<U>(subPath: string): DbCollection<U> {
|
||||||
return new DbCollection<U>(this.fs, `${this.path}/${subPath}`);
|
return new DbCollection<U>(this.fs, `${this.path}/${subPath}`, this.environmentInjector);
|
||||||
}
|
}
|
||||||
|
|
||||||
public valueChanges(options?: {idField?: string}): Observable<(NonNullable<T> & {id?: string}) | undefined> {
|
public valueChanges(options?: {idField?: string}): Observable<(NonNullable<T> & {id?: string}) | undefined> {
|
||||||
return docData(this.ref as DocumentReference<T>, options as {idField?: never}) as Observable<(NonNullable<T> & {id?: string}) | undefined>;
|
return runInInjectionContext(
|
||||||
|
this.environmentInjector,
|
||||||
|
() => docData(this.ref as DocumentReference<T>, options as {idField?: never}) as Observable<(NonNullable<T> & {id?: string}) | undefined>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get ref(): DocumentReference<DocumentData> {
|
private get ref(): DocumentReference<DocumentData> {
|
||||||
@@ -77,13 +82,14 @@ export class DbDocument<T> {
|
|||||||
})
|
})
|
||||||
export class DbService {
|
export class DbService {
|
||||||
private fs = inject(Firestore);
|
private fs = inject(Firestore);
|
||||||
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public col<T>(ref: CollectionPredicate<T>): DbCollection<T> {
|
public col<T>(ref: CollectionPredicate<T>): DbCollection<T> {
|
||||||
return typeof ref === 'string' ? new DbCollection<T>(this.fs, ref) : ref;
|
return typeof ref === 'string' ? new DbCollection<T>(this.fs, ref, this.environmentInjector) : ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
public doc<T>(ref: DocumentPredicate<T>): DbDocument<T> {
|
public doc<T>(ref: DocumentPredicate<T>): DbDocument<T> {
|
||||||
return typeof ref === 'string' ? new DbDocument<T>(this.fs, ref) : ref;
|
return typeof ref === 'string' ? new DbDocument<T>(this.fs, ref, this.environmentInjector) : ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
public doc$<T>(ref: DocumentPredicate<T>): Observable<(NonNullable<T> & {id?: string}) | null> {
|
public doc$<T>(ref: DocumentPredicate<T>): Observable<(NonNullable<T> & {id?: string}) | null> {
|
||||||
@@ -98,6 +104,6 @@ export class DbService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const q = query(collection(this.fs, ref), ...queryConstraints);
|
const q = query(collection(this.fs, ref), ...queryConstraints);
|
||||||
return collectionData(q, {idField: 'id'}) as Observable<T[]>;
|
return runInInjectionContext(this.environmentInjector, () => collectionData(q, {idField: 'id'}) as Observable<T[]>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Injectable, inject} from '@angular/core';
|
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||||
import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth';
|
import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth';
|
||||||
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs';
|
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs';
|
||||||
import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators';
|
import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators';
|
||||||
@@ -25,6 +25,7 @@ export class UserService {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private showDataService = inject(ShowDataService);
|
private showDataService = inject(ShowDataService);
|
||||||
private showSongDataService = inject(ShowSongDataService);
|
private showSongDataService = inject(ShowSongDataService);
|
||||||
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public users$ = this.db.col$<User>('users').pipe(shareReplay({bufferSize: 1, refCount: true}));
|
public users$ = this.db.col$<User>('users').pipe(shareReplay({bufferSize: 1, refCount: true}));
|
||||||
private iUserId$ = new BehaviorSubject<string | null>(null);
|
private iUserId$ = new BehaviorSubject<string | null>(null);
|
||||||
@@ -32,7 +33,7 @@ export class UserService {
|
|||||||
private userByIdCache = new Map<string, Observable<User | null>>();
|
private userByIdCache = new Map<string, Observable<User | null>>();
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
authState(this.auth)
|
this.authState$()
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(auth => !!auth),
|
filter(auth => !!auth),
|
||||||
map(auth => auth?.uid ?? ''),
|
map(auth => auth?.uid ?? ''),
|
||||||
@@ -65,7 +66,7 @@ export class UserService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public async login(user: string, password: string): Promise<string | null> {
|
public async login(user: string, password: string): Promise<string | null> {
|
||||||
const aUser = await signInWithEmailAndPassword(this.auth, user, password);
|
const aUser = await this.runInFirebaseContext(() => signInWithEmailAndPassword(this.auth, user, password));
|
||||||
if (!aUser.user) return null;
|
if (!aUser.user) return null;
|
||||||
const dUser = await this.readUser(aUser.user.uid);
|
const dUser = await this.readUser(aUser.user.uid);
|
||||||
if (!dUser) return null;
|
if (!dUser) return null;
|
||||||
@@ -76,12 +77,12 @@ export class UserService {
|
|||||||
return aUser.user.uid;
|
return aUser.user.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public loggedIn$: () => Observable<boolean> = () => authState(this.auth).pipe(map(_ => !!_));
|
public loggedIn$: () => Observable<boolean> = () => this.authState$().pipe(map(_ => !!_));
|
||||||
|
|
||||||
public list$: () => Observable<User[]> = (): Observable<User[]> => this.users$;
|
public list$: () => Observable<User[]> = (): Observable<User[]> => this.users$;
|
||||||
|
|
||||||
public async logout(): Promise<void> {
|
public async logout(): Promise<void> {
|
||||||
await signOut(this.auth);
|
await this.runInFirebaseContext(() => signOut(this.auth));
|
||||||
this.iUser$.next(null);
|
this.iUser$.next(null);
|
||||||
this.iUserId$.next(null);
|
this.iUserId$.next(null);
|
||||||
}
|
}
|
||||||
@@ -92,11 +93,11 @@ export class UserService {
|
|||||||
|
|
||||||
public async changePassword(user: string): Promise<void> {
|
public async changePassword(user: string): Promise<void> {
|
||||||
const url = environment.url;
|
const url = environment.url;
|
||||||
await sendPasswordResetEmail(this.auth, user, {url});
|
await this.runInFirebaseContext(() => sendPasswordResetEmail(this.auth, user, {url}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createNewUser(user: string, name: string, password: string): Promise<void> {
|
public async createNewUser(user: string, name: string, password: string): Promise<void> {
|
||||||
const aUser = await createUserWithEmailAndPassword(this.auth, user, password);
|
const aUser = await this.runInFirebaseContext(() => createUserWithEmailAndPassword(this.auth, user, password));
|
||||||
if (!aUser.user) return;
|
if (!aUser.user) return;
|
||||||
const userId = aUser.user.uid;
|
const userId = aUser.user.uid;
|
||||||
await this.db.doc('users/' + userId).set({name, chordMode: 'onlyFirst', songUsage: {}});
|
await this.db.doc('users/' + userId).set({name, chordMode: 'onlyFirst', songUsage: {}});
|
||||||
@@ -176,6 +177,8 @@ export class UserService {
|
|||||||
return role.split(';').includes('admin');
|
return role.split(';').includes('admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private authState$ = () => runInInjectionContext(this.environmentInjector, () => authState(this.auth));
|
||||||
|
private runInFirebaseContext = <T>(factory: () => T): T => runInInjectionContext(this.environmentInjector, factory);
|
||||||
private readUser$ = (uid: string) => this.db.doc$<User>('users/' + uid);
|
private readUser$ = (uid: string) => this.db.doc$<User>('users/' + uid);
|
||||||
private readUser: (uid: string) => Promise<User | null> = (uid: string) => firstValueFrom(this.readUser$(uid));
|
private readUser: (uid: string) => Promise<User | null> = (uid: string) => firstValueFrom(this.readUser$(uid));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Injectable, inject} from '@angular/core';
|
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||||
import {CanActivate, Router, UrlTree} from '@angular/router';
|
import {CanActivate, Router, UrlTree} from '@angular/router';
|
||||||
import {Auth, authState} from '@angular/fire/auth';
|
import {Auth, authState} from '@angular/fire/auth';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
@@ -10,9 +10,10 @@ import {map, take} from 'rxjs/operators';
|
|||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
private auth = inject(Auth);
|
private auth = inject(Auth);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
private environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
public canActivate(): Observable<boolean | UrlTree> {
|
public canActivate(): Observable<boolean | UrlTree> {
|
||||||
return authState(this.auth).pipe(
|
return runInInjectionContext(this.environmentInjector, () => authState(this.auth)).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
map(user => (user ? true : this.router.createUrlTree(['user', 'login'])))
|
map(user => (user ? true : this.router.createUrlTree(['user', 'login'])))
|
||||||
);
|
);
|
||||||
|
|||||||
66
src/test.ts
66
src/test.ts
@@ -1,10 +1,74 @@
|
|||||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||||
|
|
||||||
import 'zone.js/testing';
|
import 'zone.js/testing';
|
||||||
import {getTestBed} from '@angular/core/testing';
|
import {getTestBed, TestBed} from '@angular/core/testing';
|
||||||
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
|
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
import {provideNoopAnimations} from '@angular/platform-browser/animations';
|
||||||
|
import {ActivatedRoute, provideRouter} from '@angular/router';
|
||||||
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||||
|
import {provideNativeDateAdapter} from '@angular/material/core';
|
||||||
|
import {provideFirebaseApp, initializeApp} from '@angular/fire/app';
|
||||||
|
import {getApp, getApps} from '@angular/fire/app';
|
||||||
|
import {getAuth, provideAuth} from '@angular/fire/auth';
|
||||||
|
import {initializeFirestore, provideFirestore} from '@angular/fire/firestore';
|
||||||
|
import {getStorage, provideStorage} from '@angular/fire/storage';
|
||||||
|
import {environment} from './environments/environment';
|
||||||
|
import {DbService} from './app/services/db.service';
|
||||||
|
|
||||||
type req = {keys: () => {map: (context: req) => void}};
|
type req = {keys: () => {map: (context: req) => void}};
|
||||||
|
|
||||||
// First, initialize the Angular testing environment.
|
// First, initialize the Angular testing environment.
|
||||||
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
|
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
|
||||||
|
|
||||||
|
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||||
|
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||||
|
|
||||||
|
const defaultTestingProviders = [
|
||||||
|
provideNoopAnimations(),
|
||||||
|
provideNativeDateAdapter(),
|
||||||
|
provideRouter([]),
|
||||||
|
provideFirebaseApp(() => (getApps().some(app => app.name === 'wgenerator-tests') ? getApp('wgenerator-tests') : initializeApp(environment.firebase, 'wgenerator-tests'))),
|
||||||
|
provideAuth(() => getAuth(getApp('wgenerator-tests'))),
|
||||||
|
provideFirestore(() => initializeFirestore(getApp('wgenerator-tests'), {})),
|
||||||
|
provideStorage(() => getStorage(getApp('wgenerator-tests'))),
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {params: {}, queryParams: {}, data: {}},
|
||||||
|
params: routeParams$.asObservable(),
|
||||||
|
queryParams: queryParams$.asObservable(),
|
||||||
|
data: of({}),
|
||||||
|
fragment: of(null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{provide: MAT_DIALOG_DATA, useValue: {}},
|
||||||
|
{provide: MatDialogRef, useValue: {close: () => void 0}},
|
||||||
|
{
|
||||||
|
provide: DbService,
|
||||||
|
useValue: {
|
||||||
|
col$: () => of([]),
|
||||||
|
doc$: () => of(null),
|
||||||
|
col: () => ({
|
||||||
|
valueChanges: () => of([]),
|
||||||
|
add: async () => ({id: 'test-id'}),
|
||||||
|
}),
|
||||||
|
doc: () => ({
|
||||||
|
set: async () => void 0,
|
||||||
|
update: async () => void 0,
|
||||||
|
delete: async () => void 0,
|
||||||
|
collection: () => ({
|
||||||
|
valueChanges: () => of([]),
|
||||||
|
add: async () => ({id: 'test-id'}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed);
|
||||||
|
TestBed.configureTestingModule = ((moduleDef?: Parameters<typeof TestBed.configureTestingModule>[0]) =>
|
||||||
|
originalConfigureTestingModule({
|
||||||
|
...moduleDef,
|
||||||
|
providers: [...defaultTestingProviders, ...(moduleDef?.providers ?? [])],
|
||||||
|
})) as typeof TestBed.configureTestingModule;
|
||||||
|
|||||||
Reference in New Issue
Block a user