How to obtain original XHR request with Sinon FakeXMLHttpRequest - sinon

Here's the example from docs:
{
setUp: function () {
this.xhr = sinon.useFakeXMLHttpRequest();
var requests = this.requests = [];
this.xhr.onCreate = function (xhr) {
requests.push(xhr);
};
},
tearDown: function () {
this.xhr.restore();
},
"test should fetch comments from server" : function () {
var callback = sinon.spy();
myLib.getCommentsFor("/some/article", callback);
assertEquals(1, this.requests.length);
this.requests[0].respond(200, { "Content-Type": "application/json" },
'[{ "id": 12, "comment": "Hey there" }]');
assert(callback.calledWith([{ id: 12, comment: "Hey there" }]));
}
}
This works well however it doesn't allow me to inspect what my code actually sends using the xhr.send() call so not very useful to me.
Because I am unit testing in a node env I do not have a real XMLHttpRequest object so something like nock will not work.
Sinon seems like it might but I am not finding what I need - is there a way to see what the res.send() call actually sends instead of faking that?

Solved. It is available by inspecting the this.request[0].requestBody element.
"test should fetch comments from server" : function () {
var callback = sinon.spy();
myLib.getCommentsFor("/some/article", callback);
var body = JSON.parse(this.requests[0].requestBody);
assert(body.XYX)...
}

Related

Problem sending POST body to the Firestore REST API

