Nock fixtures: Ignore request body when matching - nock

In the Readme for nock it explains how one can ignore the request body.
I am using the fixtures feature of nock and I need to ignore the request body (at least partially). How can I do this? Can I write a regex in the body field of the json entries in the fixtures files?

I solved it myself after some painful digging. Here is the solution:
I am trying to nock requests to the Kraken API. The request for the endpoint /0/private/TradesHistory is of method POST and sends a body containing a "query string like" string. For example the body will look like ofs=50&nonce=xxx. The nonce value changes with each request, so I want to ignore it to when looking for a matching nock. (The nonce can only be used once, it would make no sense for the client library to send the same value again).
So I have to add a "preprocessing" function as a config object to the call to nockBack like so:
import * as queryString from 'query-string'
import { back } from 'nock'
import * as path from 'path'
const before = (scope: any) => {
scope.filteringRequestBody = (body: string, aRecordedBody: string) => {
const { of: currentOffset, } = queryString.parse(`?${body}`) //Prepend a `?` so it is actually a query string.
const { of: recordedOffset, } = queryString.parse(`?${body}`)
if (!(currentOffset || recordedOffset)) {//This is the case where no offset is set. There is only possible recorded body in my case that matches this: The other body which has no offset. I replace the body with the recorded body to produce a match.
return aRecordedBody
}
if (currentOffset === recordedOffset) {//This is the case where the sent body has the same offset as the recorded body. I replace the body with the recorded body in order to produce a match.
return aRecordedBody
}
return body
}
}
back.fixtures = `${__dirname}/nockFixtures/`
const { nockDone } = back('nocks.json',{before})
...//Run my queris
nockDone()
Now it works like a charm.

Related

How to authenticate Shopware 6 <base-app-url> correctly

