Skip to content

Commit

Permalink
fix(services): perform lazy initialization (#1554)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewazores authored Jan 31, 2025
1 parent bd78b9d commit efbde22
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 58 deletions.
4 changes: 3 additions & 1 deletion src/app/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ export const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
}, [theme]);

React.useEffect(() => {
serviceContext.login.checkAuth();
serviceContext.api.testBaseServer();
serviceContext.notificationChannel.connect();
}, [serviceContext.api, serviceContext.notificationChannel]);
serviceContext.targets.queryForTargets().subscribe();
}, [serviceContext.login, serviceContext.api, serviceContext.notificationChannel, serviceContext.targets]);

React.useEffect(() => {
addSubscription(
Expand Down
2 changes: 1 addition & 1 deletion src/app/Shared/Services/Api.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,7 @@ export class ApiService {
return out;
}

private sendRequest(
sendRequest(
apiVersion: ApiVersion,
path: string,
config?: RequestInit,
Expand Down
61 changes: 30 additions & 31 deletions src/app/Shared/Services/Login.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/
import { Observable, ObservableInput, of, ReplaySubject } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';
import { catchError, concatMap, debounceTime, distinctUntilChanged, finalize, map, tap } from 'rxjs/operators';
import { ApiService } from './Api.service';
import { SessionState } from './service.types';
import type { SettingsService } from './Settings.service';

Expand All @@ -25,21 +25,21 @@ export class LoginService {
private readonly sessionState = new ReplaySubject<SessionState>(1);

constructor(
private readonly authority: (path: string) => Observable<string>,
private readonly api: ApiService,
private readonly settings: SettingsService,
) {
this.sessionState.next(SessionState.CREATING_USER_SESSION);
}

authority('/api/v4/auth')
checkAuth(): void {
this.api
.sendRequest('v4', 'auth', {
credentials: 'include',
mode: 'cors',
method: 'POST',
body: null,
})
.pipe(
concatMap((u) =>
fromFetch(u, {
credentials: 'include',
mode: 'cors',
method: 'POST',
body: null,
}),
),
concatMap((response) => {
let gapAuth = response?.headers?.get('Gap-Auth');
if (gapAuth) {
Expand Down Expand Up @@ -69,27 +69,26 @@ export class LoginService {
}

setLoggedOut(): Observable<boolean> {
return this.authority('/api/v4/logout').pipe(
concatMap((u) =>
fromFetch(u, {
credentials: 'include',
mode: 'cors',
method: 'POST',
body: null,
return this.api
.sendRequest('v4', 'logout', {
credentials: 'include',
mode: 'cors',
method: 'POST',
body: null,
})
.pipe(
concatMap((response) => {
return of(response).pipe(
map((response) => response.ok),
tap(() => this.resetSessionState()),
);
}),
catchError((e: Error): ObservableInput<boolean> => {
window.console.error(JSON.stringify(e, Object.getOwnPropertyNames(e)));
return of(false);
}),
),
concatMap((response) => {
return of(response).pipe(
map((response) => response.ok),
tap(() => this.resetSessionState()),
);
}),
catchError((e: Error): ObservableInput<boolean> => {
window.console.error(JSON.stringify(e, Object.getOwnPropertyNames(e)));
return of(false);
}),
finalize(() => this.navigateToLoginPage()),
);
finalize(() => this.navigateToLoginPage()),
);
}

setSessionState(state: SessionState): void {
Expand Down
10 changes: 5 additions & 5 deletions src/app/Shared/Services/Services.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,20 @@ export const defaultContext: CryostatContext = {

const target = new TargetService();
const settings = new SettingsService();
const login = new LoginService(defaultContext.url, settings);
const api = new ApiService(defaultContext, target, NotificationsInstance);
const login = new LoginService(api, settings);
const notificationChannel = new NotificationChannel(defaultContext, NotificationsInstance, login);
const reports = new ReportService(defaultContext, NotificationsInstance, notificationChannel);
const targets = new TargetsService(api, NotificationsInstance, notificationChannel);

const defaultServices: Services = {
target,
targets,
reports,
api,
notificationChannel,
settings,
api,
login,
notificationChannel,
reports,
targets,
};

const ServiceContext: React.Context<Services> = React.createContext(defaultServices);
Expand Down
3 changes: 0 additions & 3 deletions src/app/Shared/Services/Targets.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export class TargetsService {
private readonly notifications: NotificationService,
notificationChannel: NotificationChannel,
) {
// just trigger a startup query
this.queryForTargets().subscribe();

notificationChannel.messages(NotificationCategory.TargetJvmDiscovery).subscribe((v) => {
const evt: TargetDiscoveryEvent = v.message.event;
switch (evt.kind) {
Expand Down
64 changes: 47 additions & 17 deletions src/test/Shared/Services/Login.service.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,22 @@
* limitations under the License.
*/

import { ApiService } from '@app/Shared/Services/Api.service';
import { LoginService } from '@app/Shared/Services/Login.service';
import { NotificationService } from '@app/Shared/Services/Notifications.service';
import { SessionState } from '@app/Shared/Services/service.types';
import { SettingsService } from '@app/Shared/Services/Settings.service';
import { TargetService } from '@app/Shared/Services/Target.service';
import { firstValueFrom, of, timeout } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';

jest.mock('rxjs/fetch', () => {
return {
fromFetch: jest.fn((_url: unknown, _opts: unknown): unknown => of()),
};
});

jest.unmock('@app/Shared/Services/Login.service');
jest.mock('@app/Shared/Services/Api.service');

describe('Login.service', () => {
const mockFromFetch = fromFetch as jest.Mock;
let svc: LoginService;

describe('setLoggedOut', () => {
let apiSvc: ApiService;
let settingsSvc: SettingsService;
let saveLocation: Location;

Expand All @@ -58,6 +55,14 @@ describe('Login.service', () => {
});

beforeEach(() => {
apiSvc = new ApiService(
{
headers: () => of(new Headers()),
url: (p) => of(`./${p}`),
},
{} as TargetService,
{} as NotificationService,
);
settingsSvc = new SettingsService();
(settingsSvc.webSocketDebounceMs as jest.Mock).mockReturnValue(0);
});
Expand Down Expand Up @@ -90,31 +95,56 @@ describe('Login.service', () => {
},
});
const logoutResp = createResponse(200, true);
mockFromFetch
.mockReturnValueOnce(of(initAuthResp))
.mockReturnValueOnce(of(authResp))
.mockReturnValueOnce(of(logoutResp));
jest
.spyOn(apiSvc, 'sendRequest')
.mockReturnValue(
of({
ok: true,
json: new Promise((resolve) => resolve(initAuthResp)),
} as unknown as Response),
)
.mockReturnValue(
of({
ok: true,
json: new Promise((resolve) => resolve(authResp)),
} as unknown as Response),
)
.mockReturnValue(
of({
ok: true,
json: new Promise((resolve) => resolve(logoutResp)),
} as unknown as Response),
);
window.location.href = 'https://example.com/';
location.href = window.location.href;
svc = new LoginService((p) => of(`.${p}`), settingsSvc);
svc = new LoginService(apiSvc, settingsSvc);
});

it('should emit true', async () => {
const logoutResp = createResponse(200, true);
jest.spyOn(apiSvc, 'sendRequest').mockReturnValue(
of({
ok: true,
json: new Promise((resolve) => resolve(logoutResp)),
} as unknown as Response),
);
const result = await firstValueFrom(svc.setLoggedOut());
expect(result).toBeTruthy();
});

it('should make expected API calls', async () => {
expect(mockFromFetch).toHaveBeenCalledTimes(1);
expect(mockFromFetch).toHaveBeenNthCalledWith(1, `./api/v4/auth`, {
expect(apiSvc.sendRequest).toHaveBeenCalledTimes(0);
svc.checkAuth();
expect(apiSvc.sendRequest).toHaveBeenCalledTimes(1);
expect(apiSvc.sendRequest).toHaveBeenNthCalledWith(1, 'v4', 'auth', {
credentials: 'include',
mode: 'cors',
method: 'POST',
body: null,
});
await firstValueFrom(svc.setLoggedOut());
expect(mockFromFetch).toHaveBeenCalledTimes(2);
expect(mockFromFetch).toHaveBeenNthCalledWith(2, `./api/v4/logout`, {
expect(apiSvc.sendRequest).toHaveBeenCalledTimes(2);
expect(apiSvc.sendRequest).toHaveBeenNthCalledWith(2, 'v4', 'logout', {
credentials: 'include',
mode: 'cors',
method: 'POST',
Expand Down

0 comments on commit efbde22

Please sign in to comment.