Skip to content

Commit

Permalink
NAS-130955 / 25.04 / Alternative for global data stores, part 1 (#10623)
Browse files Browse the repository at this point in the history
* NAS-130955: Add `globalStore` function

* NAS-130955: Add invalidate method

* NAS-130955: Add unit tests

* NAS-130955: Add `mockGlobalStore`

* NAS-130955: Fix remarks

---------

Co-authored-by: Boris Vasilenko <bvasilenko@ixsystems.com>
  • Loading branch information
bvasilenko and Boris Vasilenko authored Sep 23, 2024
1 parent a012d4e commit 8e5e6c9
Show file tree
Hide file tree
Showing 7 changed files with 435 additions and 3 deletions.
79 changes: 79 additions & 0 deletions src/app/core/testing/classes/mock-global-store.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Injectable,
Type,
ExistingProvider,
FactoryProvider,
forwardRef,
} from '@angular/core';
import { Observable, of } from 'rxjs';
import { MockGlobalStoreResponses } from 'app/core/testing/interfaces/mock-global-store-responses.interface';
import {
ApiCallAndSubscribeMethod,
ApiCallAndSubscribeResponse,
} from 'app/interfaces/api/api-call-and-subscribe-directory.interface';
import {
ApiCallMethod,
ApiCallResponse,
} from 'app/interfaces/api/api-call-directory.interface';
import { ApiEventMethod, ApiEventTyped } from 'app/interfaces/api-message.interface';
import {
GlobalStoreMembers,
} from 'app/services/global-store/global-store.service';

export function mockGlobalStore<
M1 extends ApiCallMethod,
M2 extends ApiEventMethod,
M3 extends ApiCallAndSubscribeMethod,
>(
stores: [
store: Type<GlobalStoreMembers<M1, M2, M3>>,
mockResponses?: MockGlobalStoreResponses<M1, M2, M3>,
][],
): (FactoryProvider | ExistingProvider)[] {
return stores.map((store) => {
const mockStoreService = new (mockGlobalStoreService(store[1]))();
return [
{
provide: store[0],
useFactory: () => mockStoreService,
},
{
provide: mockStoreService,
useExisting: forwardRef(() => store[0]),
},
];
}).flat();
}

function mockGlobalStoreService<
M1 extends ApiCallMethod,
M2 extends ApiEventMethod,
M3 extends ApiCallAndSubscribeMethod,
>(
mockResponses?: MockGlobalStoreResponses<M1, M2, M3>,
): Type<GlobalStoreMembers<M1, M2, M3>> {
@Injectable({ providedIn: 'root' })
class MockGlobalStore implements GlobalStoreMembers<M1, M2, M3> {
get call(): Observable<ApiCallResponse<M1>> {
return this.getResponse(mockResponses?.call);
}

get subscribe(): Observable<ApiEventTyped<M2>> {
return this.getResponse(mockResponses?.subscribe);
}

get callAndSubscribe(): Observable<ApiCallAndSubscribeResponse<M3>[]> {
return this.getResponse(mockResponses?.callAndSubscribe);
}

invalidate(): void {}

private getResponse<R>(mockResponse: R): Observable<R> {
if (mockResponse === undefined) {
throw Error('Unmocked global store response');
}
return of(mockResponse);
}
}
return MockGlobalStore;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
ApiCallAndSubscribeMethod,
ApiCallAndSubscribeResponse,
} from 'app/interfaces/api/api-call-and-subscribe-directory.interface';
import {
ApiCallMethod,
ApiCallResponse,
} from 'app/interfaces/api/api-call-directory.interface';
import {
ApiEventMethod,
ApiEventTyped,
} from 'app/interfaces/api-message.interface';

