Skip to content

Commit

Permalink
feat: [OI-249] ogin refactoring
Browse files Browse the repository at this point in the history
- separete api functions
- separet login data hook
- separetd login buttons
- unit test
  • Loading branch information
mmoio committed Nov 29, 2024
1 parent 798490f commit 7d7ec3a
Show file tree
Hide file tree
Showing 16 changed files with 628 additions and 223 deletions.
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
});
});
});
55 changes: 55 additions & 0 deletions src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx
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 };
};
105 changes: 50 additions & 55 deletions src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
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

0 comments on commit 7d7ec3a

Please sign in to comment.