/* * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ /* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-explicit-any, unicorn/no-thenable */ import {TestBed} from '@angular/core/testing'; import {Client} from '@openstapps/api/lib/client'; import { SCDish, SCMessage, SCMultiSearchRequest, SCSaveableThing, SCSearchQuery, SCSearchResponse, SCSearchValueFilter, SCThingOriginType, SCThingType, } from '@openstapps/core'; import {sampleThingsMap} from '../../_helpers/data/sample-things'; import {StorageProvider} from '../storage/storage.provider'; import {DataModule} from './data.module'; import {DataProvider, DataScope} from './data.provider'; import {StAppsWebHttpClient} from './stapps-web-http-client.provider'; import {LoggerModule, NgxLoggerLevel} from 'ngx-logger'; import {RouterModule} from '@angular/router'; describe('DataProvider', () => { let dataProvider: DataProvider; let storageProvider: StorageProvider; const sampleThing: SCMessage = sampleThingsMap.message[0] as SCMessage; const sampleResponse: SCSearchResponse = { data: sampleThingsMap.dish as SCDish[], facets: [ { buckets: [], field: 'foo', }, ], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 123, }, }; const sampleFilter: SCSearchValueFilter = { arguments: { field: 'type', value: 'dish', }, type: 'value', }; const sampleQuery: SCSearchQuery = { filter: sampleFilter, }; const sampleSavable: SCSaveableThing = { data: sampleThing, name: sampleThing.name, origin: { created: new Date().toISOString(), type: SCThingOriginType.User, }, type: SCThingType.Message, uid: sampleThing.uid, }; const fakeStorage = new Map([ ['foo', 'Bar'], ['bar', {foo: 'BarFoo'} as any], ]); beforeEach(async () => { TestBed.configureTestingModule({ imports: [DataModule, LoggerModule.forRoot({level: NgxLoggerLevel.TRACE}), RouterModule.forRoot([])], providers: [DataProvider, StAppsWebHttpClient], }); storageProvider = TestBed.inject(StorageProvider); dataProvider = TestBed.inject(DataProvider); }); it('should generate data key', async () => { dataProvider.storagePrefix = 'foo.data'; expect(dataProvider.getDataKey('123')).toBe('foo.data.123'); }); it('should provide backend data items using search query', async () => { spyOn(Client.prototype as any, 'search').and.callFake(() => { return { then: (callback: any) => { return callback(sampleResponse); }, }; }); const response = await dataProvider.search(sampleQuery); expect(response).toEqual(sampleResponse); }); it('should provide backend data items using multi search query', async () => { spyOn(Client.prototype as any, 'multiSearch').and.callFake(() => ({ then: (callback: any) => { return callback({ a: sampleResponse, }); }, })); const response = await dataProvider.multiSearch({a: sampleQuery}); expect(response).toEqual({a: sampleResponse}); }); it('should partition search requests correctly', async () => { const request = { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', } as SCMultiSearchRequest; // and response... const requestCheck = Object.assign({}, request); const responseShould = { a: 'A', b: 'B', c: 'C', d: 'D', e: 'E', }; dataProvider.backendQueriesLimit = 2; spyOn(Client.prototype as any, 'multiSearch').and.callFake((request_: SCMultiSearchRequest) => ({ then: (callback: any) => { let i = 0; for (const key in request_) { if (request_.hasOwnProperty(key)) { i++; expect(requestCheck[key]).not.toBeNull(); expect(requestCheck[key]).toEqual(request_[key]); // @ts-expect-error is not null // eslint-disable-next-line unicorn/no-null requestCheck[key] = null; // @ts-expect-error is a string for test purposes request_[key] = request_[key].toUpperCase(); } } expect(i).toBeLessThanOrEqual(dataProvider.backendQueriesLimit); return callback(request_); }, })); const response = await dataProvider.multiSearch(request); // @ts-expect-error same type expect(response).toEqual(responseShould); }); it('should put an data item into the local database (storage)', async () => { let providedThing: SCSaveableThing; spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => { providedThing = thing; providedThing.origin.created = sampleSavable.origin.created; }); expect(storageProvider.put).not.toHaveBeenCalled(); expect(providedThing!).not.toBeDefined(); await dataProvider.put(sampleThing); expect(providedThing!).toBeDefined(); expect(providedThing!).toEqual(sampleSavable); }); it('should correctly set and get single data item from the local database (storage)', async () => { spyOn(storageProvider, 'get').and.returnValue((async () => sampleSavable)()); expect(storageProvider.get).not.toHaveBeenCalled(); const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Local); expect(storageProvider.get).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid)); expect(providedThing).toEqual(sampleSavable); }); it('should provide all data items from the local database (storage)', async () => { const fakeStorage = new Map([ ['foo', 'Bar'], ['bar', {foo: 'BarFoo'} as any], ]); spyOn(storageProvider, 'search').and.callFake(async () => { return fakeStorage; }); const result = await dataProvider.getAll(); expect([...result.keys()].sort()).toEqual([...fakeStorage.keys()].sort()); expect([...result.values()].sort()).toEqual([...fakeStorage.values()].sort()); }); it('should provide single data from the backend', async () => { spyOn(Client.prototype, 'getThing' as any).and.callFake(() => { return { then: (callback: any) => { return callback(sampleThing); }, }; }); expect(Client.prototype.getThing).not.toHaveBeenCalled(); const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Remote); expect(Client.prototype.getThing).toHaveBeenCalledWith(sampleThing.uid); expect(providedThing).toBe(sampleThing); }); it('should get an item from both local and remote database', async () => { spyOn(Client.prototype, 'getThing' as any).and.callFake(() => { return { then: (callback: any) => { return callback(sampleThing); }, }; }); spyOn(storageProvider, 'get' as any).and.callFake(() => { return { then: (callback: any) => { return callback(sampleSavable); }, }; }); const result = await dataProvider.get(sampleThing.uid); expect(result.get(DataScope.Local)).toEqual(sampleSavable); expect(result.get(DataScope.Remote)).toEqual(sampleThing); }); it('should properly delete a data item from the local database (storage)', async () => { spyOn(storageProvider, 'delete'); await dataProvider.delete(sampleThing.uid); expect(storageProvider.delete).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid)); }); it('should properly delete all the data items from the local database (storage)', async () => { spyOn(storageProvider, 'delete'); spyOn(storageProvider, 'search').and.callFake(async () => { return fakeStorage; }); await dataProvider.deleteAll(); expect(storageProvider.delete).toHaveBeenCalledWith('foo', 'bar'); }); it('should properly check if a data item has already been saved', async () => { spyOn(storageProvider, 'has').and.callFake(async storageKey => { return (async () => { return dataProvider.getDataKey(sampleThing.uid) === storageKey; })(); }); expect(await dataProvider.isSaved('some-uuid')).toBeFalsy(); expect(await dataProvider.isSaved(sampleThing.uid)).toBeTruthy(); }); });