WebMock stub request with dynamic body in response - ruby

I am attempting to stub a request with WebMock and have the body of the response expect any value within a given regex; something to the tune of:
stub_request(:get, "someurl").
with(headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
to_return(status: 200, body: { "data": [{ "id": /\d+/ }] }.to_json)
where id in the response can be any digit of any length. But this does not work. Does anyone know if this is possible?
The reason I am trying this is that there is a restraint that these IDs in question must be unique, and in a test environment I need to be able to create unique IDs and have the response still accept them, without having to manually, explicitly list out every possible digit in this stub_request.

I would suggest to use the lambda on to_return and handle your regex over there.
stub_request(:any, 'www.example.net').
to_return(lambda do |request|
# TODO: check your regex here
{body: request.body}
end)
RestClient.post('www.example.net', 'abc') # ===> "abc\n"

Related

Wrong Cypress intercept being hit

I have set up a number of intercepts in the body of my tests. Here they are, pasted from the Cypress log, with the alias added
cy:intercept ➟ // alias: getRecipesSku(971520)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields**"
Mocked Response: [{ ... }]
cy:intercept ➟ // alias: getRecipesSku(971520,971310)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields**"
Mocked Response: [{ ... }]
Our application's tests also mocks a number of routes by default, (coming from an apiClient.initialize) including this one below. FWIW, this is defined earlier than those above:
cy:intercept ➟ // alias: getJustcookRecipes
Method: GET
Matcher: "https://wpsite.com/wp-json/**"
Mocked Response: [{ ... }]
My test code sets up those first two intercepts, and ultimately calls the routes. Here is the code, heavily abridged:
it('refreshes the recipes when switching protein tabs', () => {
apiClient.initialize()
/* do lots of other stuff; load up the page, perform other tests, etc */
// call a function that sets up the intercepts. you can see from the cypress output
// that the intercepts are created correctly, so I don't feel I need to include the code here.
interceptJustCook({ skus: [beefCuts[0].id] }, [beefCut1Recipe])
interceptJustCook({ skus: [beefCuts[0].id, beefCuts[1].id] }, twoBeefRecipes)
// [#1] select 1 item;
// calls route associated with intercept "getRecipesSku(971520)"
page.click.checkboxWithSku(beefCuts[0].id)
/* assert against that call */
// [#2] select 2nd item (now 2 items are selected);
// calls route associated with intercept "getRecipesSku(971520, 971310)"
page.click.checkboxWithSku(beefCuts[1].id)
In the Cypress logs, the first call (marked by comment #1) is intercepted correctly:
cy:fetch ➟ (getRecipesSku(971520)) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
However, the second call (marked by comment #2) is intercepted by the wrong route mocker:
cy:fetch ➟ (getJustCookRecipes) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
You can see for yourself that the URL called at #2 does indeed match the getRecipesSku(971520, 971310) intercept, but it is caught by the getJustcookRecipes intercept. Now, I suppose the URL for that latter intercept would catch my second custom intercept. But it would also, in the same way, catch my first custom intercept, but that first one works.
(update:) I tried commenting out the place in the code where the getJustcookRecipes intercept is created so that it doesn't exist. Now, the call that should hit getRecipesSku(971520,971310) isn't being mocked at all! I checked and the mocked and called urls are a match.
Why is this going wrong and how do I fix it?
Something in the glob pattern for the 2nd intercept #getRecipesSku(971520,971310) is refusing to match.
It's probably not worth while analyzing what exactly (you may not be able to fix it in glob), but switching to a regex will match.
See regex101.com online test
cy.intercept(/wpsite\.com\/wp-json\/wp\/v2\/posts\?tags\[]=6287&tags\[]=6289&_fields/, {})
.as('getRecipesSku(971520,971310)')
The request query string may be badly formed
Looking at the docs for URLSearchParams, the implication is that the query string should be key/value pairs.
But the 2nd request has two identical keys using tags[] as the key.
It looks as if the correct format would be /wp/v2/posts?tags=[6287,6289] since the square brackets don't have a lot of meaning otherwise.
It may be that the server is handling the format tags[]=6287&tags[]=6289, but Cypress is probably not. If you run the following intercept you see the query object has only one tags[] key and it's the last one in the URL.
cy.intercept({
method: 'GET',
pathname: '/wp-json/wp/v2/posts',
},
(req) => {
console.log('url', req.url)
console.log('query', req.query) // Proxy {tags[]: '6289', ...
}
)
#Paolo was definitely on the right track. The WP API we're consuming uses tags[]=1,tags=[2] instead of tags[1,2] so that's correct, but some research into globs showed me that brackets are special. Why the first bracket isn't messing it up but the second one is, I'm not sure and I kind of don't care.
I thought about switching to regex, but instead I was able to escape the offender in the glob without switching to regex and needing to escape aaaallll the slashes in the url:
const interceptRecipes = (
{ category, skus }: RecipeFilterArgs,
recipes: Recipe[]
) => {
let queryString = ''
if (category) queryString = `categories[]=${CATEGORY_MAP[category]}`
if (skus) {
const tagIdMap = SkuToTagIdMap as Record<number, { tagId: number }>
// brackets break the glob pattern, so we need to escape them
queryString = skus.map((sku) => `tags\\[]=${tagIdMap[sku].tagId}`).join('&')
}
const url = `${baseUrl}?${queryString}${otherParams}`
const alias = category ? `get${category}Recipes` : `getRecipesSku(${skus})`
cy.intercept('GET', url, recipes).as(alias)
}

WebMock stub_request with hash_including -- matching only part of json

I wold like to test whether a request contained some inner value in json. Let assume the request body is:
level1: {
level2a: "value1",
level2b: "value2",
...
},
...
}
Then I have the following code:
WebMock.stub_request(:post, "http://127.0.0.1:/path")
.with(body: hash_including({level1: {level2a: 'value1'}}))
.to_return(status: 200, body: '{}', headers: {})
The code above does not work unfortunately. I've been hoping it'll work with matching only sub-set of json.
According to the doc: Matches a hash that includes the specified key(s) or key/value pairs. Ignores any additional keys. I am not sure if it works the way I want.
The request is too big here though for quoting it all.
Any ideas for why the test is not working and how to fix or rewrite it?

Using Sinatra to Parse JSON data from url

I'm using Sinatrarb to complete a task
I need to:
Parse the data of a JSON object from a url,
Single out one of attributes of the json data and store it as a variable
Run some arithmetic on the variable
Return the result as a new variable
then post this to a new url as a new json object.
I have seen bits and pieces of information all over including information on parsing JSON data in ruby and information on open-uri but I believe it would be very valuable having someone break this down step by step as most similar solutions given to this are either outdated or steeply complex.
Thanks in advance.
Here's a simple guide. I've done the same task recently.
Let's use this JSON (put it in a file called 'simple.json'):
{
"name": "obscurite",
"favorites": {
"icecream": [
"chocolate",
"pistachio"
],
"cars": [
"ferrari",
"porsche",
"lamborghini"
]
},
"location": "NYC",
"age": 100}
Parse the data of a JSON object from a url.
Step 1 is to add support for JSON parsing:
require 'json'
Step 2 is to load in the JSON data from our new .json file:
json_file = File.read('simple.json')
json_data = JSON.parse(json_file)
Single out one of attributes of the json data and store it as a variable
Our data is in the form of a Hash on the outside (curly braces with key:values). Some of the values are also hashes ('favorites' and 'cars'). The values of those inner hashes are lists (Arrays in Ruby). So what we have is a hash of hashes, where some hashes are arrays.
Let's pull out my location:
puts json_data['location'] # NYC
That was easy. It was just a top level key/value. Let's go deeper and pull out my favorite icecream(s):
puts json_data['favorites']['icecream'] # chocolate pistachio
Now only my second favorite car:
puts json_data['favorites']['cars'][1] # porsche
Run some arithmetic on the variable
Step 3. Let's get my age and cut it down by 50 years. Being 100 is tough!
new_age = json_data['age'] / 2
puts new_age
Return the result as a new variable
Step 4. Let's put the new age back into the json
json_data['age'] = new_age
puts json_data['age'] # 50
then post this to a new url as a new json object.
Step 5. Add the ability for your program to do an HTTP POST. Add this up at top:
require 'net/http'
and then you can post anywhere you want. I found a fake web service you could use, if you just want to make sure the request got there.
# use this guy's fake web service page as a test. handy!
uri = URI.parse("http://jsonplaceholder.typicode.com/posts")
header = {'Content-Type'=> 'text/json'}
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri, header)
request.body = json_data.to_json
response = http.request(request)
# Did we get something back?
puts response.body
On linux or mac you can open a localhost port and listen as a test:
nc -4 -k -l -v localhost 1234
To POST to this port change the uri to:
uri = URI.parse("http://localhost:1234")
Hope this helps. Let me know if you get stuck and I'll try to lend a hand. I'm not a ruby expert, but wanted to help a fellow explorer. Good luck.

Ruby split and parse batched HTTP Response (multipart/mixed)

I'm using the GMail API that allows me to get a batched response of multiple Gmail objects.
This comes back in the form of a multipart/mixed HTTP response with a set of separate HTTP responses separated by a boundary as defined in the header.
Each of the HTTP sub-Responses is a JSON format.
i.e.
result.response.response_headers = {...
"content-type"=>"multipart/mixed; boundary=batch_abcdefg"...
}
result.response.body = "----batch_abcdefg
<the response header>
{some JSON}
--batch_abcdefg
<another response header>
{some JSON}
--batch_abcdefg--"
Is there a library or an easy way to convert those responses from the string into a set of separate HTTP responses or JSON objects?
Thanks to Tholle above...
def parse_batch_response(response, json=true)
# Not the same delimiter in the response as we specify ourselves in the request,
# so we have to extract it.
# This should give us exactly what we need.
delimiter = response.split("\r\n")[0].strip
parts = response.split(delimiter)
# The first part will always be an empty string. Just remove it.
parts.shift
# The last part will be the "--". Just remove it.
parts.pop
if json
# collects the response body as json
results = parts.map{ |part| JSON.parse(part.match(/{.+}/m).to_s)}
else
# collates the separate responses as strings so you can do something with them
# e.g. you need the response codes
results = parts.map{ |part| part}
end
result
end

how to get each value from json in ruby sinatra?

I need to extract each key-value of json and that value should save in database. but i am getting trouble in getting value of each key in json.
JSON
{
"topo": [
{
"dpid": "00:00:00:00:00:00:00:03",
"ports": [
3,
1
]
}
],
"app": "vm_migration"
}
code
post '/save_summary', :provides => :json do
begin
params = JSON.parse(request.env["rack.input"].read)
return params["topo"][0]["dpid"]
#above code return correct value
return params["topo"][0]["ports"] #this is not working
rescue Exception => e
return e.message
end
end
i don't know what's wrong with ports statement, please help me to figure out small issue.
What you are returning – params["topo"][0]["ports"] – is an array of two elements, which is one of the things you can return from a Sinatra route:
An Array with two elements: [status (Fixnum), response body (responds to #each)]
So you are trying to return a response with status 3 and body 1. The response body needs to be an object that responds to each, and 1 doesn’t. If you check your logs or console you will probably see an error undefined method `each' for 1:Fixnum. Assuming you just want to see the array in the browser, simply convert it to a string:
return params["topo"][0]["ports"].to_s
The first example works because params["topo"][0]["dpid"] is a string, and you can return strings from routes.

Resources