I want to create a new document in Firestore using the REST API.
Very good examples here using Axios to send the POST request with some fields:
https://www.jeansnyman.com/posts/google-firestore-rest-api-examples/
axios.post(
"https://firestore.googleapis.com/v1/projects/<PROJECTIDHERE>/databases/(default)/documents/<COLLECTIONNAME>",
{
fields: {
title: { stringValue: this.title },
category: { stringValue: this.category },
post: { stringValue: this.post },
summary: { stringValue: this.description },
published: { booleanValue: this.published },
created: { timestampValue: new Date() },
modified: { timestampValue: new Date() }
}
}
).then(res => { console.log("Post created") })
And an example here using Python Requests:
Using the Firestore REST API to Update a Document Field
(this is a PATCH request but the field formatting is the same as in a POST request)
import requests
import json
endpoint = "https://firestore.googleapis.com/v1/projects/[PROJECT_ID]/databases/(default)/documents/[COLLECTION]/[DOCUMENT_ID]?currentDocument.exists=true&updateMask.fieldPaths=[FIELD_1]"
body = {
"fields" : {
"[FIELD_1]" : {
"stringValue" : "random new value"
}
}
}
data = json.dumps(body)
headers = {"Authorization": "Bearer [AUTH_TOKEN]"}
print(requests.patch(endpoint, data=data, headers=headers).json())
I am using Google Apps Script UrlFetchApp.fetch to send my requests. I am able to use GET requests with no problems. For example, to get all the documents in a collection (in Google Apps Script):
function firestore_get_documents(){
var options = {
headers: { 'Authorization': 'Bearer ' + ScriptApp.getOAuthToken() },
method:'GET'
}
var response = UrlFetchApp.fetch('https://firestore.googleapis.com/v1/projects/<PROJECTIDHERE>/databases/(default)/documents/myCollection', options);
var parsed = JSON.parse(response.getContentText());
return parsed;
}
This works nicely. And changing 'method' to 'POST' creates a new document in myCollection as expected. Then I try to add a POST body with some fields (or just one field):
function firestore_create_new_document(){
var options = {
headers: { 'Authorization': 'Bearer ' + ScriptApp.getOAuthToken() },
method:'POST',
payload: {fields: { title: { stringValue: 'newTitle' } } }, // If you comment out this line, it works as expected
muteHttpExceptions:true
}
var response = UrlFetchApp.fetch('https://firestore.googleapis.com/v1/projects/<PROJECTIDHERE>/databases/(default)/documents/myCollection', options);
var contentText = response.getContentText();
var parsed = JSON.parse(response.getContentText());
return parsed;
}
I get the following errors:
code: 400 message: "Request contains an invalid argument."
status: "INVALID_ARGUMENT"
details[0][#type]: "type.googleapis.com/google.rpc.BadRequest"
details[0][fieldViolations][0][field]: "{title={stringValue=newTitle}}"
details[0][fieldViolations][0][description]: "Error expanding 'fields' parameter. Cannot find matching fields for path '{title={stringValue=newTitle}}'."
Documentation is available here:
https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents/createDocument
https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents#Document
The problem may be the formatting of my 'fields' object - I've tried several different formats from the documentation and examples
The problem may be that the fields don't exist yet? I think I should be able to create a new document with new fields
The problem may be with the way UrlFetchApp.fetch sends my JSON body. I have tried using payload = JSON.stringify(payload_object) and that doesn't work either.
I think UrlFetchApp is doing something slightly different than Axios or Python Requests - the body is getting sent differently, and not parsing as expected.
How about the following modification?
From:
var options = {
headers: { 'Authorization': 'Bearer ' + ScriptApp.getOAuthToken() },
method:'POST',
payload: {fields: { title: { stringValue: 'newTitle' } } }, // If you comment out this line, it works as expected
muteHttpExceptions:true
}
To:
var options = {
headers: { 'Authorization': 'Bearer ' + ScriptApp.getOAuthToken() },
method:'POST',
payload: JSON.stringify({fields: { title: { stringValue: 'newTitle' } } }),
contentType: "application/json",
muteHttpExceptions:true
}
When I tested above modified request, I could confirm that it worked. But if other error occurs, please tell me.
Reference:
Class UrlFetchApp

Alexa skill forgets session between intents

I'm not sure if it's because I'm testing in the alexa developer console but it appears the session is restarted after every intent.
In the below code, if I invoke SetMyVarA it will write out the correct value to cloudwatch (or the terminal when using serverless), but if I then invoke SetMyVarB immediately after then I'll get "Hmm, I don't know that one" (running locally will just give me undefined for the value).
I've also tried following the advice in this question but it didn't seem to have an effect: adding alexa skill session attributes from nodejs
/*jslint es6 */
"use strict";
const Alexa = require(`alexa-sdk`);
module.exports.handler = (event, context, callback) => {
console.log(`handler: ${JSON.stringify(event.request)}`);
const alexa = Alexa.handler(event, context, callback);
alexa.appId = process.env.APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
};
const handlers = {
"LaunchRequest": function() {
console.log(`LaunchRequest`);
this.emit(`AMAZON.HelpIntent`);
},
"SetMyVarA": function() {
console.log(`Set MyVarA`);
var myVarA = this.event.session.attributes.myVarA = this.event.request.intent.slots.myVarA.value;
console.log(`MyVarA is ${myVarA}.`);
var speechOutput = `MyVarA has been set to ` + myVarA + `.`;
this.response.speak(speechOutput);
this.emit(`:responseReady`);
},
"SetMyVarB": function() {
console.log(`Set MyVarB`);
var myVarB = this.event.session.attributes.myVarB = this.event.request.intent.slots.myVarB.value;
console.log(`MyVarB is ${myVarB}.`);
var myVarA = this.event.session.attributes.myVarA
console.log(`MyVarA is ${myVarA}.`);
var speechOutput = {
"type": `SSML`,
"ssml": `<speak>MyVarB is ` + myVarB + `.</speak>`,
};
this.response.speak(speechOutput);
this.emit(`:responseReady`);
},
"AMAZON.HelpIntent": function() {
var speechOutput = `This is a skill.`;
var reprompt = `Help here.`;
speechOutput = speechOutput + reprompt;
this.response.speak(speechOutput)
.listen(reprompt);
this.emit(`:responseReady`);
},
"AMAZON.CancelIntent": function() {
},
"AMAZON.StopIntent": function() {
},
"AMAZON.RepeatIntent": function() {
console.log(`RepeatIntent`);
this.emit(`LaunchRequest`);
},
"Unhandled": function() {
// handle any intent in interaction model with no handler code
console.log(`Unhandled`);
this.emit(`LaunchRequest`);
},
"SessionEndedRequest": function() {
// "exit", timeout or error. Cannot send back a response
console.log(`Session ended: ${this.event.request.reason}`);
},
};
In SetMyVar if you use speak() and then emit a responseReady the session gets closed by default, so you're already out of it when you try to call your 2nd intent.
If you want to do exactly the same thing you're doing in SetMyVarA but not close the session immediately you need to set this.response.shouldEndSession to false. The Alexa Dev Console can handle skill sessions with no problems, so don't worry about that.
By the way, you're using ASK v1 which is outdated. Please switch to v2 code like this:
https://developer.amazon.com/blogs/alexa/post/decb3931-2c81-497d-85e4-8fbb5ffb1114/now-available-version-2-of-the-ask-software-development-kit-for-node-js
https://ask-sdk-for-nodejs.readthedocs.io/en/latest/ASK-SDK-Migration-Guide.html

Invoking repo actions from multi-select

I've created an UI action using various guides (include Jeff Potts really great ones) successfully and it function exactly as expected - but I want to add that action to the multi-select tool as well. It has been really difficult finding much documentation.
Some things I've tried:
Tried to find out if there was an applicable actionGroup - which there doesn't seem to be.
Tried adding the multi-select tags to my share-config-custom.xml to define the item - it shows up, but I obviously can't seem to use the action ID to reference that action.
My next step was to try and create a js file with a registerAction function in it, which I am able to do and have it run (I can see the console.log dump) but I don't really have any idea how I would go about invoking my repo action from there).
How can I complete this task?
There is already function exist for invoking the repository custom action.This function is defined inside below file.
share-war\components\documentlibrary\actions.js
You can take reference of below code for invoking the repository action.
onActionSimpleRepoAction: function dlA_onActionSimpleRepoAction(record, owner)
{
//ACE-2470 : Clone: Clicking multiple times the simple Workflow approval menu item gives unexpected results.
if (owner.title.indexOf("_deactivated") == -1)
{
// Get action params
var params = this.getAction(record, owner).params,
displayName = record.displayName,
namedParams = ["function", "action", "success", "successMessage", "failure", "failureMessage", "async"],
repoActionParams = {};
for (var name in params)
{
if (params.hasOwnProperty(name) && !Alfresco.util.arrayContains(namedParams, name))
{
repoActionParams[name] = params[name];
}
}
//Deactivate action
var ownerTitle = owner.title;
owner.title = owner.title + "_deactivated";
var async = params.async ? "async=" + params.async : null;
// Prepare genericAction config
var config =
{
success:
{
event:
{
name: "metadataRefresh",
obj: record
}
},
failure:
{
message: this.msg(params.failureMessage, displayName),
fn: function showAction()
{
owner.title = ownerTitle;
},
scope: this
},
webscript:
{
method: Alfresco.util.Ajax.POST,
stem: Alfresco.constants.PROXY_URI + "api/",
name: "actionQueue",
queryString: async
},
config:
{
requestContentType: Alfresco.util.Ajax.JSON,
dataObj:
{
actionedUponNode: record.nodeRef,
actionDefinitionName: params.action,
parameterValues: repoActionParams
}
}
};
// Add configured success callbacks and messages if provided
if (YAHOO.lang.isFunction(this[params.success]))
{
config.success.callback =
{
fn: this[params.success],
obj: record,
scope: this
};
}
if (params.successMessage)
{
config.success.message = this.msg(params.successMessage, displayName);
}
// Acd configured failure callback and message if provided
if (YAHOO.lang.isFunction(this[params.failure]))
{
config.failure.callback =
{
fn: this[params.failure],
obj: record,
scope: this
};
}
if (params.failureMessage)
{
config.failure.message = this.msg(params.failureMessage, displayName);
}
// Execute the repo action
this.modules.actions.genericAction(config);
}
},

iron router syntax: onAfterAction

Router.route('/courses/:_catalog', function () {
var courseCatalog = this.params._catalog.toUpperCase();
Meteor.subscribe("courseCatalog", courseCatalog);
this.render('CourseDetail', {
to: 'content',
data: function () {
return Courses.findOne({catalog: courseCatalog});
}
});
}, {
onAfterAction: function() {
if (!Meteor.isClient) {
return;
}
debugger
var course = this.data(); <======
SEO.set({
title: "course.catalog"
});
}
});
In the above code, please look at the debugger statement. I want to access the data but it seems I am doing something wrong because this.data doesn't exist. I also tried Courses.find().fetch() but I only get an empty array inside onAfterAction. What's the right syntax and what am I missing?
It needs to be inside a this.ready() block:
onAfterAction: function() {
if (this.ready()) {
var course = this.data();
...
}
}
You need to subscribe to data first. Have a look at the waitOn function to do this. The server only sends the documents you subscribed to, and since you didn't subscribe, Courses.find().fetch() returns an empty array.
Also, don't put SEO stuff in onAfterAction. Put it in onRun which is guaranteed to only run once.

Why is data set with Meteor Iron Router not available in the template rendered callback?

This is a bit puzzling to me. I set data in the router (which I'm using very simply intentionally at this stage of my project), as follows :
Router.route('/groups/:_id',function() {
this.render('groupPage', {
data : function() {
return Groups.findOne({_id : this.params._id});
}
}, { sort : {time: -1} } );
});
The data you would expect, is now available in the template helpers, but if I have a look at 'this' in the rendered function its null
Template.groupPage.rendered = function() {
console.log(this);
};
I'd love to understand why (presuming its an expected result), or If its something I'm doing / not doing that causes this?
From my experience, this isn't uncommon. Below is how I handle it in my routes.
From what I understand, the template gets rendered client-side while the client is subscribing, so the null is actually what data is available.
Once the client recieves data from the subscription (server), it is added to the collection which causes the template to re-render.
Below is the pattern I use for routes. Notice the if(!this.ready()) return;
which handles the no data situation.
Router.route('landing', {
path: '/b/:b/:brandId/:template',
onAfterAction: function() {
if (this.title) document.title = this.title;
},
data: function() {
if(!this.ready()) return;
var brand = Brands.findOne(this.params.brandId);
if (!brand) return false;
this.title = brand.title;
return brand;
},
waitOn: function() {
return [
Meteor.subscribe('landingPageByBrandId', this.params.brandId),
Meteor.subscribe('myProfile'), // For verification
];
},
});
Issue
I was experiencing this myself today. I believe that there is a race condition between the Template.rendered callback and the iron router data function. I have since raised a question as an IronRouter issue on github to deal with the core issue.
In the meantime, workarounds:
Option 1: Wrap your code in a window.setTimeout()
Template.groupPage.rendered = function() {
var data_context = this.data;
window.setTimeout(function() {
console.log(data_context);
}, 100);
};
Option 2: Wrap your code in a this.autorun()
Template.groupPage.rendered = function() {
var data_context = this.data;
this.autorun(function() {
console.log(data_context);
});
};
Note: in this option, the function will run every time that the template's data context changes! The autorun will be destroyed along with the template though, unlike Tracker.autorun calls.

Resources