export interface MockGlobalStoreResponses<
M1 extends ApiCallMethod,
M2 extends ApiEventMethod,
M3 extends ApiCallAndSubscribeMethod,
> {
call?: ApiCallResponse<M1>;
subscribe?: ApiEventTyped<M2>;
callAndSubscribe?: ApiCallAndSubscribeResponse<M3>[];
}
57 changes: 57 additions & 0 deletions src/app/pages/dashboard/services/widget-resources.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
createServiceFactory,
SpectatorService,
} from '@ngneat/spectator/jest';
import { mockGlobalStore } from 'app/core/testing/classes/mock-global-store.service';
import { mockCall, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { App } from 'app/interfaces/app.interface';
import { Pool } from 'app/interfaces/pool.interface';
import { WidgetResourcesService } from 'app/pages/dashboard/services/widget-resources.service';
import { appStore, poolStore } from 'app/services/global-store/stores.constant';

const pools = [
{ id: 1, name: 'pool_1' },
{ id: 2, name: 'pool_2' },
] as Pool[];

const apps = [
{ id: '1', name: 'app_1' },
{ id: '2', name: 'app_2' },
] as App[];

describe('WidgetResourcesService', () => {
let spectator: SpectatorService<WidgetResourcesService>;
const createService = createServiceFactory({
service: WidgetResourcesService,
providers: [
mockWebSocket([
mockCall('replication.query'),
mockCall('rsynctask.query'),
mockCall('cloudsync.query'),
mockCall('webui.main.dashboard.sys_info'),
mockCall('interface.query'),
mockCall('update.check_available'),
]),
mockGlobalStore([
[poolStore, { callAndSubscribe: pools }],
[appStore, { call: apps }],
]),
],
});

beforeEach(() => {
spectator = createService();
});

it('returns pools', () => {
let poolsResponse: Pool[];
spectator.service.pools$.subscribe((response) => poolsResponse = response);
expect(poolsResponse).toEqual(pools);
});

it('returns apps', () => {
let appsResponse: App[];
spectator.service.installedApps$.subscribe((response) => appsResponse = response.value);
expect(appsResponse).toEqual(apps);
});
});
7 changes: 4 additions & 3 deletions src/app/pages/dashboard/services/widget-resources.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { subHours, subMinutes } from 'date-fns';
import {
Expand All @@ -17,6 +17,7 @@ import { Pool } from 'app/interfaces/pool.interface';
import { ReportingData } from 'app/interfaces/reporting.interface';
import { VolumesData, VolumeData } from 'app/interfaces/volume-data.interface';
import { processNetworkInterfaces } from 'app/pages/dashboard/widgets/network/widget-interface/widget-interface.utils';
import { appStore, poolStore } from 'app/services/global-store/stores.constant';
import { WebSocketService } from 'app/services/ws.service';
import { AppsState } from 'app/store';
import { waitForSystemInfo } from 'app/store/system-info/system-info.selectors';
Expand Down Expand Up @@ -67,9 +68,9 @@ export class WidgetResourcesService {
shareReplay({ bufferSize: 1, refCount: true }),
);

readonly installedApps$ = this.ws.call('app.query').pipe(toLoadingState());
readonly installedApps$ = inject(appStore).call.pipe(toLoadingState());

readonly pools$ = this.ws.callAndSubscribe('pool.query').pipe(
readonly pools$ = inject(poolStore).callAndSubscribe.pipe(
shareReplay({ bufferSize: 1, refCount: true }),
);

Expand Down
187 changes: 187 additions & 0 deletions src/app/services/global-store/global-store.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { TestBed } from '@angular/core/testing';
import { mockProvider } from '@ngneat/spectator/jest';
import { TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { ApiEvent } from 'app/interfaces/api-message.interface';
import { Pool } from 'app/interfaces/pool.interface';
import { QueryParams } from 'app/interfaces/query-api.interface';
import { globalStore } from 'app/services/global-store/global-store.service';
import { WebSocketService } from 'app/services/ws.service';

const poolResponse = [
{ id: 1, name: 'pool_1' },
{ id: 2, name: 'pool_2' },
] as Pool[];

const params: QueryParams<Pool> = [
[
['id', '=', 1],
['id', '=', 2],
],
];

const poolStore = globalStore('pool.query', params);

describe('GlobalStoreService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
mockProvider(TranslateService),
mockProvider(WebSocketService, {
call: jest.fn(() => of(poolResponse)),
subscribe: jest.fn(() => of({
fields: poolResponse,
} as ApiEvent<Pool[]>)),
callAndSubscribe: jest.fn(() => of(poolResponse)),
}),
],
});
});

describe('call', () => {
it('sends a request after subscription', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.call;

const poolSubscription = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledWith('pool.query', params);
poolSubscription.unsubscribe();
});

it('caches a request result', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.call;
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(0);

const poolSubscription1 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(1);
poolSubscription1.unsubscribe();

const poolSubscription2 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(1);
poolSubscription2.unsubscribe();

const poolSubscription3 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(1);
poolSubscription3.unsubscribe();
});

it('invalidates a request result', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.call;
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(0);

const poolSubscription1 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(1);
poolSubscription1.unsubscribe();

const poolSubscription2 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(1);
poolSubscription2.unsubscribe();

poolStoreService.invalidate();

const poolSubscription3 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).call).toHaveBeenCalledTimes(2);
poolSubscription3.unsubscribe();
});
});

describe('subscribe', () => {
it('sends a request after subscription', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.subscribe;

const poolSubscription = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledWith('pool.query');
poolSubscription.unsubscribe();
});

it('caches a request result', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.subscribe;
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(0);

const poolSubscription1 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(1);
poolSubscription1.unsubscribe();

const poolSubscription2 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(1);
poolSubscription2.unsubscribe();

const poolSubscription3 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(1);
poolSubscription3.unsubscribe();
});

it('invalidates a request result', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.subscribe;
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(0);

const poolSubscription1 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(1);
poolSubscription1.unsubscribe();

const poolSubscription2 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(1);
poolSubscription2.unsubscribe();

poolStoreService.invalidate();

const poolSubscription3 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).subscribe).toHaveBeenCalledTimes(2);
poolSubscription3.unsubscribe();
});
});

describe('callAndSubscribe', () => {
it('sends a request after subscription', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.callAndSubscribe;

const poolSubscription = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledWith('pool.query', params);
poolSubscription.unsubscribe();
});

it('caches a request result', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.callAndSubscribe;
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(0);

const poolSubscription1 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(1);
poolSubscription1.unsubscribe();

const poolSubscription2 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(1);
poolSubscription2.unsubscribe();

const poolSubscription3 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(1);
poolSubscription3.unsubscribe();
});

it('invalidates a request result', () => {
const poolStoreService = TestBed.inject(poolStore);
const pools$ = poolStoreService.callAndSubscribe;
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(0);

const poolSubscription1 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(1);
poolSubscription1.unsubscribe();

const poolSubscription2 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(1);
poolSubscription2.unsubscribe();

poolStoreService.invalidate();

const poolSubscription3 = pools$.subscribe();
expect(TestBed.inject(WebSocketService).callAndSubscribe).toHaveBeenCalledTimes(2);
poolSubscription3.unsubscribe();
});
});
});
Loading

0 comments on commit 8e5e6c9

Please sign in to comment.