diff --git a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx
new file mode 100644
index 0000000000..cd0b7dfdd1
--- /dev/null
+++ b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx
@@ -0,0 +1,245 @@
+import React from 'react';
+import '@testing-library/jest-dom/extend-expect';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from '@tanstack/react-query';
+import userEvent from '@testing-library/user-event';
+import {
+ act,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+
+import { Provider } from 'react-redux';
+import { getMockStore, enterpriseId } from '../testutils';
+import { features } from '../../../../config';
+import NewExistingSSOConfigs from '../NewExistingSSOConfigs';
+import { SSOConfigContext, SSO_INITIAL_STATE } from '../SSOConfigContext';
+import LmsApiService from '../../../../data/services/LmsApiService';
+
+const queryClient = new QueryClient({
+ queries: {
+ retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis.
+ },
+});
+
+jest.mock('../../utils');
+jest.mock('../../../../data/services/LmsApiService');
+const mockSetRefreshBool = jest.fn();
+
+const initialStore = {
+ portalConfiguration: {
+ enterpriseId,
+ enterpriseSlug: 'sluggy',
+ enterpriseName: 'sluggyent',
+ contactEmail: 'foobar',
+ },
+};
+const store = getMockStore({ contactEmail: 'foobar', ...initialStore });
+const inactiveConfig = [
+ {
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: false,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: '2022-05-12T19:51:25Z',
+ validated_at: '2022-06-12T19:51:25Z',
+ submitted_at: '2022-04-12T19:51:25Z',
+ },
+];
+const activeConfig = [
+ {
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: true,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: '2022-05-12T19:51:25Z',
+ validated_at: '2022-06-12T19:51:25Z',
+ submitted_at: '2022-04-12T19:51:25Z',
+ },
+];
+const unvalidatedConfig = [
+ {
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: true,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: '2022-04-12T19:51:25Z',
+ validated_at: null,
+ submitted_at: '2022-04-12T19:51:25Z',
+ },
+];
+const inProgressConfig = [
+ {
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: false,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: '2021-04-12T19:51:25Z',
+ validated_at: null,
+ submitted_at: '2022-04-12T19:51:25Z',
+ },
+];
+const notConfiguredConfig = [
+ {
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: false,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: null,
+ validated_at: null,
+ submitted_at: '2022-04-12T19:51:25Z',
+ },
+];
+
+jest.mock('../data/actions');
+jest.mock('../../utils');
+const entryType = 'direct';
+const metadataURL = 'https://foobar.com';
+const entityID = 'foobar';
+const publicKey = 'abc123';
+const ssoUrl = 'https://foobar.com';
+const mockCreateOrUpdateIdpRecord = jest.fn();
+const mockHandleEntityIDUpdate = jest.fn();
+const mockHandleMetadataEntryTypeUpdate = jest.fn();
+jest.mock('../hooks', () => {
+ const originalModule = jest.requireActual('../hooks');
+ return {
+ ...originalModule,
+ useIdpState: () => ({
+ entryType,
+ metadataURL,
+ entityID,
+ publicKey,
+ ssoUrl,
+ createOrUpdateIdpRecord: mockCreateOrUpdateIdpRecord,
+ handleEntityIDUpdate: mockHandleEntityIDUpdate,
+ handleMetadataEntryTypeUpdate: mockHandleMetadataEntryTypeUpdate,
+ }),
+ useExistingSSOConfigs: () => [[{ hehe: 'haha' }], null, true],
+ };
+});
+
+const mockSetProviderConfig = jest.fn();
+const contextValue = {
+ ...SSO_INITIAL_STATE,
+ setCurrentError: jest.fn(),
+ currentError: null,
+ dispatchSsoState: jest.fn(),
+ ssoState: {
+ idp: {
+ metadataURL: '',
+ entityID: '',
+ entryType: '',
+ isDirty: false,
+ },
+ serviceprovider: {
+ isSPConfigured: false,
+ },
+ refreshBool: false,
+ providerConfig: {
+ id: 1337,
+ },
+ },
+ setProviderConfig: mockSetProviderConfig,
+ setRefreshBool: jest.fn(),
+};
+
+const setupNewExistingSSOConfigs = (configs) => {
+ features.AUTH0_SELF_SERVICE_INTEGRATION = true;
+ return render(
+
+
+
+
+
+
+
+
+ ,
+ );
+};
+
+describe('New Existing SSO Configs tests', () => {
+ afterEach(() => {
+ features.AUTH0_SELF_SERVICE_INTEGRATION = false;
+ jest.clearAllMocks();
+ });
+ test('checks and sets in progress configs', async () => {
+ setupNewExistingSSOConfigs(inProgressConfig);
+ expect(
+ screen.queryByText(
+ 'Your SSO Integration is in progress',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('checks and sets not configured configs', async () => {
+ setupNewExistingSSOConfigs(notConfiguredConfig);
+ expect(
+ screen.queryByText(
+ 'Your SSO Integration is in progress',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('checks and sets validated configs', async () => {
+ setupNewExistingSSOConfigs(activeConfig);
+ expect(
+ screen.queryByText(
+ 'Your SSO integration is live!',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('checks and sets un-validated configs', async () => {
+ setupNewExistingSSOConfigs(unvalidatedConfig);
+ expect(
+ screen.queryByText(
+ 'You need to test your SSO connection',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('polls for finished configs', async () => {
+ const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords');
+ spy.mockImplementation(() => Promise.resolve({
+ data: [{
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: true,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: '2022-05-12T19:51:25Z',
+ validated_at: '2022-06-12T19:51:25Z',
+ submitted_at: '2022-04-12T19:51:25Z',
+ }],
+ }));
+ setupNewExistingSSOConfigs(inProgressConfig);
+ expect(
+ screen.queryByText(
+ 'Your SSO Integration is in progress',
+ ),
+ ).toBeInTheDocument();
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
+ expect(mockSetRefreshBool).toHaveBeenCalledTimes(2);
+ });
+ test('enabling config sets loading and renders skeleton', async () => {
+ const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord');
+ spy.mockImplementation(() => Promise.resolve({}));
+ setupNewExistingSSOConfigs(inactiveConfig);
+ const button = screen.getByTestId('existing-sso-config-card-enable-button');
+ act(() => {
+ userEvent.click(button);
+ });
+ expect(spy).toBeCalledTimes(1);
+ await waitFor(() => expect(
+ screen.queryByTestId(
+ 'sso-self-service-skeleton',
+ ),
+ ).toBeInTheDocument());
+ });
+});
diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx
new file mode 100644
index 0000000000..c62c7972ac
--- /dev/null
+++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx
@@ -0,0 +1,165 @@
+import React from 'react';
+import '@testing-library/jest-dom/extend-expect';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { Provider } from 'react-redux';
+import { render, screen, waitFor } from '@testing-library/react';
+import { SSOConfigContext, SSO_INITIAL_STATE } from '../SSOConfigContext';
+import { getMockStore, initialStore } from '../testutils';
+import NewSSOConfigAlerts from '../NewSSOConfigAlerts';
+
+const store = getMockStore({ contactEmail: 'foobar', ...initialStore });
+const mockSetProviderConfig = jest.fn();
+const contextValue = {
+ ...SSO_INITIAL_STATE,
+ setCurrentError: jest.fn(),
+ currentError: null,
+ dispatchSsoState: jest.fn(),
+ ssoState: {
+ idp: {
+ metadataURL: '',
+ entityID: '',
+ entryType: '',
+ isDirty: false,
+ },
+ serviceprovider: {
+ isSPConfigured: false,
+ },
+ refreshBool: false,
+ providerConfig: {
+ id: 1337,
+ },
+ },
+ setProviderConfig: mockSetProviderConfig,
+ setRefreshBool: jest.fn(),
+};
+
+describe('New SSO Config Alerts Tests', () => {
+ test('displays inProgress alert properly', async () => {
+ render(
+
+
+
+ ,
+
+
+ ,
+ );
+ expect(
+ screen.queryByText(
+ 'Your SSO Integration is in progress',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ 'You need to test your SSO connection',
+ ),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ 'Your SSO integration is live!',
+ ),
+ ).not.toBeInTheDocument();
+ });
+ test('inProgress alert accounts for if configured before', () => {
+ render(
+
+
+
+ ,
+
+
+ ,
+ );
+ expect(
+ screen.getByText(
+ 'five minutes',
+ { exact: false },
+ ),
+ ).toBeInTheDocument();
+ });
+ test('displays untested alert properly', () => {
+ render(
+
+
+
+ ,
+
+
+ ,
+ );
+ expect(
+ screen.queryByText(
+ 'You need to test your SSO connection',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ 'Your SSO integration is live!',
+ ),
+ ).not.toBeInTheDocument();
+ });
+ test('displays live alert properly', () => {
+ render(
+
+
+
+ ,
+
+
+ ,
+ );
+ expect(
+ screen.queryByText(
+ 'Your SSO integration is live!',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('calls closeAlerts prop on close', async () => {
+ const mockCloseAlerts = jest.fn();
+ render(
+
+
+
+ ,
+
+
+ ,
+ );
+ await waitFor(() => {
+ userEvent.click(screen.getByText('Dismiss'));
+ }, []).then(() => {
+ expect(mockCloseAlerts).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx
new file mode 100644
index 0000000000..a953ecb752
--- /dev/null
+++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx
@@ -0,0 +1,222 @@
+import React from 'react';
+import '@testing-library/jest-dom/extend-expect';
+import userEvent from '@testing-library/user-event';
+import { act, render, screen } from '@testing-library/react';
+import NewSSOConfigCard from '../NewSSOConfigCard';
+import LmsApiService from '../../../../data/services/LmsApiService';
+
+describe('New SSO Config Card Tests', () => {
+ test('displays enabled and validated status icon properly', async () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-enabled-icon',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('displays not validated status icon properly', async () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-not-validated-icon',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('displays not validated status icon properly', async () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-not-active-icon',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('displays badges properly', async () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-badge-in-progress',
+ ),
+ ).toBeInTheDocument();
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-badge-disabled',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('displays configure button properly', async () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-configure-button',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('displays enable button properly', async () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByTestId(
+ 'existing-sso-config-card-enable-button',
+ ),
+ ).toBeInTheDocument();
+ });
+ test('handles kebob Delete dropdown option', async () => {
+ const spy = jest.spyOn(LmsApiService, 'deleteEnterpriseSsoOrchestrationRecord');
+ spy.mockImplementation(() => Promise.resolve({}));
+ render(
+
,
+ );
+ act(() => {
+ userEvent.click(screen.getByTestId('existing-sso-config-card-dropdown'));
+ });
+ act(() => {
+ userEvent.click(screen.getByTestId('existing-sso-config-delete-dropdown'));
+ });
+ expect(spy).toBeCalledTimes(1);
+ });
+ test('handles kebob Disable dropdown option', async () => {
+ const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord');
+ spy.mockImplementation(() => Promise.resolve({}));
+ render(
+
,
+ );
+ act(() => {
+ userEvent.click(screen.getByTestId('existing-sso-config-card-dropdown'));
+ });
+ act(() => {
+ userEvent.click(screen.getByTestId('existing-sso-config-disable-dropdown'));
+ });
+ expect(spy).toBeCalledTimes(1);
+ });
+});
diff --git a/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx b/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx
index 4a05d2b026..a46e4697d8 100644
--- a/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx
+++ b/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx
@@ -1,12 +1,18 @@
import {
act, render, screen, waitFor,
} from '@testing-library/react';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from '@tanstack/react-query';
import '@testing-library/jest-dom/extend-expect';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Provider } from 'react-redux';
import { HELP_CENTER_SAML_LINK } from '../../data/constants';
+import { features } from '../../../../config';
import SettingsSSOTab from '..';
import LmsApiService from '../../../../data/services/LmsApiService';
@@ -19,14 +25,24 @@ const initialStore = {
enterpriseId,
enterpriseSlug: 'sluggy',
enterpriseName: 'sluggyent',
+ contactEmail: 'foobar',
},
};
+const queryClient = new QueryClient({
+ queries: {
+ retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis.
+ },
+});
const mockStore = configureMockStore([thunk]);
const getMockStore = aStore => mockStore(aStore);
const store = getMockStore({ ...initialStore });
describe('SAML Config Tab', () => {
+ afterEach(() => {
+ features.AUTH0_SELF_SERVICE_INTEGRATION = false;
+ jest.clearAllMocks();
+ });
test('renders base page with correct text and help center link', async () => {
const aResult = () => Promise.resolve(1);
LmsApiService.getProviderConfig.mockImplementation(() => (
@@ -57,7 +73,7 @@ describe('SAML Config Tab', () => {
() => expect(mockSetHasSSOConfig).toBeCalledWith(false),
);
});
- test('page sets has valid sso config with valid configs ', async () => {
+ test('page sets has valid sso config with valid configs', async () => {
LmsApiService.getProviderConfig.mockImplementation(() => (
{ data: { results: [{ was_valid_at: '10/10/22' }] } }
));
@@ -70,4 +86,31 @@ describe('SAML Config Tab', () => {
() => expect(mockSetHasSSOConfig).toBeCalledWith(true),
);
});
+ test('page renders new sso self service tool properly', async () => {
+ features.AUTH0_SELF_SERVICE_INTEGRATION = true;
+ const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords');
+ spy.mockImplementation(() => Promise.resolve({
+ data: [{
+ uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca',
+ display_name: 'foobar',
+ active: true,
+ modified: '2022-04-12T19:51:25Z',
+ configured_at: '2022-05-12T19:51:25Z',
+ validated_at: '2022-06-12T19:51:25Z',
+ submitted_at: '2022-04-12T19:51:25Z',
+ }],
+ }));
+ await waitFor(() => render(
+
+
+
+
+ ,
+
+ ,
+ ));
+ expect(screen.queryByText(
+ 'Great news! Your test was successful and your new SSO integration is live and ready to use.',
+ )).toBeInTheDocument();
+ });
});
diff --git a/src/components/settings/SettingsTabs.jsx b/src/components/settings/SettingsTabs.jsx
index 8f471233c3..729ac106bc 100644
--- a/src/components/settings/SettingsTabs.jsx
+++ b/src/components/settings/SettingsTabs.jsx
@@ -1,4 +1,8 @@
import React, { useState, useMemo } from 'react';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from '@tanstack/react-query';
import {
Container,
Tabs,
@@ -27,6 +31,12 @@ import SettingsApiCredentialsTab from './SettingsApiCredentialsTab';
import { features } from '../../config';
import { updatePortalConfigurationEvent } from '../../data/actions/portalConfiguration';
+const queryClient = new QueryClient({
+ queries: {
+ retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis.
+ },
+});
+
const SettingsTabs = ({
enterpriseId,
enterpriseSlug,
@@ -80,10 +90,12 @@ const SettingsTabs = ({
eventKey={SETTINGS_TABS_VALUES.sso}
title={SETTINGS_TAB_LABELS.sso}
>
-
+
+
+
,
);
}
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 18f2cca502..b3c32f662d 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -46,7 +46,7 @@ class LmsApiService {
return LmsApiService.apiClient().get(enterpriseSsoOrchestrationFetchUrl);
}
- static listEnterpriseSsoOrchestration(enterpriseCustomerUuid) {
+ static listEnterpriseSsoOrchestrationRecords(enterpriseCustomerUuid) {
const enterpriseSsoOrchestrationListUrl = `${LmsApiService.enterpriseSsoOrchestrationUrl}`;
if (enterpriseCustomerUuid) {
return LmsApiService.apiClient().get(`${enterpriseSsoOrchestrationListUrl}?enterprise_customer=${enterpriseCustomerUuid}`);