With the Admin SDK it's possible to further enrich the administration in Shopware 6. As in the installation guide for apps stated, an entry point (base-app-url) needs to be provided in the manifest file of an app.
Since every request needs to be authenticated properly, this GET request also needs authentication. However, I am not able to authenticate this one in the same way as I am successfully doing it with the GET request from modules.
The base-app-url request looks the following (in my case with some [custom] entity privileges):
http://localhost:3000/sdk?location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE&shopware-shop-signature=e7b20a46487046a515638f76c6fadab6b1c749ea4a8ac6e7653527e73ba18380
The shop has the following data
Shop {
_id: 'sbzqJiPRrbHAlC2K',
_url: 'http://localhost:8888',
_secret: '3c5a2f031006791f2aca40ffa22e8febbc8a53d8',
_apiKey: 'SWIAB2PVODCWSLZNDMC5ZM1XWA',
_secretKey: 'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg'
}
I am currently authenticating my modules like the following (Node.js):
const SHOPWARE_SHOP_SIGNATURE = 'shopware-shop-signature';
export function authenticateGetRequest(req: Request, shop: Shop): void {
// e7b20a46487046a515638f76c6fadab6b1c749ea4a8ac6e7653527e73ba18380
const signature = getSignatureFromQuery(req);
verifySignature(shop.secret, removeParamsFromQuery(req), signature);
}
function getSignatureFromQuery(req: Request): string {
if (!req.query[SHOPWARE_SHOP_SIGNATURE]) {
throw new Error('Signature is not present in request!');
}
return req.query[SHOPWARE_SHOP_SIGNATURE] as string;
}
function removeParamsFromQuery(req: Request): string {
// Some code
// Returns following string - Does neither work for base-app-url nor for module GET requests:
// 'shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964'
// If the string follows this pattern, it works only for modules:
// shop-id={id}&shop-url={url}&timestamp={ts}&sw-version={v}&sw-context-language={cl}&sw-user-language={ul}
}
function verifySignature(secret: string, message: string, signature: string): void {
const hmac = crypto.createHmac('sha256', secret).update(message).digest('hex');
if (hmac !== signature) {
throw new Error('Signature could not be verified!');
}
}
However the base-app-url cannot be verified correctly and the "Signature could not be verified!" error is thrown.
What am I doing wrong here?
More info:
Additionally I added a GET request for a module where everything is working:
http://localhost:3000/faq?shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE&shopware-shop-signature=0f0889c9e8086c6c3553dc946a01f2ef27b34cd1c55b0c03901b6d8a6a9b6f53
The resulting string can be verified:
shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE
Try out following code in some php sandbox environment:
<?php
$message = 'shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE';
$secret = '3c5a2f031006791f2aca40ffa22e8febbc8a53d8';
$signature = '0f0889c9e8086c6c3553dc946a01f2ef27b34cd1c55b0c03901b6d8a6a9b6f53';
$hmac = hash_hmac('sha256', $message, $secret);
if (!hash_equals($hmac, $signature)) {
echo 'Signature not valid';
} else {
echo 'Signature valid';
}
SOLUTION:
Express decodes the query strings automatically with req.query depending on your express configuration. Keep in mind to validate the hmac with encoded query params as they are passed from shopware.
In my case the only difference where the decoded privileges and they looked like this:
&privileges={"read":["language","ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"create":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"update":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"delete":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"]}
But they need to look like this:
&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D
Looking at the QuerySigner, this is how the signature is generated on the side of Shopware with the actual arguments:
hash_hmac(
'sha256',
'location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE',
'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg'
);
// 8034a13561b75623420b06fb7be01f20d97556441268939e9a5222ffec12215a
Given on your side you remove the shopware-shop-signature query param AND that the secrets are equal on both sides, you should be able to regenerate the matching signature.
const crypto = require('crypto');
const message = 'location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE';
const hmac = crypto.createHmac('sha256', 'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg').update(message).digest('hex');
// 8034a13561b75623420b06fb7be01f20d97556441268939e9a5222ffec12215a
So in theory your code looks fine. Verify that the query string matches exactly. Things to check:
Maybe your node server decodes the url entities unwantedly?
Does your node serve escape special characters in the query string?
Do the secrets match on both sides?
To consider additionally:
Consider to just point the base-app-url to a static page outside of the scope of your app server instead. As that page will be loaded inside an iframe, you can use client side javascript to read the query parameters and, only if necessary, make requests to your app server using the credentials from inside the iframe. Keep in mind you really only need the authentication if you need to handle personalized data, otherwise you might as well serve static assets without the need for authentication.

How can I get webflux response body twice? Store some data from request/response

I created a filter to log some important data from the request and response...
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI uri = request.url();
HttpMethod method = request.method();
return next.exchange(request).onErrorResume(err -> {
writeToFile(method, uri, 500, "", true);
return Mono.error(err);
}).flatMap((response) -> {
return response.bodyToMono(String.class).flatMap(body -> {
writeToFile(method, uri, response.rawStatusCode(), body, false);
return Mono.just(response);
});
});
}
The idea of this filter is to register some data and let the rest work exactly the same.
Expected: This was my attempt to keep the rest of the code untouched and just include a filter to store some data apart from the normal flow.
Actual: It works, except when the real code calls bodyToMono, it returns null
At some point of the normal flow, I am doing something similar to this:
return client.get().uri(...).exchangeToMono((response) -> {
return response.bodyToMono(MyObject.class);
})
It works flawlessly when I remove the filter.
So I discovered I can't call bodyToMono twice the way I am using probably because the first call is consuming the body and then it is empty in the second call.

NestJS: Set #Param and body values to DTO in POST request

