Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [OI-249] spinner on idps fetch #549

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vi.mock('./services/analyticsService', () => ({
}));

// Mock the components
vi.mock('./pages/login/Login', () => ({
vi.mock('./pages/login', () => ({
default: () => <div>Mocked Login Component</div>,
}));
vi.mock('./pages/logout/Logout', () => ({
Expand Down
2 changes: 1 addition & 1 deletion src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Login from './pages/login/Login';
import {
ROUTE_LOGIN,
ROUTE_LOGIN_ERROR,
Expand All @@ -7,6 +6,7 @@ import {
import { redirectToLogin } from './utils/utils';
import Logout from './pages/logout/Logout';
import { LoginError } from './pages/loginError/LoginError';
import Login from './pages/login';

const onLogout = () => <Logout />;
const onLoginError = () => <LoginError />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Mock, vi } from 'vitest';
import { useLoginData } from './useLoginData';
import { fetchBannerContent, getIdpList, getClientData } from '../services/api';
import { ENV } from '../utils/env';
import { renderHook, waitFor } from '@testing-library/react';

vi.mock('../services/api', () => ({
fetchBannerContent: vi.fn(),
getIdpList: vi.fn(),
getClientData: vi.fn(),
}));

describe('useLoginData', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('should fetch and set bannerContent, idpList, and clientData on mount', async () => {
const mockBanner = [
{ enable: true, severity: 'info', description: 'Test Banner' },
];
const mockIdpList = {
identityProviders: [{ entityID: 'test-idp' }],
richiediSpid: 'https://example.com/spid',
};
const mockClientData = {
clientID: 'test-client-id',
friendlyName: 'Test Client',
logoUri: 'https://example.com/logo.png',
policyUri: 'https://example.com/policy',
tosUri: 'https://example.com/tos',
};

// Mock API responses
(fetchBannerContent as Mock).mockResolvedValueOnce(mockBanner);
(getIdpList as Mock).mockResolvedValueOnce({ idps: mockIdpList });
(getClientData as Mock).mockResolvedValueOnce({
clientData: mockClientData,
});

// Render the hook
const { result } = renderHook(useLoginData);

await waitFor(() => {
expect(fetchBannerContent).toHaveBeenCalledWith(ENV.JSON_URL.ALERT);
expect(getIdpList).toHaveBeenCalledWith(ENV.JSON_URL.IDP_LIST);
expect(getClientData).toHaveBeenCalledWith(ENV.JSON_URL.CLIENT_BASE_URL);
});

// Check if the state is updated correctly
expect(result.current.bannerContent).toEqual(mockBanner);
expect(result.current.idpList).toEqual(mockIdpList);
expect(result.current.clientData).toEqual(mockClientData);
});

it('should not set state if API calls fail', async () => {
// Mock failed API responses
(fetchBannerContent as Mock).mockRejectedValueOnce(
new Error('Failed to fetch banner')
);
(getIdpList as Mock).mockRejectedValueOnce(
new Error('Failed to fetch IDP list')
);
(getClientData as Mock).mockRejectedValueOnce(
new Error('Failed to fetch client data')
);

const { result } = renderHook(useLoginData);

await waitFor(() => {
// Check that the state has not been set
expect(result.current.bannerContent).toBeUndefined();
expect(result.current.idpList).toEqual({
identityProviders: [],
richiediSpid: '',
});
expect(result.current.clientData).toBeUndefined();
});
});

it('should handle partial data successfully', async () => {
const mockBanner = [
{ enable: true, severity: 'info', description: 'Test Banner' },
];
const mockIdpList = {
identityProviders: [{ entityID: 'test-idp' }],
richiediSpid: 'https://example.com/spid',
};

// Mock API responses
(fetchBannerContent as Mock).mockResolvedValueOnce(mockBanner);
(getIdpList as Mock).mockResolvedValueOnce({ idps: mockIdpList });
(getClientData as Mock).mockResolvedValueOnce({ clientData: undefined }); // No client data

const { result } = renderHook(useLoginData);

await waitFor(() => {
// Verify that bannerContent and idpList are correctly set
expect(result.current.bannerContent).toEqual(mockBanner);
expect(result.current.idpList).toEqual(mockIdpList);
expect(result.current.clientData).toBeUndefined(); // clientData should be undefined
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import { ENV } from '../utils/env';
import { IdentityProviders } from '../utils/IDPS';
import {
type BannerContent,
type Client,
fetchBannerContent,
getIdpList,
getClientData,
} from '../services/api';

export const useLoginData = () => {
const [bannerContent, setBannerContent] = useState<Array<BannerContent>>();
const [idpList, setIdpList] = useState<IdentityProviders>({
identityProviders: [],
richiediSpid: '',
});
const [clientData, setClientData] = useState<Client>();

useEffect(() => {
const bannerRequest = fetchBannerContent(ENV.JSON_URL.ALERT);
const idpsRequest = getIdpList(ENV.JSON_URL.IDP_LIST);
const clientDataRequest = getClientData(ENV.JSON_URL.CLIENT_BASE_URL);

Promise.allSettled([idpsRequest, clientDataRequest, bannerRequest]).then(
([idpsResult, clientDataResult, bannerResult]) => {
if (idpsResult.status === 'fulfilled' && idpsResult.value.idps) {
setIdpList(idpsResult.value.idps);
} else {
console.error('Failed to fetch IDP list:', idpsResult.status);
}

if (
clientDataResult.status === 'fulfilled' &&
clientDataResult.value.clientData
) {
setClientData(clientDataResult.value.clientData);
} else {
console.error(
'Failed to fetch client data:',
clientDataResult.status
);
}

if (bannerResult.status === 'fulfilled' && bannerResult.value?.length) {
setBannerContent(bannerResult.value);
} else {
console.error('Failed to fetch banner content:', bannerResult.status);
}
}
);
}, []);

return { bannerContent, idpList, clientData };
};
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
/* eslint-disable functional/immutable-data */
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { afterAll, beforeAll, expect, Mock, test, vi } from 'vitest';

import { afterAll, beforeAll, afterEach, test, vi, Mock } from 'vitest';
import { ENV } from '../../utils/env';
import { i18nTestSetup } from '../../__tests__/i18nTestSetup';
import Login from './Login';

// Mock fetch
global.fetch = vi.fn();

import Login from '.';

// Constants for repeated strings
const SPID_LOGIN = 'spidButton';
const CIE_LOGIN = 'CIE Login';
const LOGIN_TITLE = 'Login Title';
const LOGIN_DESCRIPTION = 'Login Description';
const TEMPORARY_LOGIN_ALERT = 'Temporary Login Alert';
const ALERT_DESCRIPTION = 'This is a warning!';
const MOCK_IDP_ENTITY_ID = 'test-idp';
const MOCK_RICHIEDI_SPID_URL = 'https://example.com/spid';

// Setup translations
i18nTestSetup({
loginPage: {
title: LOGIN_TITLE,
description: LOGIN_DESCRIPTION,
loginBox: {
spidLogin: SPID_LOGIN,
cieLogin: CIE_LOGIN,
},
privacyAndCondition: {
text: 'terms: {{termsLink}} privacy: {{privacyLink}}',
},
temporaryLogin: {
alert: TEMPORARY_LOGIN_ALERT,
join: 'Join',
},
},
spidSelect: {
modalTitle: 'test modal',
},
});

// Clear mocks after each test
afterEach(() => {
vi.clearAllMocks();
});

beforeEach(() => {
// Mock fetch
global.fetch = vi.fn();
});

const oldWindowLocation = global.window.location;
Expand All @@ -28,22 +58,16 @@ afterAll(() => {
Object.defineProperty(window, 'location', { value: oldWindowLocation });
});

// Clear mocks after each test
afterEach(() => {
vi.clearAllMocks();
});

// Test cases
test('Renders Login component', () => {
render(<Login />);
expect(screen.getByText('loginPage.title')).toBeInTheDocument();
expect(screen.getByText(LOGIN_TITLE)).toBeInTheDocument();
expect(screen.getByText(LOGIN_DESCRIPTION)).toBeInTheDocument();
});

const mockWarning = 'This is a warning!';

test('Fetches and displays banner alerts', async () => {
// Mock the fetch response
const mockBannerResponse = [
{ enable: true, severity: 'warning', description: mockWarning },
{ enable: true, severity: 'warning', description: ALERT_DESCRIPTION },
];
(fetch as Mock).mockResolvedValueOnce({
json: vi.fn().mockResolvedValueOnce(mockBannerResponse),
Expand All @@ -52,7 +76,7 @@ test('Fetches and displays banner alerts', async () => {
render(<Login />);

await waitFor(() => {
expect(screen.getByText(mockWarning)).toBeInTheDocument();
expect(screen.getByText(ALERT_DESCRIPTION)).toBeInTheDocument();
});
});

Expand All @@ -61,16 +85,15 @@ test('Handles fetch error for alert message', async () => {

render(<Login />);

// Optionally check if an error message or warning is displayed
await waitFor(() => {
expect(screen.queryByText(mockWarning)).not.toBeInTheDocument();
expect(screen.queryByText(ALERT_DESCRIPTION)).not.toBeInTheDocument();
});
});

test('Fetches IDP list on mount', async () => {
const mockIDPListResponse = {
identityProviders: [{ entityID: 'test-idp' }],
richiediSpid: 'https://example.com/spid',
identityProviders: [{ entityID: MOCK_IDP_ENTITY_ID }],
richiediSpid: MOCK_RICHIEDI_SPID_URL,
};
(fetch as Mock).mockResolvedValueOnce({
json: vi.fn().mockResolvedValueOnce(mockIDPListResponse),
Expand All @@ -84,7 +107,7 @@ test('Fetches IDP list on mount', async () => {
});

test('Handles invalid client ID gracefully', async () => {
window.history.pushState({}, '', `?client_id=invalidId`);
window.history.pushState({}, '', '?client_id=invalidId');

render(<Login />);

Expand All @@ -93,46 +116,18 @@ test('Handles invalid client ID gracefully', async () => {
});
});

test('Clicking SPID button opens modal', () => {
render(<Login />);
const buttonSpid = document.getElementById('spidButton');
fireEvent.click(buttonSpid as HTMLElement);

expect(screen.getByRole('dialog')).toBeInTheDocument(); // Check if modal opens
});

test('Clicking CIE button redirects correctly', () => {
render(<Login />);
const buttonCIE = screen.getByRole('button', {
name: 'loginPage.loginBox.cieLogin',
});
const buttonCIE = screen.getByText(CIE_LOGIN);
fireEvent.click(buttonCIE);

expect(global.window.location.assign).toHaveBeenCalledWith(
`${ENV.URL_API.AUTHORIZE}?idp=${ENV.SPID_CIE_ENTITY_ID}`
);
});

test('Clicking terms and conditions link redirects correctly', () => {
render(<Login />);

const termsConditionLink = screen.getByText(
'loginPage.privacyAndCondition.terms'
);
fireEvent.click(termsConditionLink);

expect(global.window.location.assign).toHaveBeenCalledWith(
ENV.URL_FOOTER.TERMS_AND_CONDITIONS
);
});

test('Clicking privacy link redirects correctly', () => {
test('Displays temporary login alert if enabled', () => {
render(<Login />);

const privacyLink = screen.getByText('loginPage.privacyAndCondition.privacy');
fireEvent.click(privacyLink);

expect(global.window.location.assign).toHaveBeenCalledWith(
ENV.URL_FOOTER.PRIVACY_DISCLAIMER
);
const temporaryLoginAlert = screen.getByText(TEMPORARY_LOGIN_ALERT);
expect(temporaryLoginAlert).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, vi } from 'vitest';
import { CieButton, CieButtonProps } from './CieButton';
import { i18nTestSetup } from '../../../__tests__/i18nTestSetup';

describe('CieButton', () => {
const BUTTON_TEXT = 'CIE Login';
const onClickMock = vi.fn();

const renderComponent = (props: Partial<CieButtonProps> = {}) => {
i18nTestSetup({ 'loginPage.loginBox.cieLogin': BUTTON_TEXT });
render(<CieButton onClick={onClickMock} {...props} />);
};

it('renders the button with the correct text', () => {
renderComponent();
expect(
screen.getByRole('button', { name: BUTTON_TEXT })
).toBeInTheDocument();
});

it('displays the CIE icon', () => {
renderComponent();
const icon = screen.getByAltText('CIE Icon');
expect(icon).toBeInTheDocument();
expect(icon).toHaveAttribute('src', expect.stringContaining('CIEIcon'));
});

it('calls the onClick handler when clicked', async () => {
renderComponent();
const button = screen.getByRole('button', { name: BUTTON_TEXT });
await userEvent.click(button);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});
Loading
Loading