My challenge here is to find the best way to test a Symfony (3.4) API application using Behat/Mink for functionnal test, in my CICD platform.
Because my testing processes must be called in a shell script, all the tests must be very linear. I have no way to start a standalone webserver like Apache or the PHP/Symfony webserver. Also, Docker is not an option.
For the moment, I can successfully test the GET verbs of the API using the Mink syntax :
-- file test.feature
#function1
Scenario Outline: Test my api
When I go to "/api/v1/hello"
Then the response is JSON
The "I go to" instruction is implemented by Mink (http://docs.behat.org/en/v2.5/cookbook/behat_and_mink.html) and it emulates a GET request only. When this instruction is called by BeHat, the app Symfony kernel is "spawned" and the "api/v1/hello" method is called internally : there is no network trafic, no TCP connection, there is no need for a dedicated webserver (apache, or the symfony standalone server). It looks like Behat is emulating a webserver and start by itself the Symfony app it its own user space.
Now I want to test the POST verbs of my API, with a json payload, but unfortunally Mink do not have other verbs than GET.
I have read some articles over the web (keyword : behat test post api) but all I have seen is based on a Guzzl/Curl client. So a real client-to-server connection is made to http://localhost and a real webserver have to respond to the request.
I want the Symfony API to be called internally without using an other webserver.
Is there a way to do that ? How to test a Symfony REST API and specially the POST verb without needing a standalone server to reply ?
Thank you.
Here is how I do a functional test of a POST API, with BeHat, without a local running webserver :
test.feature :
#function1
Scenario Outline: Test my api
Given I have the payload
"""
{ "data":"object"}
"""
When I request "POST /api/v1/post"
Then the response is JSON
The featureContext file implement two functions :
"I Have The Payload" : See here https://github.com/philsturgeon/build-apis-you-wont-hate/blob/master/chapter8/app/tests/behat/features/bootstrap/FeatureContext.php
"I request" : based on code provided by philsturgeon just above, I modify it to have something like that :
/**
* #When /^I request "(GET|PUT|POST|DELETE|PATCH) ([^"]*)"$/
*/
public function iRequest($httpMethod, $resource)
{
$this->lastResponse = $this->lastRequest = null;
$this->iAmOnHomepage();
$method = strtoupper($httpMethod);
$components = parse_url($this->getSession()->getCurrentUrl());
$baseUrl = $components['scheme'].'://'.$components['host'];
$this->requestUrl = $baseUrl.$resource;
$formParams = json_decode($this->requestPayload, true);
$formParamsList = [];
foreach($formParams as $param => $value) {
$formParamsList[$param] = json_encode($value);
}
// Construct request
$headers = [
'Accept'=>'application/json',
'Content-Type'=>'application/x-www-form-urlencoded'
];
try {
// Magic is here : allow to simulate any HTTP verb
$client = $this->getSession()->getDriver()->getClient();
$client->request(
$method,
$this->requestUrl,
$formParamsList,
[],
$headers,
null);
} catch (BadResponseException $e) {
$response = $e->getResponse();
// Sometimes the request will fail, at which point we have
// no response at all. Let Guzzle give an error here, it's
// pretty self-explanatory.
if (null === $response) {
throw $e;
}
$this->lastResponse = $e->getResponse();
throw new \Exception('Bad response.');
}
}
If you use Mink then it is quite easy
class FeatureContext extends RawMinkContext
{
/**
* #When make POST request to some Uri
*/
public function makePostRequestToSomeUri(): void
{
$uri = '/some-end-point';
/** #var \Symfony\Component\BrowserKit\Client $client */
$client = $this->getSession()->getDriver()->getClient();
$postParams = [];
$files = [];
$serverParams = [];
$rawContent = '';
$client->request(
\Symfony\Component\HttpFoundation\Request::METHOD_POST,
$uri,
$postParams,
$files,
$serverParams,
$rawContent
);
/** #var \Symfony\Component\HttpFoundation\Response $response */
$response = $client->getResponse();
//...
}
}
Related
I'm not very savvy with web API calls, but I've been using the following powershell code (this site in this example is one I found that has some public data... my site is internal and requires I pass the credential, which has been working for me without issue):
If(-not (Get-InstalledModule -Name 'ConfluencePS')){Install-Module ConfluencePS}
Import-Module ConfluencePS
Set-ConfluenceInfo -BaseUri "https://wiki.opnfv.org"
$space = Get-confluencepage -Spacekey ds
ForEach($item in $space)
{
$splatParams = #{
Uri = "https://wiki.opnfv.org/rest/api/content/$($item.ID)/restriction"
ContentType = 'application/json'
method = 'GET'
}
#reference https://developer.atlassian.com/cloud/confluence/rest/#api-api-content-id-restriction-get
Invoke-RestMethod #splatParams
}
The documentation for the ConfluencePS shows that restrictions is still an open feature request but I need to get this working for a project.
I put a breakpoint in on line 982 from ConfluencePS.psm1 and was able to see the various calls and how the params are structured but when I try to mimic it (and change the URI based on the confluence documentation) I get an error "HTTP error 405 - MethodNotAllowed". Anyone have suggestions on how I can get this working? I'm trying to return back the permissions applied for all pages in a specific space.
Get Restrictions by Content ID
As you found out by yourself, it is required to add "byOperation".
I was able to get the restrictions of a specific page with the following code:
# for testing purposes ONLY, I've specified the URL and ID
$wikiUrl = "https://wiki.opnfv.org"
$itemId = "6820746"
$splatParams = #{
Uri = "$wikiUrl/rest/api/content/$itemId/restriction/byOperation"
ContentType = 'application/json'
method = 'GET'
}
$result = Invoke-RestMethod #splatParams
Tested on version 6.0.4 and 6.15.9
Filter by user name
If you like to filter the result by a specific username, you can use the following URI:
"$wikiUrl/rest/api/content/$itemId/restriction/byOperation/.../user?userName=".
Bt, there's an open bug on this way of action:
restriction returns ambiguous responses
I am setting up a simple REST Service in ColdFusion 11. The web server is IIS 8.5 on Windows Server 2012R2.
This REST Service needs to be secured to prevent unauthorized users from accessing/writing data. For the time being, there will be only one authorized user, so I want to keep authentication/authorization as simple as possible. My initial thought is to use HTTP BASIC Authentication.
Here's the setup for the REST Service:
Source Directory: C:\web\site1\remoteapi\
REST path: inventory
To implement this, I configured the source directory of the REST Service in IIS to authorize only one user, disable Anonymous authentication, and enable Basic authentication.
When I call the source directory directly in a browser (i.e. http://site1/remoteapi/inventory.cfc?method=read), I am presented with the Basic authentication dialog.
However, when I attempt to request the REST path (http://site1/rest/inventory/), I am not challenged at all.
How can I implement HTTP BASIC authentication on the REST path?
So, due to the need to get this done without much delay, I went ahead and using some principles from Ben Nadel's website, I wrote my own authentication into the onRequestStart() method of the REST Service's Application.cfc. Here is the basic code, though it uses hard-coded values in the VARIABLES scope to validate the username and password and also does not include any actual "authorization" setting:
public boolean function onRequestStart(required string targetPage) {
LOCAL.Response = SUPER.onRequestStart(ARGUMENTS.targetpage);
if (!StructKeyExists(GetHTTPRequestData().Headers, "Authorization")) {
cfheader(
name="WWW-Authenticate",
value="Basic realm=""REST API Access"""
);
LOCAL.RESTResponse = {
status = 401,
content = {Message = "Unauthorized"}
};
restSetResponse(LOCAL.RESTResponse);
}
else {
LOCAL.IsAuthenticated = true;
LOCAL.EncodedCredentials =
GetToken( GetHTTPRequestData().Headers.Authorization, 2, " " );
// Credential string is not Base64
if ( !ArrayLen(
REMatch(
"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$",
LOCAL.EncodedCredentials
)
)
) {
LOCAL.IsAuthenticated = false;
}
else {
// Convert Base64 to String
LOCAL.Credentials =
ToString(ToBinary( LOCAL.EncodedCredentials ));
LOCAL.Username = GetToken( LOCAL.Credentials, 1, ":" );
LOCAL.Password = GetToken( LOCAL.Credentials, 2, ":" );
if ( LOCAL.Username != VARIABLES.CREDENTIALS.Username
|| LOCAL.Password != VARIABLES.CREDENTIALS.Password
) {
LOCAL.IsAuthenticated = false;
}
}
if (!LOCAL.IsAuthenticated) {
LOCAL.Response = {
status = 403,
content = {Message = "Forbidden"}
};
restSetResponse(LOCAL.Response);
}
}
return LOCAL.Response;
}
I am using SLIM Framework with a simple post route function like:
$app->post( '/addresses', 'addAddress' );
...
function addAddress()
{
global $app;
$request = $app->request();
$firstname = $request->params('firstname');
echo $firstname;
/* insert into action ... */
}
and want to get the post variables, but if I sent a post request with the Advanced Rest Client for Chrome like the following:
firstname=Test
the result is always null :(
EDIT:
the mistake was text/plain if I set it up to application/x-www-form-urlencoded it works
The mistake was to select text/plain for Advanced Rest Client, if I set it up to application/x-www-form-urlencoded it works
I am trying to use the Wink RestClient to do functional testing on a Rest service endpoint. I use mocks for unit testing but I'd like to functionally test it as an endpoint consumer.
I understand some will object to me calling it a REST endpoint while using form-based auth but that is the current architecture I have.
The majority of the resources I want to test are protected resources and the application (running on Tomcat6) is protected by form authentication. (as in the below web.xml snippet).
What I've tried so far is to make an initial call to an unprotected resource, to obtain the set-cookie header, that contains JSESSIONID, and use that JSESSIONID in the header ( via Resource.cookie() ) in subsequent requests but that does not yield fruit.
web.xml
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login.html?failure=true</form-error-page>
</form-login-config>
</login-config>
My Wink RestClient code looks like below. All responses are 200, but two things I notice are that the response from the call to /j_security_check/ does not include the jsessionid cookie, and the call to the protected resource said I had a signin failure. The payload for the call to j_security_check was captured directly from a previous successful browser request intercepted.
ClientConfig config = new ClientConfig();
config.setBypassHostnameVerification(true);
RestClient restClient = new RestClient(config);
Resource unprotectedResource = restClient.resource( BASE_URL + "/");
unprotectedResource.header( "Accept", "*/*" );
ClientResponse clientResponse = unprotectedResource.get();
String response = clientResponse.getEntity(String.class);
// get jSession ID
String jSessionId = clientResponse.getHeaders().get("set-cookie").get(0);
jSessionId = jSessionId.split(";")[0];
System.out.println(jSessionId);
// create a request to login via j_security_check
Resource loginResource = restClient.resource(BASE_URL + "/j_security_check/");
loginResource.accept("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
loginResource.header("referer", "http://localhost:8080/contextroot/");
loginResource.cookie( jSessionId );
loginResource.header("Connection", "keep-alive");
loginResource.header("Content-Type", "application/x-www-form-urlencoded");
loginResource.header("Content-Length", "41");
ClientResponse loginResponse = loginResource.post("j_username=*****&j_password=*************");
/* the loginResponse, as this point, does not have the jsessionid cookie, my browser client does */
Resource protectedResource = restClient.resource(BASE_URL + "/protected/test/");
systemResource.accept("application/json");
systemResource.cookie( jSessionId );
ClientResponse systemResponse = systemResource.get();
response = clientResponse.getEntity(String.class);
System.out.println(response);
Any thoughts or experience with using the Wink RestClient to exercise form-auth-protected resources would be greatly appreciated. I suppose I'd entertain other frameworks, I have heard of REST-Assured and others, but since the application uses Wink and the RestClient seems to provide me with what I need, I figured I'd stick with it.
Found the problem, and the solution
j_security_check was responding to my POST request (to authenticate), with a #302/redirect. That was being followed by the wink RestClient, but my JSESSIONID cookie was not being appended to it. That was causing the response (from the redirected URL) to contain a set-cookie header, with a new header. My subsequent calls, into which I inserted the JSESSIONID from the first call, failed, because that cookie was expired. All I needed to do was instruct the RestClient to NOT follow redirects. If the redirect were necessary, I would construct it on my own, containing the appropriate cookie.
Chromium and Firefox carry the cookie from the original request to the redirected request so it's all good.
Here is some code that worked for me, using JUnit4, RestClient from the Apache Wink project (and a Jackson ObjectMapper)
#Test
public void testGenerateZipEntryName() throws JsonGenerationException, JsonMappingException, IOException
{
final ObjectMapper mapper = new ObjectMapper();
final String BASE_URL = "http://localhost:8080/rest";
// Configure the Rest client
ClientConfig config = new ClientConfig();
config.proxyHost("localhost"); // helpful when sniffing traffic
config.proxyPort(50080); // helpful when sniffing traffic
config.followRedirects(false); // This is KEY for form auth
RestClient restClient = new RestClient(config);
// Get an unprotected resource -- to get a JSESSIONID
Resource resource = restClient.resource( BASE_URL + "/");
resource.header( "Accept", "*/*" );
ClientResponse response = resource.get();
// extract the jSession ID, in a brittle and ugly way
String jSessId = response.getHeaders().get("set-cookie").get(0).split(";")[0].split("=")[1];
// Get the login resource *j_security_check*
resource = restClient.resource(BASE_URL + "/j_security_check");
resource.cookie("j_username_tmp=admin; j_password_tmp=; JSESSIONID=" + jSessId);
resource.header("Content-Type", "application/x-www-form-urlencoded");
resource.header("Content-Length", "41");
// Verify that login resource redirects us
response = resource.post("j_username=admin&j_password=***********");
assertTrue( response.getStatusCode() == 302 );
// Grab a public resource
resource = restClient.resource(BASE_URL + "/");
resource.cookie("j_username_tmp=admin; j_password_tmp=; JSESSIONID=" + jSessId);
response = resource.get();
// verify status of response
assertTrue( response.getStatusCode() == 200 );
// Grab a protected resource
resource = restClient.resource(BASE_URL + "/rest/system");
resource.cookie("j_username_tmp=admin; j_password_tmp=; JSESSIONID=" + jSessId);
// Verify resource returned OK
response = resource.contentType("application/json").accept("*/*").get();
assertTrue( response.getStatusCode() == 200 );
// Deserialize body of protected response into domain object for further testing
MyObj myObj = mapper.readValue(response.getEntity(String.class), MyObj.class );
assertTrue( myObj.customerArchived() == false );
}
I'm trying to follow the information about how to use soap in magento, but always get same message in error.log
If any one experience something similar, that could give me some tip, it will be welcome.
"PHP Fatal error: SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://www.site.com/index.php/api/?wsdl' : failed to load external entity "http://www.site.com/index.php/api/?wsdl"\n in /var/www/test.php on line 1"
$client = new SoapClient('http://www.site.com/api/?wsdl');
$session = $client->login('apiUser', 'apiKey');
$result = $client->call($session, 'somestuff.method');
$result = $client->call($session, 'somestuff.method', 'arg1');
$result = $client->call($session, 'somestuff.method', array('arg1', 'arg2', 'arg3'));
$result = $client->multiCall($session, array(
array('somestuff.method'),
array('somestuff.method', 'arg1'),
array('somestuff.method', array('arg1', 'arg2'))
));
// If you don't need the session anymore
$client->endSession($session);
where you have www.site.com in your SOAP code, replace it with localhost or whatever the correct URL is for your server. You'll also need to replace somestuff.method with real objects and methods as per the Magento documentation