I have a POST request entry where I specify an "account" parameter as a path parameter, and a boolean in the body to set validation state, like:
POST /users/authorized/USER_ACCOUNT1
The body would carry:
valid=1
I have the following controller entry:
#ApiTags('users')
#ApiOperation( { summary: 'Set user account status. '} )
#Post('authorized/:account')
async setAuthStatus(params: SetUserAuthDto) {
return this.userService.setUserAuthDto(params);
}
How I can feed both the "account" and the request body "status" parameter to the same DTO? I assume I cannot use both decorators #Param and #Body there.
Should I use pipes?
I'm new in NestJS so excuse my ignorance.
Thanks.
There's nothing wrong with using both #Params() and #Body(). It's a common pattern actually
#Post('authorized/:account')
async setAuthStatus(#Param('account') accountValue: string, #Body() body: SetUserAuthDto) {
// do your thing here
}
However, if you want to use a single decorator you could use the createParamDecorator method and have it return req.params and req.body in a single, merged object, so you could do #ParamAndBody()

Sending response in async function

I need to return an array of labels, but I can only return 1 of the labels so far. The error which I get is "Cannot set headers after they are sent to the client". So I tried res.write and placed res.end after my for loop then I get the obvious error of doing a res.end before a res.write. How do I solve this?
for(let i=0;i<arr.length;i++){
request.get(arr[i], function (error, response, body) {
if (!error && response.statusCode == 200) {
myfunction();
async function myfunction(){
const Labels = await Somefunctioncallwhoseresponseigetlater(body)
res.send(Labels);
}
}
});}
New code-
async function getDataSendResponse(res) {
let allLabels = [];
for (let url of arr) {
let body = await got(url).buffer();
var imgbuff= Buffer.from(body,'base64')
const imageLabels = await rekognition.detectLabels(imgbuff);
allLabels.push(...imageLabels);
}
res.send(allLabels);
}
The error I have with this code is
"Resolver: AsyncResolver
TypeError: Cannot destructure property Resolver of 'undefined' or 'null'."
You are trying to call res.send() inside a for loop. That means you'll be trying to call it more than once. You can't do that. You get to send one response for any given http request and res.send() sends an entire response. So, when you try to call it again inside the loop, you can the warning you see.
If what you're trying to do is to send an array of labels, then you need to accumulate the array of labels first and then make one call to res.send() to send the final array.
You don't show the whole calling context here, but making the following assumptions:
Somefunctioncallwhoseresponseigetlater() returns a promise that resolves when it is done
You want to accumulate all the labels you collected in your loop
Your Labels variable is an array
Your http request returns a text response. If it returns something else like JSON, then .text() would need to be changed to .json().
then you can do it like this:
const got = require('got');
async function getDataSendResponse(res) {
let allLabels = [];
for (let url of arr) {
let body = await got(url).buffer();
const labels = await Somefunctioncallwhoseresponseigetlater(body);
allLabels.push(...labels);
}
res.send(allLabels);
}
Note, I'm using the got() library instead of the deprecated request() library both because request() is not deprecated and because this type of code is way easier when you have an http library that supports promises (like got() does).

Create a route with "search url" as parameter using Express

Using Express, how I can create a route such as:
When the URL /search?s=<SEARCH> is invoked, answers with
{status:200, message:"ok", data:<SEARCH>} if is provided
When not provided, the answer should be {status:500, error:true, message:"you have to provide a search"}.
Be sure to set the HTTP status to 500 too.
This code checks, if the query parameter s has been added and replies it as asked. If there is no query parameter named s, req.query.s will be undefinded. (Docs) In this case a HTTP-500 answer is sent.
app.get('/search',(req,res) => {
const search = req.query.s;
if (typeof search != 'undefined') {
// Search string applied
const response = {
status:200, message:"ok", data: search
};
res.send(response);
}
else {
const response = {
status:500, error:true, message: "you have to provide a search"
};
res.status(500);
res.send(response);
}
});
This code has the advantage, that the correct Content-Type header is set by express automatically. The resultant object will also be JSON formatted, it can therefore directly consumed by any client.
Please be advised, that the 500 Internal Server Error status should not be applied to the described situation here. 404 Not found might be a better solution.