Skip to content

Commit

Permalink
Merge pull request #11025 from truenas/NAS-131557-v4
Browse files Browse the repository at this point in the history
NAS-131557 / 25.04 / New classes to replace websocket and api services
  • Loading branch information
undsoft authored Nov 14, 2024
2 parents f71aabe + 6ce4718 commit f9455e8
Show file tree
Hide file tree
Showing 762 changed files with 3,118 additions and 2,669 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Observable, Subject, of } from 'rxjs';
import {
CallResponseOrFactory,
JobResponseOrFactory,
} from 'app/core/testing/interfaces/mock-websocket-responses.interface';
} from 'app/core/testing/interfaces/mock-api-responses.interface';
import { ApiCallAndSubscribeMethod } from 'app/interfaces/api/api-call-and-subscribe-directory.interface';
import {
ApiCallMethod,
Expand All @@ -20,28 +20,28 @@ import {
} from 'app/interfaces/api/api-job-directory.interface';
import { ApiEvent } from 'app/interfaces/api-message.interface';
import { Job } from 'app/interfaces/job.interface';
import { ApiService } from 'app/services/api.service';
import { WebSocketConnectionService } from 'app/services/websocket-connection.service';
import { WebSocketService } from 'app/services/ws.service';

/**
* Better than just expect.anything() because it allows null and undefined.
*/
const anyArgument = when((_: ApiJobParams<ApiJobMethod>) => true);

/**
* MockWebSocketService can be used to update websocket mocks on the fly.
* For initial setup prefer mockWebSocket();
* MockApiService can be used to update api mocks on the fly.
* For initial setup prefer mockApi();
*
* To update on the fly:
* @example
* ```
* // In test case:
* const websocketService = spectator.inject(MockWebSocketService);
* websocketService.mockCallOnce('filesystem.stat', { gid: 5 } as FileSystemStat);
* const apiService = spectator.inject(MockApiService);
* apiService.mockCallOnce('filesystem.stat', { gid: 5 } as FileSystemStat);
* ```
*/
@Injectable()
export class MockWebSocketService extends WebSocketService {
export class MockApiService extends ApiService {
private subscribeStream$ = new Subject<ApiEvent>();
private jobIdCounter = 1;

Expand All @@ -59,13 +59,13 @@ export class MockWebSocketService extends WebSocketService {
this.callAndSubscribe = jest.fn();

when(this.call).mockImplementation((method: ApiCallMethod, args: unknown) => {
throw Error(`Unmocked websocket call ${method} with ${JSON.stringify(args)}`);
throw Error(`Unmocked api call ${method} with ${JSON.stringify(args)}`);
});
when(this.callAndSubscribe).mockImplementation((method: ApiCallAndSubscribeMethod, args: unknown) => {
throw Error(`Unmocked websocket callAndSubscribe ${method} with ${JSON.stringify(args)}`);
throw Error(`Unmocked api callAndSubscribe ${method} with ${JSON.stringify(args)}`);
});
when(this.job).mockImplementation((method: ApiJobMethod, args: unknown) => {
throw Error(`Unmocked websocket job call ${method} with ${JSON.stringify(args)}`);
throw Error(`Unmocked api job call ${method} with ${JSON.stringify(args)}`);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import {
} from 'app/interfaces/api/api-job-directory.interface';
import { Job } from 'app/interfaces/job.interface';

export enum MockWebSocketResponseType {
export enum MockApiResponseType {
// eslint-disable-next-line @typescript-eslint/no-shadow
Job = 'job',
Call = 'call',
}

export interface MockWebSocketCallResponse {
type: MockWebSocketResponseType.Call;
export interface MockApiCallResponse {
type: MockApiResponseType.Call;
method: ApiCallMethod;
response: unknown;
id?: number;
}

export interface MockWebSocketJobResponse {
type: MockWebSocketResponseType.Job;
export interface MockApiJobResponse {
type: MockApiResponseType.Job;
method: ApiJobMethod;
response: Job | ((params: unknown) => Job);
id?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import { MockEnclosureConfig } from 'app/core/testing/mock-enclosure/interfaces/
import { MockEnclosureGenerator } from 'app/core/testing/mock-enclosure/mock-enclosure-generator.utils';
import { ApiCallMethod, ApiCallParams, ApiCallResponse } from 'app/interfaces/api/api-call-directory.interface';
import { SystemInfo } from 'app/interfaces/system-info.interface';
import { ApiService } from 'app/services/api.service';
import { WebSocketConnectionService } from 'app/services/websocket-connection.service';
import { WebSocketService } from 'app/services/ws.service';

@Injectable({
providedIn: 'root',
})
export class MockEnclosureWebsocketService extends WebSocketService {
export class MockEnclosureApiService extends ApiService {
private mockConfig: MockEnclosureConfig = environment.mockConfig;
private mockStorage = new MockEnclosureGenerator(this.mockConfig);

Expand All @@ -25,7 +25,7 @@ export class MockEnclosureWebsocketService extends WebSocketService {
) {
super(router, wsManager, translate);

console.warn('MockEnclosureWebsocketService is in effect. Some calls will be mocked');
console.warn('MockEnclosureApiService is in effect. Some calls will be mocked');
}

override call<M extends ApiCallMethod>(method: M, params?: ApiCallParams<M>): Observable<ApiCallResponse<M>> {
Expand Down
13 changes: 13 additions & 0 deletions src/app/core/testing/utils/empty-api.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getMissingInjectionErrorFactory, getMissingInjectionErrorObservable } from 'app/core/testing/utils/missing-injection-factories';
import { ApiService } from 'app/services/api.service';

export class EmptyApiService {
readonly clearSubscriptions$ = getMissingInjectionErrorObservable(ApiService.name);
call = getMissingInjectionErrorFactory(ApiService.name);
job = getMissingInjectionErrorFactory(ApiService.name);
callAndSubscribe = getMissingInjectionErrorFactory(ApiService.name);
startJob = getMissingInjectionErrorFactory(ApiService.name);
subscribe = getMissingInjectionErrorFactory(ApiService.name);
subscribeToLogs = getMissingInjectionErrorFactory(ApiService.name);
clearSubscriptions = getMissingInjectionErrorFactory(ApiService.name);
}
13 changes: 0 additions & 13 deletions src/app/core/testing/utils/empty-ws.service.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,36 @@ import {
} from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { MockWebSocketService } from 'app/core/testing/classes/mock-websocket.service';
import { MockApiService } from 'app/core/testing/classes/mock-api.service';
import {
CallResponseOrFactory, JobResponseOrFactory,
MockWebSocketCallResponse, MockWebSocketJobResponse,
MockWebSocketResponseType,
} from 'app/core/testing/interfaces/mock-websocket-responses.interface';
MockApiCallResponse, MockApiJobResponse,
MockApiResponseType,
} from 'app/core/testing/interfaces/mock-api-responses.interface';
import { ApiCallMethod } from 'app/interfaces/api/api-call-directory.interface';
import { ApiJobDirectory, ApiJobMethod } from 'app/interfaces/api/api-job-directory.interface';
import { Job } from 'app/interfaces/job.interface';
import { ApiService } from 'app/services/api.service';
import { WebSocketConnectionService } from 'app/services/websocket-connection.service';
import { WebSocketService } from 'app/services/ws.service';

/**
* This is a sugar syntax for creating simple websocket mocks.
* This is a sugar syntax for creating simple api mocks.
* @example
* providers: [
* mockWebSocket([
* mockApi([
* mockCall('filesystem.stat': { gid: 0 } as FileSystemStat),
* mockCall('filesystem.stat', () => ({ gid: 0 } as FileSystemStat)),
* mockJob('filesystem.setacl', fakeSuccessfulJob()),
* ...
* }),
* ]
*
* It also makes available MockWebSocketService, which allows customizing calls on the fly.
* It also makes available MockApiService, which allows customizing calls on the fly.
*
* If you need more customization, use ordinary mockProvider().
* @example
* providers: [
* mockProvider(WebSocketService, {
* mockProvider(ApiService, {
* call: jest.fn((method) => {
* if (method === 'filesystem.stat') {
* return of({ user: 'john' } as FileSystemStat);
Expand All @@ -42,31 +42,31 @@ import { WebSocketService } from 'app/services/ws.service';
* ]
*/

export function mockWebSocket(
mockResponses?: (MockWebSocketCallResponse | MockWebSocketJobResponse)[],
export function mockApi(
mockResponses?: (MockApiCallResponse | MockApiJobResponse)[],
): (FactoryProvider | ExistingProvider | ValueProvider)[] {
return [
{
provide: WebSocketService,
provide: ApiService,
useFactory: (router: Router, wsManager: WebSocketConnectionService, translate: TranslateService) => {
const mockWebSocketService = new MockWebSocketService(router, wsManager, translate);
const mockApiService = new MockApiService(router, wsManager, translate);
(mockResponses || []).forEach((mockResponse) => {
if (mockResponse.type === MockWebSocketResponseType.Call) {
mockWebSocketService.mockCall(mockResponse.method, mockResponse.response);
} else if (mockResponse.type === MockWebSocketResponseType.Job) {
mockWebSocketService.mockJob(
if (mockResponse.type === MockApiResponseType.Call) {
mockApiService.mockCall(mockResponse.method, mockResponse.response);
} else if (mockResponse.type === MockApiResponseType.Job) {
mockApiService.mockJob(
mockResponse.method,
mockResponse.response as Job<ApiJobDirectory[ApiJobMethod]['response']>,
);
}
});
return mockWebSocketService;
return mockApiService;
},
deps: [Router, WebSocketConnectionService, TranslateService],
},
{
provide: MockWebSocketService,
useExisting: forwardRef(() => WebSocketService),
provide: MockApiService,
useExisting: forwardRef(() => ApiService),
},
{
provide: WebSocketConnectionService,
Expand All @@ -78,25 +78,25 @@ export function mockWebSocket(
export function mockCall<M extends ApiCallMethod>(
method: M,
response: CallResponseOrFactory<M> = undefined,
): MockWebSocketCallResponse {
): MockApiCallResponse {
return {
response,
method,
type: MockWebSocketResponseType.Call,
type: MockApiResponseType.Call,
};
}

/**
* Mocks immediate call() and job() responses and core.get_jobs when id is queried.
* @see MockWebSocketService.mockJob()
* @see MockApiService.mockJob()
*/
export function mockJob<M extends ApiJobMethod>(
method: M,
response: JobResponseOrFactory<M> = undefined,
): MockWebSocketJobResponse {
): MockApiJobResponse {
return {
response,
method,
type: MockWebSocketResponseType.Job,
type: MockApiResponseType.Job,
};
}
4 changes: 2 additions & 2 deletions src/app/core/testing/utils/mock-auth.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { MockAuthService } from 'app/core/testing/classes/mock-auth.service';
import { AccountAttribute } from 'app/enums/account-attribute.enum';
import { Role } from 'app/enums/role.enum';
import { LoggedInUser } from 'app/interfaces/ds-cache.interface';
import { ApiService } from 'app/services/api.service';
import { AuthService } from 'app/services/auth/auth.service';
import { TokenLastUsedService } from 'app/services/token-last-used.service';
import { WebSocketConnectionService } from 'app/services/websocket-connection.service';
import { WebSocketService } from 'app/services/ws.service';

export const dummyUser = {
privilege: {
Expand Down Expand Up @@ -44,7 +44,7 @@ export function mockAuth(
isConnected$: of(true),
}),
createSpyObject(Store),
createSpyObject(WebSocketService),
createSpyObject(ApiService),
createSpyObject(TokenLastUsedService),
createSpyObject(Window),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum WebSocketErrorName {
export enum ApiErrorName {
NotAuthenticated = 'ENOTAUTHENTICATED',
NoAccess = 'EACCES',
NoMemory = 'ENOMEM',
Expand Down
11 changes: 11 additions & 0 deletions src/app/helpers/api.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiError } from 'app/interfaces/api-error.interface';

export function isApiError(error: unknown): error is ApiError {
if (error === null) return false;

return typeof error === 'object'
&& 'error' in error
&& 'extra' in error
&& 'reason' in error
&& 'trace' in error;
}
6 changes: 3 additions & 3 deletions src/app/helpers/operators/to-loading-state.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { of, OperatorFunction, pipe } from 'rxjs';
import {
catchError, map, startWith,
} from 'rxjs/operators';
import { WebSocketError } from 'app/interfaces/websocket-error.interface';
import { ApiError } from 'app/interfaces/api-error.interface';

export interface LoadingState<T> {
isLoading: boolean;
value?: T;
error?: WebSocketError | Error;
error?: ApiError | Error;
}

/**
Expand All @@ -25,7 +25,7 @@ export interface LoadingState<T> {
export function toLoadingState<T>(): OperatorFunction<T, LoadingState<T>> {
return pipe(
map((value) => ({ isLoading: false, value })),
catchError((error: WebSocketError | Error) => of({ isLoading: false, error })),
catchError((error: ApiError | Error) => of({ isLoading: false, error })),
startWith({ isLoading: true }),
);
}
11 changes: 0 additions & 11 deletions src/app/helpers/websocket.helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InjectionToken } from '@angular/core';
import { webSocket as rxjsWebSocket } from 'rxjs/webSocket';
import { WebSocketError } from 'app/interfaces/websocket-error.interface';

// eslint-disable-next-line @typescript-eslint/naming-convention
export const WEBSOCKET = new InjectionToken<typeof rxjsWebSocket>(
Expand All @@ -10,13 +9,3 @@ export const WEBSOCKET = new InjectionToken<typeof rxjsWebSocket>(
factory: () => rxjsWebSocket,
},
);

export function isWebSocketError(error: unknown): error is WebSocketError {
if (error === null) return false;

return typeof error === 'object'
&& 'error' in error
&& 'extra' in error
&& 'reason' in error
&& 'trace' in error;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { ApiErrorName } from 'app/enums/api-error-name.enum';
import { ResponseErrorType } from 'app/enums/response-error-type.enum';
import { WebSocketErrorName } from 'app/enums/websocket-error-name.enum';

export interface WebSocketError {
errname: WebSocketErrorName;
export interface ApiError {
errname: ApiErrorName;
error: number;
extra: unknown;
reason: string;
trace: WebSocketErrorTrace;
trace: ApiErrorTrace;
type: ResponseErrorType | null;
}

export interface WebSocketErrorTrace {
export interface ApiErrorTrace {
class: string;
formatted: string;
frames: WebSocketTraceFrame[];
frames: ApiTraceFrame[];
}

export interface WebSocketTraceFrame {
export interface ApiTraceFrame {
argspec: string[];
filename: string;
line: string;
Expand Down
Loading

0 comments on commit f9455e8

Please sign in to comment.