I'm making a request to a 3rd party API via NestJS's built in HttpService. I'm trying to simulate a scenario where the initial call to one of this api's endpoints might return an empty array on the first try. I'd like to use RxJS's retryWhen to hit the api again after a delay of 1 second. I'm currently unable to get the unit test to mock the second response however:
it('Retries view account status if needed', (done) => {
jest.spyOn(httpService, 'post')
.mockReturnValueOnce(of(failView)) // mock gets stuck on returning this value
.mockReturnValueOnce(of(successfulView));
const accountId = '0812081208';
const batchNo = '39cba402-bfa9-424c-b265-1c98204df7ea';
const response =client.viewAccountStatus(accountId, batchNo);
response.subscribe(
data => {
expect(data[0].accountNo)
.toBe('0812081208');
expect(data[0].companyName)
.toBe('Some company name');
done();
},
)
});
My implementation is:
viewAccountStatus(accountId: string, batchNo: string): Observable<any> {
const verificationRequest = new VerificationRequest();
verificationRequest.accountNo = accountId;
verificationRequest.batchNo = batchNo;
this.logger.debug(`Calling 3rd party service with batchNo: ${batchNo}`);
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = this.httpService.post(url, verificationRequest, config)
.pipe(
map(res => {
console.log(res.data); // always empty
if (res.status >= 400) {
throw new HttpException(res.statusText, res.status);
}
if (!res.data.length) {
this.logger.debug('Response was empty');
throw new HttpException('Account not found', 404);
}
return res.data;
}),
retryWhen(errors => {
this.logger.debug(`Retrying accountId: ${accountId}`);
// It's entirely possible the first call will return an empty array
// So we retry with a backoff
return errors.pipe(
delayWhen(() => timer(1000)),
take(1),
);
}),
);
return response;
}
When logging from inside the initial map, I can see that the array is always empty. It's as if the second mocked value never happens. Perhaps I also have a solid misunderstanding of how observables work and I should somehow be trying to assert against the SECOND value that gets emitted? Regardless, when the observable retries, we should be seeing that second mocked value, right?
I'm also getting
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
On each run... so I'm guessing I'm not calling done() in the right place.
I think the problem is that retryWhen(notifier) will resubscribe to the same source when its notifier emits.
Meaning that if you have
new Observable(s => {
s.next(1);
s.next(2);
s.error(new Error('err!'));
}).pipe(
retryWhen(/* ... */)
)
The callback will be invoked every time the source is re-subscribed. In your example, it will call the logic which is responsible for sending the request, but it won't call the post method again.
The source could be thought of as the Observable's callback: s => { ... }.
What I think you'll have to do is to conditionally choose the source, based on whether the error took place or not.
Maybe you could use mockImplementation:
let hasErr = false;
jest.spyOn(httpService, 'post')
.mockImplementation(
() => hasErr ? of(successView) : (hasErr = true, of(failView))
)
Edit
I think the above does not do anything different, where's what I think mockImplementation should look like:
let err = false;
mockImplementation(
() => new Observable(s => {
if (err) {
s.next(success)
}
else {
err = true;
s.next(fail)
}
})
)
Related
From response A (/list.json) my app receives a list of items. Based on the output of A, my app makes another set of requests B for individual items (/one.txt, /two.txt, ...).
Now in my test I want to make sure that all responses B return HTTP 200.
Waiting (cy.wait) for response A is fine. However, waiting for responses B is more difficult, because I have to start waiting just upon receiving response A where I learn about responses B.
I tried 2 options:
start waiting inside of cy.wait of response A - code,
start waiting outside of cy.wait of response A - code
Neither of those work. With option 1 I get
`cy.wait()` timed out waiting `5000ms` for the 1st request to the route: `one.txt`. No request ever occurred
And with option 2 I get a pass, even though /two.txt doesn't exist. Looks like cy.wait for responses B is added after the responses were received
Since all requests are triggered off the visit, and are dynamic, you need a single intercept that handles all requests.
To me that means adding some javascript and dynamic aliases.
// avoid status code 304, disable browser cache
Cypress.automation('remote:debugger:protocol', {
command: 'Network.clearBrowserCache'
})
describe('spec', () => {
it('test', () => {
let items = [];
cy.intercept('GET', '*', (req) => {
const slug = req.url.substring(req.url.lastIndexOf('/') + 1)
if (slug === 'list.json') {
req.alias = 'list'
}
if (items.includes(slug)) {
req.alias = 'item'
}
req.continue((res) => {
if (slug === 'list.json')) {
items = res.body;
}
})
})
cy.visit('https://demo-cypress.netlify.app');
cy.wait('#list') // wait for list
.then(() => { // now items is populated
for (let item of items) { // really just need the count
cy.wait('#item').then(interception => { // wait n-times
expect(interception.response.statusCode).to.eq(200);
})
}
})
})
})
I have been trying to grab an old response to assert it has a certain response.
The issue is that the same call is posted at the same time and I can only grab the second response.
I was just wondering if there was a way to grab both responses so I can read each body to make sure the correct posts are made
I have used the following
public assertMixPanelCall(call: string): void {
cy.intercept('POST', 'https://api-js.mixpanel.com/track/*', (req) => {
if (atob(req.body.replace('data=', '')).includes(`"event": "${call}"`)) {
req.alias = 'correctBody'
}
});
cy.wait('#correctBody');
}
So the response I get is the last response,
But I want to grab the penultimate response
I'm not seeing the complete picture, but I think you can use this pattern Intercepting a response
let responseCount = 0;
cy.intercept('POST', 'https://api-js.mixpanel.com/track/*', (req) => {
if (atob(req.body.replace('data=', '')).includes(`"event": "${call}"`)) {
req.continue((res) => {
responseCount++;
if (responseCount === 1) {
req.alias = 'penultimate'
}
if (responseCount === 2) {
req.alias = 'final'
}
})
}
});
cy.wait('#penultimate')
Not sure if dynamic aliasing works on a per-response basis.
There's also an undocumented alias suffix that lets you access the nth response
cy.wait('#correctBody'); // must wait first
cy.get('#correctBody.1'); // then get to access response history
// - not sure if you need #correctBody.0 or #correctBody.1
But I can't see why cy.wait('#correctBody') doesn't catch the first response, generally you need to issue the wait twice to get both responses. Anyway, there's some things to try out here.
So I found the solution
From wherever I want to start capturing
cy.intercept('POST', 'https://api-js.mixpanel.com/track/*').as('call');
generate array based on the number of calls previously I wish to check
const genArr = Array.from({length:noOfCalls},(v,k)=>k+1);
const calls = [];
cy.wrap(genArr).each(() => {
calls.push(`#${call}`)
})
make the call based on the amount of times I wish to post the response
cy.wait(calls).then(differentRequests => {
differentRequests.forEach(differentRequest => {
if(atob(differentRequest.request.body.replace('data=','')).includes(call)) {
pass = true
}
});
expect(pass).to.be.true
});
}
Got the solution from here
https://medium.com/slido-dev-blog/cypress-io-is-pretty-amazing-and-if-you-havent-used-it-yet-and-need-a-jump-start-i-recommend-my-66ee6ac5f8d9
I have a lambda function that returns a message to the client.
function replyToMessage (messageText,connectionId) {
const data = {message:messageText}
const params = {
ConnectionId : connectionId,
Data: Buffer.from(JSON.stringify(data))
}
return api.postToConnection(params).promise()
.then(data => {})
.catch(error => {console.log("error",error)})
}
This code is called once when the connection is made and I get a response to my client. When I call the function again with a different endpoint, it doesn't send a response to my client. However, when I call it a third time, I get the response to my client from the second call. Here's my switch when the Lambda function is called.
switch(route) {
case "$connect":
break
case "$disconnect":
break
case "connectTo":
await connectToService(JSON.parse(event.body).eventId,connectionId)
await replyToMessage("Connected eventId to connId",connectionId)
break
case "disconnectFrom":
await disConnectToService(JSON.parse(event.body).eventId,connectionId)
break
case "project":
responseItems = await getBroadcastIds (JSON.parse(event.body).eventId,JSON.parse(event.body).sourceId,connectionId)
console.log(responseItems)
responseItems.Items.forEach(async function(item) {
await replyToMessage(JSON.parse(event.body).sourceId,item.connectionId)
})
responseItems = []
break
default :
console.log("Unknown route", route)
The issue appears to be the async forEach loop. Switching to the following resolves the issue.
for (const item of responseItems.Items) {
console.log("Sending to:",item.connectionId);
await replyToMessage(JSON.parse(event.body).sourceId,item.connectionId)
}
See this post for the answer that led to this resolution. Using async/await with a forEach loop
I'm having trouble promises and observables. I have a handful of http requests which are defined in a package using promises. In the rest of my code I am using observables for various things, including other http calls. In one particular section I am checking to see if the user's bearer token is expired and if so then I get a new token and then proceed with the rest of the call.
if (!token || token.exp < Math.round((new Date()).getTime() / 1000)) {
from(this._store.refreshBearerToken())
.pipe(flatMap(resp => {
let newToken = resp.data;
newToken.exp = (new Date()).getTime() / 1000 + newToken.expires_in;
localStorage.setItem('token', JSON.stringify(newToken))
options = options || {};
options.headers = new HttpHeaders({
"Authorization": `${newToken.token_type} ${newToken.access_token}`,
"Content-Type": "application/json"
});
return this._http$.request<T>(method, url, options as Object).pipe(share());
}));
}
Bearer Token method:
async refreshBearerToken() {
const response = await this._q2.sources.requestExtensionData({
route: "refreshBearerToken"
});
console.log(response);
return response;
}
Since this._store.refreshBearerToken returns a promise I wrapped the call in a from to convert it to an observable. This compiles but when it runs I get "Cannot read property 'pipe' of undefined".
How can I convert this promise to an observable so that I can refresh the token and then continue with the rest of the call?
Edit:
I am importing from via import { Observable, from } from "rxjs";.
So, I thought the error was coming from the line .pipe(flatMap(resp =>... but I was wrong. The error is coming from the method which is calling this.
GetInitialLinkList(): Observable<Institution[]>
{
let base = { 'MemberId': localStorage.getItem('memberId') };
let ins = localStorage.getItem("initialInstitutionList");
if (ins)
{
return of(JSON.parse(ins));
}
return this._settingsService.get().pipe(
flatMap(settings =>
{
this._settings = settings;
return this._api.request<Institution[]>("Post", `${this._settings.mea}/GetInitialLinkList`, { body: base })
.pipe(
retry(1),
catchError(this.handleError)
)
.pipe(flatMap(instList =>
{
localStorage.setItem("initialInstitutionList", JSON.stringify(instList));
return of(instList);
}))
}));
}
and that is being subscribed to inside my component:
private GetLinkList()
{
this.showWaiting.emit(true);
this._data.GetInitialLinkList().subscribe((result) =>
{
this.initialList = result;
this.showWaiting.emit(false);
});
}
From what Brandon said (I forgot to return /facepalm...) I added the return so I have return from(this._store.refreshBearerToken()) which changed my error to
ERROR Error Code: undefined
Message: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
defaultErrorLogger # core.js:6014
Can you show the actual error and the line in the code that the error occurs on? Also show where and how you import from.
I notice your code snippet does not return the observable it builds up via from(...).pipe(...) nor does it subscribe to it. It might help to show how your code actually uses this observable.
I'm running into an issue with some code for an Ionic 3 app.
Basically, I have a list of objects that all have a unique id. The unique id for each object must be sent through a GET request so that we can get the appropriate data back for each object from the server. This has to be done on a per object basis; I can't bundle them into one request because there is no API endpoint for that.
Therefore the objects are all stored in an array, so I've been trying to loop through the array and call the provider for each one. Note that the provider is returning an observable.
Since the provider is an asynchronous function the promise will resolve before the loop is finished, unless I time out the promise resolution. This defeats the whole point of the promise.
What is the correct way that I should go about doing this so that the looping provider calls are done before the promise resolves?
If I have an inner promise to resolve when the looping is done, won't it also resolve prematurely?
I also read that it is bad to have a bunch of observables open. Should I instead return each observable as a promise using toPromise()?
Here is the code to build the data:
asyncBuildData() {
var promise = new Promise((resolve, reject) => {
let completedRequests = 0;
for (let i = 0; i < 10; i++) {
this.provider.getStuffById(listOfStuff[i].id).subscribe(val => {
list.push(val)
completedRequests++;
})
}
console.log('cp', completedRequests); // completedRequests = 0
setTimeout(() => {
console.log('cp', completedRequests); // completedRequests = 10
let done = true;
if (done) {
resolve('Done');
} else {
reject('Not done');
}
}, 1500)
})
return promise;
}
Code from Provider:
getStuffById(stuffId) {
let url = url + stuffId;
return this.http.get(url)
.map(res => res.json());
}
Even though you can't bundle them into one request, you can still bundle them into one observable, of which those requests are fired in parallel, using .forkJoin():
buildData$() {
let parallelRequests = listOfStuffs.map(stuff => this.provider.getStuffById(stuff.id));
return Observable.forkJoin([...parallelRequests]);
}
and then in your component, you can just call:
buildData$.subscribe(val=>{
//val is an array of all the results of this.provider.getStuffById()
list =val;
})
Note that Obersvable.forkJoin() will for all requests to complete before emitting any values.
If I understand correctly then the following code should get you on your way. This will execute a promise, one at a time, for each element in the array.
var ids = [1,2,3,4,5];
ids.reduce(function (promise, id) {
return promise.then(function () {
let url = url + id;
return this.http.get(url)
.map(res => res.json());
});
}, Promise.resolve()).then(function(last) {
// handle last result
}, function(err) {
// handle errors
});
I tested this with a jQuery post and replaced it with yours from Ionic. If it fails then let me know.