Skip to content

Commit

Permalink
Add support for form parameters. Close #8 (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
christeredvartsen committed Sep 11, 2016
1 parent 0455de9 commit efc9db7
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 5 deletions.
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ v1.0.2
------
__N/A__

* #8: Step(s) for working with form data

Bug fixes:

* #7: Don't allow request body when sending multipart/form-data requests
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The following configuration options are available for the extension:
Given I attach :path to the request as :partName
Given I am authenticating as :username with password :password
Given the :header request header is :value
Given the following form parameters are set: <TableNode>

When I request :path
When I request :path using HTTP :method
Expand Down Expand Up @@ -126,6 +127,22 @@ Trying to force specific headers to have certain values combined with other step
| Given the "`X-Foo`" request header is `Bar`<br>Given the "`X-Foo`" request header is `Baz` | `X-Foo` | `Bar, Baz` |
| Given the `Accept` request header is "`application/json`" | `Accept` | `application/json`|

#### Given the following form parameters are set: `<TableNode>`

This step can be used to set form parameters (as if the request is a `<form>` being submitted. A table node must be used to specify which fields / values to send:

```gherkin
Given the following form parameters are set:
| name | value |
| foo | bar |
| bar | foo |
| bar | bar |
```

The first row in the table must contain two values: `name` and `value`. The rows that follows are the fields / values you want to send. This step sets the HTTP method to `POST` and the `Content-Type` request header to `application/x-www-form-urlencoded`, unless the step is combined with `Given I attach :path to the request as :partName`, in which case the `Content-Type` will be `multipart/form-data` and all the specified fields will be sent as parts in the multipart request.

This step can not be used when sending requests with a request body. Doing so results in an `InvalidArgumentException` exception.

### Send the request

After setting up the request it can be sent to the server in a few different ways. Keep in mind that all configuration regarding the request must be set prior to any of these steps, as they will actually send the request.
Expand Down
7 changes: 7 additions & 0 deletions features/bootstrap/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@
return new JsonResponse($_FILES);
});

/**
* Return information about $_POST and $_FILES vars
*/
$app->post('/formData', function(Request $request) {
return new JsonResponse(['_POST' => $_POST, '_FILES' => $_FILES]);
});

/**
* Return the HTTP method
*/
Expand Down
92 changes: 92 additions & 0 deletions features/form-data.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
Feature: Test form-data handling
In order to test form-data handling
As a developer
I want to be able to test all related steps

Background:
Given a file named "behat.yml" with:
"""
default:
formatters:
progress: ~
extensions:
Imbo\BehatApiExtension:
base_uri: http://localhost:8080
suites:
default:
contexts: ['Imbo\BehatApiExtension\Context\ApiContext']
"""

Scenario: Attach form data to the request
Given a file named "features/attach-form-data.feature" with:
"""
Feature: Set up the request
Scenario: Use the Given step to attach form-data
Given the following form parameters are set:
| name | value |
| foo | bar |
| bar | foo |
| bar | bar |
When I request "/formData"
Then the response body contains:
'''
{
"_POST": {
"foo": "bar",
"bar": ["foo", "bar"]
},
"_FILES": "@length(0)"
}
'''
"""
When I run "behat features/attach-form-data.feature"
Then it should pass with:
"""
...
1 scenario (1 passed)
3 steps (3 passed)
"""

Scenario: Attach form data and files to the request
Given a file named "features/attach-form-data-and-files.feature" with:
"""
Feature: Set up the request
Scenario: Use the Given step to attach form-data
Given the following form parameters are set:
| name | value |
| foo | bar |
| bar | foo |
| bar | bar |
And I attach "behat.yml" to the request as file
When I request "/formData"
Then the response body contains:
'''
{
"_POST": {
"foo": "bar",
"bar": ["foo", "bar"]
},
"_FILES": {
"file": {
"name": "behat.yml",
"type": "text/yaml",
"tmp_name": "<re>/.*/</re>",
"error": 0,
"size": "<re>/[0-9]+/</re>"
}
}
}
'''
"""
When I run "behat features/attach-form-data-and-files.feature"
Then it should pass with:
"""
....
1 scenario (1 passed)
4 steps (4 passed)
"""
58 changes: 56 additions & 2 deletions src/Context/ApiContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ public function givenTheRequestHeaderIs($header, $value) {
$this->addRequestHeader($header, $value);
}

/**
* Set form parameters
*
* @param TableNode $table Table with name / value pairs
* @Given the following form parameters are set:
*/
public function givenTheFollowingFormParametersAreSet(TableNode $table) {
if (!isset($this->requestOptions['form_params'])) {
$this->requestOptions['form_params'] = [];
}

foreach ($table as $row) {
$name = $row['name'];
$value = $row['value'];

if (isset($this->requestOptions['form_params'][$name]) && !is_array($this->requestOptions['form_params'][$name])) {
$this->requestOptions['form_params'][$name] = [$this->requestOptions['form_params'][$name]];
}

if (isset($this->requestOptions['form_params'][$name])) {
$this->requestOptions['form_params'][$name][] = $value;
} else {
$this->requestOptions['form_params'][$name] = $value;
}
}
}

