如何在端到端测试中自动化登录SAP IdP

huangapple go评论86阅读模式
英文:

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

问题

Our backend API的认证方法已经更换为OAuth 2.0。
现在我们想要编写不同业务用户的端对端认证测试。
我的想法是在BTP中编写测试代码,该代码将调用启用了OAuth的后端SAP服务。

刚刚按照使用Nightwatch和Cucumber的端对端测试教程进行操作。
但现在登录页面已更改为SAP IDP登录页面。
您知道如何自动化处理此IDP登录页面的登录和登出吗?

非常感谢!

IDP登录页面

我找不到IDP登录页面中用户名和密码的元素名称。

英文:

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.

答案1

得分: 1

以下是代码的中文翻译:

在JS SDK中我们使用puppeteer来自动获取令牌最后它还提供了用户名称和密码给IdP以下是一个示例

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('获取JWT失败');
  }
  logger.info(`获取JWT成功,用于${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);

  // 捕获所有失败的请求,如4xx..5xx状态代码
  page.on('requestfailed', request => {
    if (!request.failure()!.errorText.includes('ERR_ABORTED')) {
      logger.error(
        `url: ${request.url()}, errText: ${
          request.failure()?.errorText
        }, method: ${request.method()}`
      );
    }
  });

  // 捕获控制台日志错误
  page.on('pageerror', err => {
    logger.error(`页面错误: ${err.toString()}`);
  });

  page.on('request', request => {
    if (request.url().includes('/login/callback?code=')) {
      request.abort('中止');
    } 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(
      `在URL ${url}/${route}上未显示#j_username - 可能您在您的approuter的xs-security.json中有identityProvider吗?`
    );
  }
  await page.click('#j_username');
  await page.keyboard.type(userAndPwd.username);

  const passwordSelect = await page
    .waitForSelector('#j_password', { visible: true, timeout: 1000 })
    .catch(() => null);

  // 对于ldap IdP,需要多一步导航到第二个页面
  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('最终位置重定向不包含代码');
  }

  return parsedLocation.searchParams.get('code');
}

希望对你有所帮助。

英文:

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 &#39;https&#39;;
import { createLogger } from &#39;@sap-cloud-sdk/util&#39;;
import { Service, parseSubdomain } from &#39;@sap-cloud-sdk/connectivity/internal&#39;;
import puppeteer from &#39;puppeteer&#39;;
import axios from &#39;axios&#39;;
import { UserNamePassword } from &#39;./test-parameters&#39;;
const logger = createLogger(&#39;e2e-util&#39;);
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&lt;UserTokens&gt; {
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&lt;UserTokens&gt; {
let httpsAgent: https.Agent;
const params = new URLSearchParams();
params.append(&#39;redirect_uri&#39;, `${redirectUri}/login/callback`);
params.append(&#39;code&#39;, xsuaaCode);
params.append(&#39;grant_type&#39;, &#39;authorization_code&#39;);
params.append(&#39;client_id&#39;, xsuaaService.credentials.clientid);
if (xsuaaService.credentials.clientsecret) {
params.append(&#39;client_secret&#39;, 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(&#39;Failed to get the JWT&#39;);
}
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&lt;string&gt; {
const browser = await puppeteer.launch({
headless: true,
args: [&#39;--no-sandbox&#39;]
});
const page = await browser.newPage();
await page.setRequestInterception(true);
// Catch all failed requests like 4xx..5xx status codes
page.on(&#39;requestfailed&#39;, request =&gt; {
if (!request.failure()!.errorText.includes(&#39;ERR_ABORTED&#39;)) {
logger.error(
`url: ${request.url()}, errText: ${
request.failure()?.errorText
}, method: ${request.method()}`
);
}
});
// Catch console log errors
page.on(&#39;pageerror&#39;, err =&gt; {
logger.error(`Page error: ${err.toString()}`);
});
page.on(&#39;request&#39;, request =&gt; {
if (request.url().includes(&#39;/login/callback?code=&#39;)) {
request.abort(&#39;aborted&#39;);
} else {
request.continue();
}
});
try {
await Promise.all([
await page.goto(`${url}/${route}`),
await page.waitForSelector(&#39;#j_username&#39;, {
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(&#39;#j_username&#39;);
await page.keyboard.type(userAndPwd.username);
const passwordSelect = await page
.waitForSelector(&#39;#j_password&#39;, { visible: true, timeout: 1000 })
.catch(() =&gt; null);
// For ldap IdP one step in between with navigation to second page
if (!passwordSelect) {
await Promise.all([
page.waitForNavigation({ waitUntil: &#39;domcontentloaded&#39; }),
page.click(&#39;button[type=&quot;submit&quot;]&#39;)
]);
}
await page.click(&#39;#j_password&#39;);
await page.keyboard.type(userAndPwd.password);
const [authCodeResponse] = await Promise.all([
page.waitForResponse(response =&gt;
response.url().includes(&#39;oauth/authorize?&#39;)
),
page.click(&#39;button[type=&quot;submit&quot;]&#39;)
]);
await browser.close();
const parsedLocation = new URL(authCodeResponse.headers().location);
if (!parsedLocation.searchParams.get(&#39;code&#39;)) {
throw new Error(&#39;Final location redirect did not contain a code&#39;);
}
return parsedLocation.searchParams.get(&#39;code&#39;);
}

Best
Frank

huangapple
  • 本文由 发表于 2023年2月19日 11:55:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/75497893.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定