Why rack/test is combining hashes into one when performing POST or PUT operations - ruby-on-rails-3

In my rspec test, I defined the following array of hashes and performed a POST:
body = {:event => { :invitations_attributes =>
[ {:recipient_id => 40}, {:email => 'a#a.com'}, {:facebook_id => 123456789} ] } }
post "#{#url}.json", body.reverse_merge(:auth_token => #token)
Based on the above, I expected the Rails server to receive "invitations_attributes" as an array of hashes. However, the developer.log file has the following:
Parameters: {"auth_token"=>"RSySKfN2L8b5QPqnfGf7", "event"=>{"invitations_attributes"=>
[{"recipient_id"=>"40", "email"=>"a#a.com", "facebook_id"=>"123456789"}]}}
(In the parameters above, "invitation_attributes" array contains only 1 hash.)
The following curl statement:
curl -X POST -H "Content-type: application/json" http://localhost:3000/api/v1/events.json -d '{"auth_token":"RSySKfN2L8b5QPqnfGf7","event":{"invitation_attributes":[{"recipient_id":40},{"email":"a#a.com"},{"facebook_id":123456789}]}}'
results in Rails' receiving the array of hashes intact, as evidenced by the log file entry below.
Parameters: {"auth_token"=>"RSySKfN2L8b5QPqnfGf7", "event"=>{"invitation_attributes"=>
[{"recipient_id"=>40}, {"email"=>"a#a.com"}, {"facebook_id"=>123456789}]}}
Rack/test is exhibiting this behavior for PUT operations as well as POST.
Why is rack/test combining the 3 hashes into 1 rather than sending the array exactly as it is defined? Is there a setting which will cause rack to exhibit the behavior I expected?

One workaround is to ensure that each hash contains each key by inserting nil value placeholder keys as follows:
body = {:event => { :invitations_attributes => [
{:recipient_id => 40, :recipient_email => nil, :recipient_facebook_id => nil},
{:recipient_email => user.email, :recipient_id => nil, :recipient_facebook_id => nil},
{:recipient_facebook_id => new_unused_facebook_id, :recipient_email => nil, :recipient_id => nil} ] } }
The hash above does cause the server to receive 3 separate hashes within the array. However, inserting placeholder keys is inconvenient and should not be required. Furthermore, scenarios where a controller acts differently based on the presence of a such a key (albeit uncommon), cannot be tested.

Related

How to send a patch api request using a variable

I am trying to update a user(s) type in the Zoom conference application using their API. I use PATCH as per their documentation, and this works when I hard code the userId in the URL, but I need to use an array variable instead because multiple users will need to be updated at once.
This code works with the manually entered userId.
The userId and bearer code are made up for the purpose of this question.
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client();
$response = $client->PATCH('https://api.zoom.us/v2/users/jkdflg4589jlmfdhw7', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer my token goes here',
],
'body' => json_encode([
'type' => '1',
])
]);
$body = $response->getBody() ;
$string = $body->getContents();
$json = json_decode($string);
This way the code works and changes my user's type to 1.
The following code is the one that doesn't work.
In the Zoom API reference there is a test section and the userId can be added in a tab called Settings under the field: Path Parameters.
https://marketplace.zoom.us/docs/api-reference/zoom-api/users/userupdate
Hence I can add the userId there and when I run it, it actually replaces {userId} in the URL with the actual userId into the url patch command.
Hence from this ->
PATCH https://api.zoom.us/v2/users/{userId}
It becomes this after all transformations, scripts,
and variable replacements are run.
PATCH https://api.zoom.us/v2/users/jkdflg4589jlmfdhw7
However, when I try it in my code it doesn't work, I don't know where to add the path params. I am more used to PHP but I'll use whatever I can to make it work. Also I would like userId to be a variable that may contain 1 or more userIds (array).
This is my code that doesn't work:
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client();
$response = $client->PATCH('https://api.zoom.us/v2/users/{userId}', [
'params' => [
'userId' => 'jkdflg4589jlmfdhw7',
],
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer my token goes here',
],
'body' => json_encode([
'type' => '1',
])
]);
$body = $response->getBody() ;
$string = $body->getContents();
$json = json_decode($string);
My code fails with error:
Fatal error: Uncaught GuzzleHttp\Exception\ClientException: Client error: `PATCH https://api.zoom.us/v2/users/%7BuserId%7D` resulted in a `404 Not Found` response: {"code":1001,"message":"User not exist: {userId}"}
in /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113 Stack trace:
#0 /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/guzzle/src/Middleware.php(66): GuzzleHttp\Exception\RequestException::create(Object(GuzzleHttp\Psr7\Request), Object(GuzzleHttp\Psr7\Response))
#1 /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/promises/src/Promise.php(203): GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object(GuzzleHttp\Psr7\Response))
#2 /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/promises/src/Promise.php(156): GuzzleHttp\Promise\Promise::callHandler(1, Object(GuzzleHttp\Psr7\Response), Array)
#3 /home/.../publ in /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php on line 113
If I understood you correctly, then this is basic string concatenation in PHP that you are trying to do
$userId = 'jkdflg4589jlmfdhw7';
$response = $client->PATCH('https://api.zoom.us/v2/users/' . $userId, [
// other options
]);
However, when I try it in my code it doesn't work, I don't know where to add the path params.
You add URL path in the first argument, since path is part of the URL. You can however set query parameters (e.g. for GET requests) and form data (e.g. for POST form requests) through Guzzle options, but not the path.
Also I would like userId to be a variable that may contain 1 or more userIds (array).
Using just a simple implode to convert an array to a comma separated list should work, but the API point you linked to does not seem to support multiple user IDs.
$userId = ['jkdflg4589jlmfdhw7', 'asdfa123sdfasdf'];
$response = $client->PATCH('https://api.zoom.us/v2/users/' . implode(',', $userId), [
// other options
]);