/**
* Request a path using GET or another HTTP method
*
Expand Down Expand Up @@ -486,6 +513,33 @@ protected function getClient() {
* @throws RequestException
*/
private function sendRequest() {
if (!empty($this->requestOptions['form_params'])) {
$this->setRequestMethod('POST');
}

if (!empty($this->requestOptions['multipart']) && !empty($this->requestOptions['form_params'])) {
foreach ($this->requestOptions['form_params'] as $name => $contents) {
if (is_array($contents)) {
$name .= '[]';

foreach ($contents as $content) {
$this->requestOptions['multipart'][] = [
'name' => $name,
'contents' => $content,
];
}
} else {
$this->requestOptions['multipart'][] = [
'name' => $name,
'contents' => $contents
];
}
}

// Remove form_params from the options, otherwise Guzzle will throw an exception
unset($this->requestOptions['form_params']);
}

try {
$this->response = $this->client->send(
$this->request,
Expand Down Expand Up @@ -604,9 +658,9 @@ private function setRequestMethod($method) {
* @return self
*/
private function setRequestBody($body) {
if (isset($this->requestOptions['multipart'])) {
if (!empty($this->requestOptions['multipart']) || !empty($this->requestOptions['form_params'])) {
throw new InvalidArgumentException(
'It\'s not allowed to set a request body when using multipart/form-data.'
'It\'s not allowed to set a request body when using multipart/form-data or form parameters.'
);
}

Expand Down
98 changes: 95 additions & 3 deletions tests/Context/ApiContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\RequestException;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use RuntimeException;

/**
Expand Down Expand Up @@ -344,7 +345,6 @@ public function testWhenIRequestPathWithBody() {
* @covers ::setRequestPath
* @covers ::setRequestBody
* @covers ::sendRequest
* @group whens
*/
public function testWhenIRequestPathWithQueryParameters() {
$this->mockHandler->append(new Response(200));
Expand Down Expand Up @@ -953,12 +953,104 @@ public function testThenTheResponseBodyContainsOnFailure() {
* @covers ::setRequestBody
*/
public function testDontAllowRequestBodyWithMultipartFormDataRequests() {
$this->mockHandler->append(new Response(200, [], '{"foo":"bar"}'));
$this->mockHandler->append(new Response(200));
$this->context->givenIAttachAFileToTheRequest(__FILE__, 'file');

$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('It\'s not allowed to set a request body when using multipart/form-data or form parameters.');

$this->context->whenIRequestPathWithBody('/some/path', 'POST', new PyStringNode(['some body'], 1));
}

/**
* @covers ::givenTheFollowingFormParametersAreSet
* @covers ::sendRequest
*/
public function testGivenTheFollowingFormParametersAreSet() {
$this->mockHandler->append(new Response(200));
$this->context->givenTheFollowingFormParametersAreSet(new TableNode([
['name', 'value'],
['foo', 'bar'],
['bar', 'foo'],
['bar', 'bar'],
]));
$this->context->whenIRequestPath('/some/path');

$this->assertSame(1, count($this->historyContainer));

$request = $this->historyContainer[0]['request'];

$this->assertSame('POST', $request->getMethod());
$this->assertSame('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type'));
$this->assertSame(37, (int) $request->getHeaderLine('Content-Length'));
$this->assertSame('foo=bar&bar%5B0%5D=foo&bar%5B1%5D=bar', (string) $request->getBody());
}

/**
* @covers ::sendRequest
*/
public function testGivenTheFollowingFormParametersAreSetCombinedWithAttachingAFile() {
$this->mockHandler->append(new Response(200));
$this->context->givenTheFollowingFormParametersAreSet(new TableNode([
['name', 'value'],
['foo', 'bar'],
['bar', 'foo'],
['bar', 'bar'],
]));
$this->context->givenIAttachAFileToTheRequest(__FILE__, 'file');
$this->context->whenIRequestPath('/some/path');

$this->assertSame(1, count($this->historyContainer));

$request = $this->historyContainer[0]['request'];
$boundary = $request->getBody()->getBoundary();

$this->assertSame('POST', $request->getMethod());
$this->assertSame(sprintf('multipart/form-data; boundary=%s', $boundary), $request->getHeaderLine('Content-Type'));

$contents = $request->getBody()->getContents();

$this->assertContains('Content-Disposition: form-data; name="file"; filename="ApiContextTest.php"', $contents);
$this->assertContains(file_get_contents(__FILE__), $contents);

$foo = <<<FOO
Content-Disposition: form-data; name="foo"
Content-Length: 3
bar
FOO;

$bar0 = <<<BAR
Content-Disposition: form-data; name="bar[]"
Content-Length: 3
foo
BAR;
$bar1 = <<<BAR
Content-Disposition: form-data; name="bar[]"
Content-Length: 3
bar
BAR;
$this->assertContains($foo, $contents);
$this->assertContains($bar0, $contents);
$this->assertContains($bar1, $contents);
}

/**
* @covers ::setRequestBody
*/
public function testDontAllowRequestBodyWithFormParameters() {
$this->mockHandler->append(new Response(200));
$this->context->givenTheFollowingFormParametersAreSet(new TableNode([
['name', 'value'],
['foo', 'bar'],
]));

$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('It\'s not allowed to set a request body when using multipart/form-data');
$this->expectExceptionMessage('It\'s not allowed to set a request body when using multipart/form-data or form parameters.');

$this->context->whenIRequestPathWithBody('/some/path', 'POST', new PyStringNode(['some body'], 1));
}

}

0 comments on commit efc9db7

Please sign in to comment.