Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions src/__mocks__/svgrMock.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from 'features/authentication/store/authentication.selectors';
import { getErrorMessage } from 'shared/lib/error';
import { fetchIdpSettings } from 'shared/config/idp-settings';
import { useAppParametersInvalidationListener } from 'features/app-parameters/hooks/use-app-parameters-invalidation-listener';
import { useAppParametersInvalidationListener } from './notifications/use-app-parameters-invalidation-listener';
import { useAppDispatch, useAppSelector } from './store/store';

function App() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { renderHook } from '@testing-library/react';
import { NotificationsUrlKeys, useNotificationsListener } from '@gridsuite/commons-ui';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAppParametersInvalidationListener } from 'app/notifications/use-app-parameters-invalidation-listener';
import { invalidateConfigQueries } from 'shared/api/config-api/config-api';
import { createTestContext } from 'test-utils/create-test-context';

vi.mock('shared/api/config-api/config-api', () => ({
invalidateConfigQueries: vi.fn(),
}));

describe('useAppParametersInvalidationListener', () => {
let listenerCallbackMessage: ((event: MessageEvent) => void) | undefined;

beforeEach(() => {
vi.clearAllMocks();
listenerCallbackMessage = undefined;

vi.mocked(useNotificationsListener).mockImplementation((_urlKey, options) => {
listenerCallbackMessage = options.listenerCallbackMessage;
});
});

it('registers a notifications listener on the config channel', () => {
const { wrapper } = createTestContext();

expect(listenerCallbackMessage).not.toBeDefined();
renderHook(() => useAppParametersInvalidationListener(), { wrapper });

expect(useNotificationsListener).toHaveBeenCalledWith(NotificationsUrlKeys.CONFIG, {
listenerCallbackMessage: expect.any(Function),
});
expect(listenerCallbackMessage).toBeDefined();
});

it('invalidates config queries when receiving a parameter name', () => {
const { wrapper } = createTestContext();

renderHook(() => useAppParametersInvalidationListener(), { wrapper });

listenerCallbackMessage?.({
data: JSON.stringify({
headers: { parameterName: 'theme' },
}),
} as MessageEvent);

expect(invalidateConfigQueries).toHaveBeenCalledTimes(1);
expect(invalidateConfigQueries).toHaveBeenCalledWith(expect.anything(), 'theme');
});

it('does nothing when parameterName is missing', () => {
const { wrapper } = createTestContext();

renderHook(() => useAppParametersInvalidationListener(), { wrapper });

listenerCallbackMessage?.({
data: JSON.stringify({
headers: {},
}),
} as MessageEvent);

expect(invalidateConfigQueries).not.toHaveBeenCalled();
});

it('throws on invalid JSON payloads', () => {
const { wrapper } = createTestContext();

renderHook(() => useAppParametersInvalidationListener(), { wrapper });

expect(() => {
listenerCallbackMessage?.({
data: 'not-json',
} as MessageEvent);
}).toThrow(SyntaxError);
expect(invalidateConfigQueries).not.toHaveBeenCalled();
});
});
31 changes: 31 additions & 0 deletions src/app/notifications/use-app-parameters-invalidation-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { NotificationsUrlKeys, useNotificationsListener } from '@gridsuite/commons-ui';
import { useAppDispatch } from '../store/store';
import { invalidateConfigQueries } from '../../shared/api/config-api/config-api';

type ConfigNotificationData = {
headers?: {
parameterName?: string;
};
};

