Search code examples
sap-cloud-sdk

How to automate the login on SAP IdP in an end-to-end test


Our backend API's auth method's been replaced to OAuth 2.0. Now we would like to write end2end auth testing with different business users. My idea is to write testing code in the BTP, which will call the backend OAuth enabled SAP service.

Just followed the e2e test tutorial using nightwatch and cucumber. But now the logon page has been changed to the SAP IDP logon page. Do you know how to automate the login&logout for this idp logon page?

Thanks a lot!

idp logon page

I could not find the element name of username and the password in the idp logon page.


Solution

  • In the JS SDK We use puppeteer to fetch a token programatically. In the end it also provides the user name and password to the IdP. Here is a sample:

    import https from 'https';
    import { createLogger } from '@sap-cloud-sdk/util';
    import { Service, parseSubdomain } from '@sap-cloud-sdk/connectivity/internal';
    import puppeteer from 'puppeteer';
    import axios from 'axios';
    import { UserNamePassword } from './test-parameters';
    
    const logger = createLogger('e2e-util');
    
    export interface UserTokens {
      access_token: string;
      refresh_token: string;
    }
    
    export interface GetJwtOption {
      redirectUrl: string;
      route: string;
      xsuaaService: Service;
      subdomain: string;
      userAndPwd: UserNamePassword;
    }
    
    export async function getJwt(options: GetJwtOption): Promise<UserTokens> {
      const { redirectUrl, route, userAndPwd, xsuaaService, subdomain } = options;
      const xsuaaCode = await getAuthorizationCode(redirectUrl, route, userAndPwd);
      return getJwtFromCode(xsuaaCode, redirectUrl, xsuaaService, subdomain);
    }
    
    export async function getJwtFromCode(
      xsuaaCode: string,
      redirectUri: string,
      xsuaaService: Service,
      subdomain: string
    ): Promise<UserTokens> {
      let httpsAgent: https.Agent;
    
      const params = new URLSearchParams();
      params.append('redirect_uri', `${redirectUri}/login/callback`);
      params.append('code', xsuaaCode);
      params.append('grant_type', 'authorization_code');
      params.append('client_id', xsuaaService.credentials.clientid);
    
      if (xsuaaService.credentials.clientsecret) {
        params.append('client_secret', xsuaaService.credentials.clientsecret);
        httpsAgent = new https.Agent();
      } else {
        httpsAgent = new https.Agent({
          cert: xsuaaService.credentials.certificate,
          key: xsuaaService.credentials.key
        });
      }
    
      const url = xsuaaService.credentials.clientsecret
        ? xsuaaService.credentials.url
        : xsuaaService.credentials.certurl;
      const subdomainProvider = parseSubdomain(url);
    
      const urlReplacedSubdomain = url.replace(subdomainProvider, subdomain);
    
      const response = await axios.post(
        `${urlReplacedSubdomain}/oauth/token`,
        params,
        {
          httpsAgent
        }
      );
    
      if (!response.data.access_token) {
        throw new Error('Failed to get the JWT');
      }
      logger.info(`Obtained JWT for ${redirectUri}.`);
      return {
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token
      };
    }
    
    async function getAuthorizationCode(
      url: string,
      route: string,
      userAndPwd: UserNamePassword
    ): Promise<string> {
      const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox']
      });
      const page = await browser.newPage();
      await page.setRequestInterception(true);
    
      // Catch all failed requests like 4xx..5xx status codes
      page.on('requestfailed', request => {
        if (!request.failure()!.errorText.includes('ERR_ABORTED')) {
          logger.error(
            `url: ${request.url()}, errText: ${
              request.failure()?.errorText
            }, method: ${request.method()}`
          );
        }
      });
    
      // Catch console log errors
      page.on('pageerror', err => {
        logger.error(`Page error: ${err.toString()}`);
      });
    
      page.on('request', request => {
        if (request.url().includes('/login/callback?code=')) {
          request.abort('aborted');
        } else {
          request.continue();
        }
      });
    
      try {
        await Promise.all([
          await page.goto(`${url}/${route}`),
          await page.waitForSelector('#j_username', {
            visible: true,
            timeout: 5000
          })
        ]);
      } catch (err) {
        throw new Error(
          `The #j_username did not show up on URL ${url}/${route} - perhaps you have the identityProvider in the xs-security.json of your approuter?`
        );
      }
      await page.click('#j_username');
      await page.keyboard.type(userAndPwd.username);
    
      const passwordSelect = await page
        .waitForSelector('#j_password', { visible: true, timeout: 1000 })
        .catch(() => null);
    
      // For ldap IdP one step in between with navigation to second page
      if (!passwordSelect) {
        await Promise.all([
          page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
          page.click('button[type="submit"]')
        ]);
      }
    
      await page.click('#j_password');
    
      await page.keyboard.type(userAndPwd.password);
      const [authCodeResponse] = await Promise.all([
        page.waitForResponse(response =>
          response.url().includes('oauth/authorize?')
        ),
        page.click('button[type="submit"]')
      ]);
    
      await browser.close();
    
      const parsedLocation = new URL(authCodeResponse.headers().location);
      if (!parsedLocation.searchParams.get('code')) {
        throw new Error('Final location redirect did not contain a code');
      }
    
      return parsedLocation.searchParams.get('code');
    }
    

    Best Frank