CakePHP 3.6 Controller Integration Testing - HTTP requests not sent

I am trying to implement controller integration testing in CakePHP 3.6 using its testing tools. I assumed that this would be handled by making a 'real' (as in CURL) HTTP request against the running webserver, but it looks like it isn't. Below is the test case code I'm using.
The problems I'm running into:
The test case is somehow managing to access the controler action,
even when the webserver is not running at all (Apache down and no
dev webserver running).
When running this test, the controller does not have access to
$_SERVER (see below) and any of the $postData defined in the test case appears empty on the controller side.
When I place exit; in the controller code, the whole test case
stops, which suggests that the controller code is run directly, not
via a HTTP request.
Question: How can I make a 'real' HTTP requests when testing controllers, apart from resorting to using CURL and handling the requests manually?
Clearly, I am either not understanding how the controller testing is done, or I'm doing something wrong.
Test case I'm using:
/tests/TestCase/Controller/JobsControllerTest.php
<?php
namespace App\Test\TestCase\Controller;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\IntegrationTestCase;
/**
* App\Controller\JobsController Test Case
*/
class JobsControllerTest extends IntegrationTestCase
{
/**
* Test add method
*
* #return void
*/
public function testAdd()
{
$this->useHttpServer(true);
$this->configRequest([
'headers' => [
'Content-Type' => 'application/json',
'X-Api-Key' => '8f083c8f083c8f083c8f083c'
]
]);
$postData = [
'user_id' => 3,
'job_status' => 'New'
];
$this->post('/jobs/add', $postData);
$this->assertResponseSuccess();
$jobs = TableRegistry::get('Jobs');
$query = $jobs->find()->where(['user_id' => $postData['user_id']]);
$this->assertEquals(1, $query->count());
}
}
The dump of $_SERVER global from the controller that I'm testing:
Array
(
[LS_COLORS] => rs=0:di=01;34 [...]
[LANG] => en_US.UTF-8
[HOME] => /home/tomasz
[TERM] => screen
[PATH] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
[MAIL] => /var/mail/root
[LOGNAME] => root
[USER] => root
[USERNAME] => root
[SHELL] => /bin/bash
[SUDO_COMMAND] => vendor/bin/phpunit --verbose
[SUDO_USER] => tomasz
[SUDO_UID] => 1000
[SUDO_GID] => 1000
[PHP_SELF] => vendor/bin/phpunit
[SCRIPT_NAME] => vendor/bin/phpunit
[SCRIPT_FILENAME] => vendor/bin/phpunit
[PATH_TRANSLATED] => vendor/bin/phpunit
[DOCUMENT_ROOT] =>
[REQUEST_TIME_FLOAT] => 1546631688.0758
[REQUEST_TIME] => 1546631688
[argv] => Array
(
[0] => vendor/bin/phpunit
[1] => --verbose
)
[argc] => 2
)
CakePHP integration tests do not issue actual HTTP requests, they simulate them, it's very fast, allows for certain mocking, inspecting session contents, accessing exception details, etc., all sorts of things that wouldn't really be possible (at least not easily) when using real HTTP requests. If you really need to issue actual requests, then you should look into using other utilities, like for example Codeception (specifically acceptance tests).
When using CakePHP, it is advised that you do not access PHP superglobals directly, but that you retrieve the data from the abstracted APIs provided by CakePHP! Breaking your integration tests is one of the reasons for this. The simulated request will receive a request object that has been prepared with the data from your test case, that is where you need to look it up.
For example if you want to access POST data in your app, maybe in your controller, then you do it like this:
$user_id = $this->request->getData('user_id');
See also
Cookbook > Testing > Controller Integration Testing
Cookbook > Request & Response Objects

Rails 5 dropping some keys in params which contains non null values

