Many have experience with generic automated tests with Puppeteer. This is mostly about imitating user behavior on web pages and asserting results. But what if you need to test representation of errored state, which you cannot (or don’t know how to) trigger by a user action? For example, we have a form. When user submits the app sends a POST request to the server and shows success messages as soon as the server responds with 200 OK. That is the expected behavior. Yet, in real world the server may happened to be down or respond with Internal Error. Imagine the app covers gratefully this case. But how we would reproduce it with Puppeteer?
Given that Puppeteer provides a lot of possibilities on handling HTTP/S requests, I came up with the following solution. While submitting the form I mock the server response with my own.
Basically I set up an interceptor, which replaces the next request matching a given URL fragment with custom server response:
await bs.mockRequest( "response.json", { "GET", "500 Internal Server Error", "application/javascript", "{ \"status\": \"FAIL\" }", [] });
While it’s watching the network, I can submit the form, e.g. by emulating click on the submit button. The following request will be replaced so that the app under test would treat it as in the case of real server error.
Here the source code of the mockRequest:
class BrowserSession {
// obtaining page context from Puppeteer
// #see https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-puppeteerlaunchoptions
async setup( options ) {
this.browser = await puppeteer.launch( options );
this.context = await this.browser.createIncognitoBrowserContext();
this.page = await this.context.newPage();
}
/**
* Intercept and replace request
* #param {String} url - URL substring to match
* #param {Object} newRespond
*/
async mockRequest( url, { method, status, contentType, newBody, headers }) {
const session = await this.page.target().createCDPSession(),
patterns = [ `*${ url }*` ];
await session.send( "Network.enable" );
await session.send( "Network.setRequestInterception", {
patterns: patterns.map( pattern => ({
urlPattern: pattern,
interceptionStage: "HeadersReceived"
}))
});
session.on( "Network.requestIntercepted", async ({ interceptionId, request, responseHeaders, resourceType }) => {
if ( ( method || "GET" ) !== request.method.toUpperCase() ) {
await session.send( "Network.continueInterceptedRequest", { interceptionId });
return;
}
const newHeaders = [
"Date: " + ( new Date() ).toUTCString(),
"Connection: closed",
"Content-Length: " + newBody.length,
"Content-Type: " + contentType,
...headers
];
await session.send( "Network.continueInterceptedRequest", {
interceptionId,
rawResponse: btoa( `HTTP/1.1 ${ status }\r\n`
+ newHeaders.join('\r\n') + '\r\n\r\n' + newBody )
});
session.detach();
});
}
}
Related
I was trying to test sign-in page of our app. I am using cypress to test Vuejs frontend works with AspNet Api. When I click on the signin button on chrome it makes following requests and visits the homepage "localhost:44389"
first request from Chrome
second request from Chrome
if I want to simulate it on cypress it sends same request but get 302 from second request instead of 200.
first request from Cypress
second request from Cypress
Can someone help me to find out the problem?
Cypress.Commands.add('IdentityServerAPILogin', (email, password) => {
console.log("SERVER_URL is called. Server: " + Cypress.env('SERVER_URL'));
cy.request({
method: 'GET',
url: Cypress.env('SERVER_URL'),
failOnStatusCode: false,
headers: {
'Cookie': '...coookies...'
}
})
.then(response => {
if (response.status == 401){
console.log ("Check for NOnce");
console.dir(response, { depth: null });
const requestURL = response.allRequestResponses[1]["Request URL"]
console.dir(requestURL, { depth: null })
//const signInPage = (response.redirects[0]).replace('302: ', '');
const signInPage = (response.redirects[1]).replace('302: ', '');
console.log("signInPage:" + signInPage);
const nOnceObj = response.allRequestResponses[0]["Response Headers"];
console.dir(nOnceObj, { depth: null });
const nOnce = nOnceObj["set-cookie"][0];
console.log("Nonce:" + nOnce);
cy.visit({
method: 'GET',
url: signInPage,
failOnStatusCode: false,
headers: {
//'Cookie': nOnce
}
})
// TODO: Create all needed tests later to test sign in
cy.get('#username').type(email)
cy.get('#password').type(password)
// TODO: Waiting for to click signIn button. When I call the click() method I get infinite loop!!
cy.get('.ping-button')
// .click()
// .then((response_login) => {
// console.log("Status REsponse_Login: "+ response_login);
// console.dir(response_login, { depth: null });
// if (response_login.status == 401){
// cy.visit(Cypress.env('SERVER_URL'))
// }
// })
}else
cy.visit(Cypress.env('SERVER_URL'))
})
console.log("vorbei");
});
Just figured out Cypress is not able to get Cookies from .../signin-oidc, because there is an error as in the photo below
Asking kindly for a solution. I am not allowed to make changes on authorization service. Looking for a possibility around cypress.
I'm trying to make an app that can send payments to PayBill numbers with Safaricom's "Lipa Na M-Pesa" (a Kenyan thing). The call is a POST request to URL:
https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest
with header:
{
'Host': 'sandbox.safaricom.co.ke',
'Authorization': 'Bearer ${await mpesaAccessToken}',
'Content-Type': 'application/json',
}
and body:
{
"BusinessShortCode": "$businessShortCode",
"Password": "${generateLnmPassword(timeStamp)}",
"Timestamp": "$timeStamp",
"TransactionType": "CustomerPayBillOnline",
"Amount": "10",
"PartyA": "$userPhoneNumber",
"PartyB": "$businessShortCode",
"PhoneNumber": "$userPhoneNumber",
"CallBackURL": "?????????????????????????????",
"AccountReference": "account",
"TransactionDesc": "test",
}
I've received an access token, generated a password and made the call successfully, except for that CallBackURL thing... The M-Pesa docs describe their callback like this:
CallBackURL
This is the endpoint where you want the results of the transaction delivered. Same rules for Register URL API callbacks apply.
all API callbacks from transactional requests are POST requests, do not expect GET requests for callbacks. Also, the data is not formatted into application/x-www-form-urlencoded format, it is application/json, so do not expect the data in the usual POST fields/variables of your language, read the results directly from the incoming input stream.
(More info here, but you may need to be logged in: https://developer.safaricom.co.ke/get-started see "Lipa na M-Pesa")
My app is hosted on Firebase Cloud Firestore. Is there any way I can create a callback URL with them that will receive their callback as a document in a Firestore collection?...
Or would this be impossible, given that they would need authorization tokens and stuff to do so... and I can't influence what headers and body M-Pesa will send?
(PS Btw, I code in Flutter/Dart so plz don't answer in Javascript or anything! I'll be clueless... :p Flutter/Dart or just plain text will be fine. Thanks!)
Is there any way I can create a callback URL with them that will
receive their callback as a document in a Firestore collection?...
The most common way to do that in the Firebase ecosystem is to write an HTTPS Cloud Function that will be called by the Safaricom service.
Within the Cloud Function you will be able to update the Firestore document, based on the content of the POST request.
Something like:
exports.safaricom = functions.https.onRequest((req, res) => {
// Get the header and body through the req variable
// See https://firebase.google.com/docs/functions/http-events#read_values_from_the_request
return admin.firestore().collection('...').doc('...').update({ foo: bar })
.then(() => {
res.status(200).send("OK");
})
.catch(error => {
// ...
// See https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3
})
});
I did note that you ask us to not "answer in Javascript or anything" but in Flutter/Dart, but I don't think you will able to implement that in Flutter: you need to implement this webhook in an environment that you fully control and that exposes an API endpoint, like your own server or a Cloud Function.
Cloud Functions may seem complex at first sight, but implementing an HTTPS Cloud Functions is not that complicated. I suggest you read the Get Started documentation and watch the three videos about "JavaScript Promises" from the Firebase video series, and if you encounter any problem, ask a new question on SO.
Cloud functions are not Dart-based.
See below solution;
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const parse = require("./parse");
admin.initializeApp();
exports.lmno_callback_url = functions.https.onRequest(async (req, res) => {
const callbackData = req.body.Body.stkCallback;
const parsedData = parse(callbackData);
let lmnoResponse = admin.firestore().collection('lmno_responses').doc('/' + parsedData.checkoutRequestID + '/');
let transaction = admin.firestore().collection('transactions').doc('/' + parsedData.checkoutRequestID + '/');
let wallets = admin.firestore().collection('wallets');
if ((await lmnoResponse.get()).exists) {
await lmnoResponse.update(parsedData);
} else {
await lmnoResponse.set(parsedData);
}
if ((await transaction.get()).exists) {
await transaction.update({
'amount': parsedData.amount,
'confirmed': true
});
} else {
await transaction.set({
'moneyType': 'money',
'type': 'deposit',
'amount': parsedData.amount,
'confirmed': true
});
}
let walletId = await transaction.get().then(value => value.data().toUserId);
let wallet = wallets.doc('/' + walletId + '/');
if ((await wallet.get()).exists) {
let balance = await wallet.get().then(value => value.data().moneyBalance);
await wallet.update({
'moneyBalance': parsedData.amount + balance
})
} else {
await wallet.set({
'moneyBalance': parsedData.amount
})
}
res.send("Completed");
});
Parse function.
const moment = require("moment");
function parse(responseData) {
const parsedData = {};
parsedData.merchantRequestID = responseData.MerchantRequestID;
parsedData.checkoutRequestID = responseData.CheckoutRequestID;
parsedData.resultDesc = responseData.ResultDesc;
parsedData.resultCode = responseData.ResultCode;
if (parsedData.resultCode === 0) {
responseData.CallbackMetadata.Item.forEach(element => {
switch (element.Name) {
case "Amount":
parsedData.amount = element.Value;
break;
case "MpesaReceiptNumber":
parsedData.mpesaReceiptNumber = element.Value;
break;
case "TransactionDate":
parsedData.transactionDate = moment(
element.Value,
"YYYYMMDDhhmmss"
).unix();
break;
case "PhoneNumber":
parsedData.phoneNumber = element.Value;
break;
}
});
}
return parsedData;
}
module.exports = parse;
my first question to the community out here!
i'm working on an app which does communicates to the API in the following way
step1: create request options, add request payload --> Post request to API
API responds with a request ID
Step2: update request options, send request ID as payload --> post request to API
final response: response.json
Now the final response can take a bit of time, depending on the data requested.
this can take from anywhere between 4 to 20 seconds on an average.
How do i chain these requests using observables, i've tried using switchmap and failed (as below) but not sure how do i add a interval?
Is polling every 4 second and unsubscribing on response a viable solution? how's this done in the above context?
Edit1:
End goal: i'm new to angular and learning observables, and i'm looking to understand what is the best way forward.. does chaining observable help in this context ? i.e after the initial response have some sort of interval and use flatMap
OR use polling with interval to check if report is ready.
Here's what i have so far
export class reportDataService {
constructor(private _http: Http) { }
headers: Headers;
requestoptions: RequestOptions;
payload: any;
currentMethod: string;
theCommonBits() {
//create the post request options
// headers, username, endpoint
this.requestoptions = new RequestOptions({
method: RequestMethod.Post,
url: url,
headers: newheaders,
body: JSON.stringify(this.payload)
})
return this.requestoptions;
}
// report data service
reportService(payload: any, method: string): Observable<any> {
this.payload = payload;
this.currentMethod = method;
this.theCommonBits();
// fetch data
return this._http.request(new Request(this.requestoptions))
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body || {};
}
private handleError(error: any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
in my component
fetchData() {
this._reportService.reportService(this.payload, this.Method)
.switchMap(reportid => {
return this._reportService.reportService(reportid, this.newMethod)
}).subscribe(
data => {
this.finalData = data;
console.info('observable', this.finalData)
},
error => {
//console.error("Error fetcing data!");
return Observable.throw(error);
}
);
}
What about using Promise in your service instead of Observable, and the .then() method in the component. You can link as much .then() as you want to link actions between them.
I've found a lot of tutorials that have gotten me as far as logging in with OAuth in Meteor / MeteorAngular. Unfortunately, they've never gotten me as far as successfully accessing the Microsoft graph API. I know I need to (somehow) transform my user access token into a bearer token, but I haven't found any guides on how to do that in Meteor -- and the sample Node apps I've found to do similar things don't run out of the box, either. I have some routes that should return the data I need, I just can't quite seem to hit them.
Meteor + Microsoft... is darned near nonexistent, as far as I can tell
lol you aren't wrong. However as one of the primary engineers behind Sidekick AI (a scheduling app integrating with Microsoft+Google calendar built with Meteor) I have some very practical examples of using said access_token to work with the Microsoft Graph API with Meteor.
Here's some examples of functions I've written which use the access_token to communicate with the Graph API to work with calendars. Notably the line that matters is where I'm setting
Authorization: `Bearer ${sources.accessToken} // Sources just being the MongoDB collection we store that info in
NOTE: if you need more I'd be happy to provide but didn't want to clutter this initially
NOTE: wherever you see "Outlook" it really should say "Microsoft"; that's just a symptom of uninformed early development.
/**
* This function will use the Meteor HTTP package to make a POST request
* to the Microsoft Graph Calendar events endpoint using an access_token to create a new
* Event for the connected Outlook account
*
* #param { String } sourceId The Sources._id we are using tokens from for the API request
*
* #param { Object } queryData Contains key value pairs which will describe the new Calendar
* Event
*
* #returns { Object } The newly created Event Object
*/
export const createOutlookCalendarEvent = async (
sourceId: string,
queryData: {
subject: any
start: {
dateTime: any
timeZone: string
}
end: {
dateTime: any
timeZone: string
}
isOnlineMeeting: any
body: {
content: string
contentType: string
},
attendees: {
emailAddress: {
address: string
name: string
}
}[]
}
): Promise<object> => {
await attemptTokenRefresh(sourceId)
const sources = Sources.findOne({ _id: sourceId })
const options = {
headers: {
Accept: `application/json`,
Authorization: `Bearer ${sources.accessToken}`,
},
data: queryData,
}
return new Promise((resolve, reject) => {
HTTP.post(`${OauthEndpoints.Outlook.events}`, options, (error, response) => {
if (error) {
reject(handleError(`Failed to create a new Outlook Calendar event`, error, { options, sourceId, queryData, sources }))
} else {
resolve(response.data)
}
})
})
}
/**
* This function will use the Meteor HTTP package to make a GET request
* to the Microsoft Graph Calendars endpoint using an access_token obtained from the
* Microsoft common oauth2 v2.0 token endpoint. It retrieves Objects describing the calendars
* under the connected account
*
* #param sourceId The Sources._id we are using tokens from for the API request
*
* #returns Contains Objects describing the user's connected account's calendars
*/
export const getCalendars = async (sourceId: string): Promise<Array<any>> => {
await attemptTokenRefresh(sourceId)
const sources = Sources.findOne({ _id: sourceId })
const options = {
headers: {
Accept: `application/json`,
Authorization: `Bearer ${sources.accessToken}`,
},
}
return new Promise((resolve, reject) => {
HTTP.get(OauthEndpoints.Outlook.calendars, options, (error, response) => {
if (error) {
reject(handleError(`Failed to retrieve the calendars for a user's connected Outlook account`, error, { options, sources, sourceId }))
} else {
resolve(response.data.value)
}
})
})
}
// reference for the afore-mentioned `OauthEndpoints`
/**
* Object holding endpoints for our Oauth integrations with Google,
* Microsoft, and Zoom
*/
export const OauthEndpoints = {
...
Outlook: {
auth: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?',
token: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
calendars: 'https://graph.microsoft.com/v1.0/me/calendars',
calendarView: 'https://graph.microsoft.com/v1.0/me/calendar/calendarView',
events: 'https://graph.microsoft.com/v1.0/me/calendar/events',
messages: 'https://graph.microsoft.com/v1.0/me/messages',
sendMail: 'https://graph.microsoft.com/v1.0/me/sendMail',
},
...
}
On my .NET Web API 2 server, I am using OWIN for authentication. I have followed Taiseer's tutorial and successfully implemented an access token refresh mechanism.
I would like to know if there are any impacts on anything if clients refresh their access tokens frequently, e.g. refresh once every 5 minutes on average.
I am asking this question because I have a button on my page, when user clicks it, the data on that page is sent to different endpoints. These endpoints are marked with the attribute [Authorize].
Previously, when I send a request to a single protected endpoint, I can check if the response is 401 (unauthorized). If so, I can refresh the user's access token first, then resend the rejected request with the new token. However, I don't know how can the same thing be done this time, as there are so many requests being sent at once. The aforementioned method is implemented in my AngularJS interceptor. It can handle a single but not multiple rejected unauthorized requests.
FYI, here is the code for my interceptor, which is found and modified from a source on GitHub.
app.factory('authInterceptor', function($q, $injector, $location, localStorageService) {
var authInterceptor = {};
var $http;
var request = function(config) {
config.headers = config.headers || {};
var jsonData = localStorageService.get('AuthorizationData');
if (jsonData) {
config.headers.Authorization = 'Bearer ' + jsonData.token;
}
return config;
}
var responseError = function(rejection) {
var deferred = $q.defer();
if (rejection.status === 401) {
var authService = $injector.get('authService');
authService.refreshToken().then(function(response) {
_retryHttpRequest(rejection.config, deferred);
}, function() {
authService.logout();
$location.path('/login');
deferred.reject(rejection);
});
} else {
deferred.reject(rejection);
}
return deferred.promise;
}
var _retryHttpRequest = function(config, deferred) {
$http = $http || $injector.get('$http');
$http(config).then(function(response) {
deferred.resolve(response);
}, function(response) {
deferred.reject(response);
});
}
authInterceptor.request = request;
authInterceptor.responseError = responseError;
return authInterceptor;
});