Mocking authentication when testing MSAL React Apps
Asked Answered
I

2

14

Our app is wrapped in the MSAL Authentication Template from @azure/msal-react in a standard way - key code segments are summarized below.

We would like to test app's individual components using react testing library (or something similar). Of course, when a React component such as SampleComponentUnderTest is to be properly rendered by a test as is shown in the simple test below, it must be wrapped in an MSAL component as well.

Is there a proper way to mock the MSAL authentication process for such purposes? Anyway to wrap a component under test in MSAL and directly provide test user's credentials to this component under test? Any references to useful documentation, blog posts, video, etc. to point us in the right direction would be greatly appreciated.

A Simple test

test('first test', () => {
  const { getByText } = render(<SampleComponentUnderTest />);
  const someText = getByText('A line of text');
  expect(someText).toBeInTheDocument();
});

Config

export const msalConfig: Configuration = {
  auth: {
    clientId: `${process.env.REACT_APP_CLIENT_ID}`,
    authority: `https://login.microsoftonline.com/${process.env.REACT_APP_TENANT_ID}`,
    redirectUri:
      process.env.NODE_ENV === 'development'
        ? 'http://localhost:3000/'
        : process.env.REACT_APP_DEPLOY_URL,
  },
  cache: {
    cacheLocation: 'sessionStorage',
    storeAuthStateInCookie: false,
  },
  system: {
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
          default:
            console.error(message);
        }
      },
    },
  },
};

Main app component

const msalInstance = new PublicClientApplication(msalConfig);

<MsalProvider instance={msalInstance}>
  {!isAuthenticated && <UnauthenticatedHomePage />}
  {isAuthenticated && <Protected />}
</MsalProvider>

Unauthenticated component

const signInClickHandler = (instance: IPublicClientApplication) => {
  instance.loginRedirect(loginRequest).catch((e) => {
    console.log(e);
  });
};

<UnauthenticatedTemplate>
  <Button onClick={() => signInClickHandler(instance)}>Sign in</Button>
</UnauthenticatedTemplate>

Protected component

<MsalAuthenticationTemplate
  interactionType={InteractionType.Redirect}
  errorComponent={ErrorComponent}
  loadingComponent={LoadingComponent}
>
    <SampleComponentUnderTest />
</MsalAuthenticationTemplate>
Inchworm answered 21/6, 2022 at 2:47 Comment(0)
J
13

I had the same issue as you regarding component's test under msal-react. It took me a couple of days to figure out how to implement a correct auth mock. That's why I've created a package you will find here, that encapsulates all the boilerplate code : https://github.com/Mimetis/msal-react-tester

Basically, you can do multiple scenaris (user is already logged, user is not logged, user must log in etc ...) in a couple of lines, without having to configure anything and of course without having to reach Azure AD in any cases:

describe('Home page', () => {

  let msalTester: MsalReactTester;
  beforeEach(() => {
    // new instance of msal tester for each test
    msalTester = new MsalReactTester();

    // spy all required msal things
    msalTester.spyMsal();
  });

  afterEach(() => {
    msalTester.resetSpyMsal();
  });

  test('Home page render correctly when user is logged in', async () => {

    msalTester.isLogged();

    render(
      <MsalProvider instance={msalTester.client}>
        <MemoryRouter>
          <Layout>
            <HomePage />
          </Layout>
        </MemoryRouter>
      </MsalProvider>,
    );

    await msalTester.waitForRedirect();

    let allLoggedInButtons = await screen.findAllByRole('button', { name: `${msalTester.activeAccount.name}` });
    expect(allLoggedInButtons).toHaveLength(2);
  });

  test('Home page render correctly when user logs in using redirect', async () => {

    msalTester.isNotLogged();
    render(
      <MsalProvider instance={msalTester.client}>
        <MemoryRouter>
          <Layout>
            <HomePage />
          </Layout>
        </MemoryRouter>
      </MsalProvider>,
    );

    await msalTester.waitForRedirect();

    let signin = screen.getByRole('button', { name: 'Sign In - Redirect' });
    userEvent.click(signin);

    await msalTester.waitForLogin();

    let allLoggedInButtons = await screen.findAllByRole('button', { name: `${msalTester.activeAccount.name}` });
    expect(allLoggedInButtons).toHaveLength(2);
  });


Jolynjolynn answered 4/7, 2022 at 12:46 Comment(3)
This is really great, thank you so much for putting this package together!Inchworm
Thank you for this. Resolved an issue I was struggling with today.Applaud
God bless you brother! Works like charm!Wacker
D
0

I am also curious about this, but from a slightly different perspective. I am trying to avoid littering the code base with components directly from msal in case we want to swap out identity providers at some point. The primary way to do this is to use a hook as an abstraction layer such as exposing isAuthenticated through that hook rather than the msal component library itself.

The useAuth hook would use the MSAL package directly. For the wrapper component however, I think we have to just create a separate component that either returns the MsalProvider OR a mocked auth provider of your choice. Since MsalProvider uses useContext beneath the hood I don't think you need to wrap it in another context provider.

Hope these ideas help while you are thinking through ways to do this. Know this isn't a direct answer to your question.

Debut answered 24/6, 2022 at 16:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.