I am moving from rails 3 to 5. I have a request to an API which accepts some parameter but is getting dropped in rails 5 for reasons I am not sure.
The params which I recieved in Rails 3 was as below:
params = {"Envelope" => {"Body" => {"notifications" => {"Notification" => {"id =>" XYZ, "sObject" => {"data1" => ABC, "data2" => PQR}}}}}, "action" => XXX, "controller" => YYY, "format" => "xml", "auth" => AUTH_TOKEN, "entity" => ENTITY_NAME}
Whereas in Rails 5, the params I am receiving is:
params = <ActionController::Parameters {"entity"=>ENTITY_NAME, "auth"=>AUTH_TOKEN, "format"=>"xml", "controller"=> YYY, "action"=> XXX} permitted: false>
How are the parameters getting dropped when the request is made?
The request was sending the body in XML format. Rails 3 parses the XML body by default to json but Rails 5 does not. actionpack-xml_parser gem has to be explicitly installed so that the request body is parsed.

How do I use the Rails Vacuum Gem and get XML from the response

I am trying to fool around with the Vacuum gem, but I have some issues with the example. When I run the code as described on the frontpage:
req = Vacuum.new
req.configure key: 'foo',
secret: 'secret',
tag: 'biz-val'
params = { 'Operation' => 'ItemSearch',
'SearchIndex' => 'Books',
'Keywords' => 'Architecture' }
res = req.get query: params # XPath is your friend.
I am having issues getting the XML from the response. I can see that the object type is Excon::Response and actually contains data, but how do I retrieve the XML it contains?
I have tried with both res.to_xml and res.xml, and even loading it into Nokogiri, but with no success.
I found out. XML was saved in the object Body in the response.

How do I test my JSON API with Sinatra + rspec

I have a post method that accepts JSON:
post '/channel/create' do
content_type :json
#data = JSON.parse(env['rack.input'].gets)
if #data.nil? or !#data.has_key?('api_key')
status 400
body({ :error => "JSON corrupt" }.to_json)
else
status 200
body({ :error => "Channel created" }.to_json)
end
As a newbie to rspec I am bewildered trying to figure out how to write a test against that POST with an acceptable JSON payload. The closest I got to is this which is woefully inaccurate but I don't seem to be asking the Google god the right questions to help me out here.
it "accepts create channel" do
h = {'Content-Type' => 'application/json'}
body = { :key => "abcdef" }.to_json
post '/channel/create', body, h
last_response.should be_ok
end
Any best practice guidance for testing APIs in Sinatra will be most appreciated also.
The code you've used is fine, although I would structure it slightly differently as I don't like to use it blocks the way you normally see them, I think it encourages testing of more than one aspect of a system at a time:
let(:body) { { :key => "abcdef" }.to_json }
before do
post '/channel/create', body, {'CONTENT_TYPE' => 'application/json'}
end
subject { last_response }
it { should be_ok }
I've used let because it's better than an instance variable in a before block (kudos to you for not doing that). The post is in a before block because it's not really part of the spec, but a side effect that occurs prior to what you're speccing. The subject is the response and that makes the it a simple call.
Because checking the response is ok is needed so often I put it in a shared example:
shared_examples_for "Any route" do
subject { last_response }
it { should be_ok }
end
and then call it as such:
describe "Creating a new channel" do
let(:body) { { :key => "abcdef" }.to_json }
before do
post '/channel/create', body, {'CONTENT_TYPE' => 'application/json'}
end
it_should_behave_like "Any route"
# now spec some other, more complicated stuff…
subject { JSON.parse(last_response.body) }
it { should == "" }
and because the content type changes so often, I put that in a helper:
module Helpers
def env( *methods )
methods.each_with_object({}) do |meth, obj|
obj.merge! __send__(meth)
end
end
def accepts_html
{"HTTP_ACCEPT" => "text/html" }
end
def accepts_json
{"HTTP_ACCEPT" => "application/json" }
end
def via_xhr
{"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
end
It's easy to add this in where it's needed by including it via the RSpec config:
RSpec.configure do |config|
config.include Helpers, :type => :request
then:
describe "Creating a new channel", :type => :request do
let(:body) { { :key => "abcdef" }.to_json }
before do
post '/channel/create', body, env(:accepts_json)
end
Having said all that, personally, I wouldn't post using JSON. HTTP POST is simple to handle, and every form and javascript library does it easily and well. Respond with JSON by all means, but don't post JSON, HTTP is a lot easier.
Edit: after writing out the Helpers bit above I realised it would be more helpful as a gem.
Looks like the ability to do post :update, '{"some": "json"}' was added to the internal ActionPack test_case.rb used by rspec in this commit:
https://github.com/rails/rails/commit/5b9708840f4cc1d5414c64be43c5fc6b51d4ecbf
Since you're using Sinatra I'm not sure the best way to get those changes—you might be able to upgrade ActionPack directly, or patch from the above commit.
If you want to look at last_response as JSON, you could try rack-test-json which makes this trivial:
expect(last_response).to be_json
expect(last_response.as_json['key']).to be == 'value'