export const useAppParametersInvalidationListener = () => {
const dispatch = useAppDispatch();

const invalidateAppParameter = (event: MessageEvent) => {
const eventData = JSON.parse(event.data) as ConfigNotificationData;
if (eventData.headers?.parameterName) {
invalidateConfigQueries(dispatch, eventData.headers.parameterName);
}
};

useNotificationsListener(NotificationsUrlKeys.CONFIG, {
listenerCallbackMessage: invalidateAppParameter,
});
};
8 changes: 7 additions & 1 deletion src/app/providers/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StyledEngineProvider, ThemeProvider, CssBaseline } from '@mui/material'
import {
CardErrorBoundary,
getComputedLanguage,
NotificationsProvider,
PARAM_LANGUAGE,
PARAM_THEME,
SnackbarProvider,
Expand All @@ -22,6 +23,7 @@ import { appMessages } from 'app/config/app-messages';
import { getAppTheme } from 'app/config/app-theme';
import { useGetConfigParameterWithFallback } from 'features/app-parameters/hooks/use-get-config-parameter-with-fallback';
import { SnackRefRegisterer } from './SnackRefRegisterer';
import { useNotificationsUrlGenerator } from '../../shared/api/ws/use-notifications-url-generator';

const basename = new URL(document.querySelector('base')?.href ?? '').pathname;

Expand All @@ -30,6 +32,8 @@ function AppProvidersWithStore() {
const computedLanguage = getComputedLanguage(language);
const { data: theme } = useGetConfigParameterWithFallback(PARAM_THEME);

const urlMapper = useNotificationsUrlGenerator();

return (
<IntlProvider locale={computedLanguage} messages={appMessages[computedLanguage]}>
<BrowserRouter basename={basename}>
Expand All @@ -39,7 +43,9 @@ function AppProvidersWithStore() {
<SnackRefRegisterer />
<CssBaseline />
<CardErrorBoundary>
<App />
<NotificationsProvider urls={urlMapper}>
<App />
</NotificationsProvider>
</CardErrorBoundary>
</SnackbarProvider>
</ThemeProvider>
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { NotificationsUrlKeys, PREFIX_CONFIG_NOTIFICATION_WS } from '@gridsuite/commons-ui';
import { renderHook } from '@testing-library/react';
import { APP_NAME } from 'app/config/app-config';
import { useAppSelector } from 'app/store/store';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useNotificationsUrlGenerator } from 'shared/api/ws/use-notifications-url-generator';

vi.mock('app/store/store', () => ({
useAppSelector: vi.fn(),
}));

describe('useNotificationsUrlGenerator', () => {
let mockedUser: { id_token?: string } | null;

beforeEach(() => {
vi.clearAllMocks();
mockedUser = { id_token: 'token-123' };
Object.defineProperty(document, 'baseURI', {
configurable: true,
value: 'https://gridapp.test/',
});
vi.mocked(useAppSelector).mockImplementation(() => mockedUser);
});

it('returns an undefined config URL when the token is missing', () => {
mockedUser = null;

const { result } = renderHook(() => useNotificationsUrlGenerator());

expect(result.current).toEqual({
[NotificationsUrlKeys.CONFIG]: undefined,
});
});

it('builds a secure websocket URL from an https base URI', () => {
const { result } = renderHook(() => useNotificationsUrlGenerator());

const expectedConfigUrl = new URL(
`wss://gridapp.test/${PREFIX_CONFIG_NOTIFICATION_WS}/notify?appName=${APP_NAME}`
);
expectedConfigUrl.searchParams.set('access_token', 'token-123');

expect(result.current).toEqual({
[NotificationsUrlKeys.CONFIG]: expectedConfigUrl.toString(),
});
});
});
36 changes: 36 additions & 0 deletions src/shared/api/ws/use-notifications-url-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { NotificationsUrlKeys, PREFIX_CONFIG_NOTIFICATION_WS } from '@gridsuite/commons-ui';
import { APP_NAME } from 'app/config/app-config';
import { selectUser } from '../../../features/authentication/store/authentication.selectors';
import { useAppSelector } from '../../../app/store/store';

const getUrlWithToken = (baseUrl: string, tokenId?: string) => {
if (!tokenId) {
return undefined;
}
const url = new URL(baseUrl);
url.searchParams.set('access_token', tokenId);
return url.toString();
};

export const useNotificationsUrlGenerator = (): Partial<Record<NotificationsUrlKeys, string | undefined>> => {
// The websocket API doesn't allow relative urls
const webSocketBaseUrl = document.baseURI.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://');
const tokenId = useAppSelector(selectUser)?.id_token;

// return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId)
// it will be used to register listeners as soon as possible.
return {
[NotificationsUrlKeys.CONFIG]: getUrlWithToken(
`${webSocketBaseUrl}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({
appName: APP_NAME,
})}`,
tokenId
),
};
};
Loading
Loading