英文:
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登录页面中用户名和密码的元素名称。
英文:
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!
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 '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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论