Skip to content

Commit 872f57a

Browse files
committed
added HttpAssert & Assert::fetch()
1 parent b8ac5d7 commit 872f57a

File tree

4 files changed

+438
-0
lines changed

4 files changed

+438
-0
lines changed

src/Framework/Assert.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,22 @@ public static function matchFile(string $file, string $actual, ?string $descript
482482
}
483483

484484

485+
/**
486+
* Creates HTTP request for chaining expectations.
487+
*/
488+
public static function fetch(
489+
string $url,
490+
string $method = 'GET',
491+
array $headers = [],
492+
array $cookies = [],
493+
bool $follow = false,
494+
?string $body = null,
495+
): HttpAssert
496+
{
497+
return HttpAssert::fetch($url, $method, $headers, $cookies, $follow, $body);
498+
}
499+
500+
485501
/**
486502
* Assertion that fails.
487503
*/

src/Framework/HttpAssert.php

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Tester.
5+
* Copyright (c) 2009 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Tester;
11+
12+
use function curl_close, curl_error, curl_exec, curl_getinfo, curl_init, curl_setopt, explode, is_int, is_string, rtrim, str_contains, strtoupper, substr, trim;
13+
14+
15+
/**
16+
* HTTP testing helpers.
17+
*/
18+
class HttpAssert
19+
{
20+
private function __construct(
21+
private string $body,
22+
private int $code,
23+
private array $headers,
24+
) {
25+
}
26+
27+
28+
/**
29+
* Creates HTTP request, executes it and returns HttpTest instance for chaining expectations.
30+
*/
31+
public static function fetch(
32+
string $url,
33+
string $method = 'GET',
34+
array $headers = [],
35+
array $cookies = [],
36+
bool $follow = false,
37+
?string $body = null,
38+
): self
39+
{
40+
$ch = curl_init();
41+
curl_setopt($ch, CURLOPT_URL, $url);
42+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
43+
curl_setopt($ch, CURLOPT_HEADER, true);
44+
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $follow);
45+
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
46+
47+
if ($headers) {
48+
$headerList = [];
49+
foreach ($headers as $key => $value) {
50+
if (is_int($key)) {
51+
$headerList[] = $value;
52+
} else {
53+
$headerList[] = "$key: $value";
54+
}
55+
}
56+
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerList);
57+
}
58+
59+
if ($body !== null) {
60+
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
61+
}
62+
63+
if ($cookies) {
64+
$cookieString = '';
65+
foreach ($cookies as $name => $value) {
66+
$cookieString .= "$name=$value; ";
67+
}
68+
curl_setopt($ch, CURLOPT_COOKIE, rtrim($cookieString, '; '));
69+
}
70+
71+
$response = curl_exec($ch);
72+
if ($response === false) {
73+
throw new \Exception('HTTP request failed: ' . curl_error($ch));
74+
}
75+
76+
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
77+
$res = new self(
78+
substr($response, $headerSize),
79+
curl_getinfo($ch, CURLINFO_HTTP_CODE),
80+
[],
81+
);
82+
83+
$headerString = substr($response, 0, $headerSize);
84+
foreach (explode("\r\n", $headerString) as $line) {
85+
if (str_contains($line, ':')) {
86+
[$name, $value] = explode(':', $line, 2);
87+
$res->headers[strtolower(trim($name))] = trim($value);
88+
}
89+
}
90+
91+
curl_close($ch);
92+
return $res;
93+
}
94+
95+
96+
/**
97+
* Asserts HTTP response code matches expectation.
98+
*/
99+
public function expectCode(int|\Closure $expected): self
100+
{
101+
if ($expected instanceof \Closure) {
102+
Assert::true($expected($this->code), 'HTTP status code validation failed');
103+
} else {
104+
Assert::same($expected, $this->code, "Expected HTTP status code $expected");
105+
}
106+
107+
return $this;
108+
}
109+
110+
111+
/**
112+
* Asserts HTTP response header matches expectation.
113+
*/
114+
public function expectHeader(
115+
string $name,
116+
string|\Closure|null $expected = null,
117+
?string $contains = null,
118+
?string $matches = null,
119+
): self
120+
{
121+
$headerValue = $this->headers[strtolower($name)] ?? null;
122+
Assert::true(isset($headerValue), "Header '$name' should exist");
123+
124+
if (is_string($expected)) {
125+
Assert::same($expected, $headerValue, "Header '$name' should equal '$expected'");
126+
} elseif ($expected instanceof \Closure) {
127+
Assert::true($expected($headerValue), "Header '$name' validation failed");
128+
} elseif ($contains !== null) {
129+
Assert::contains($contains, $headerValue, "Header '$name' should contain '$contains'");
130+
} elseif ($matches !== null) {
131+
Assert::match($matches, $headerValue, "Header '$name' should match pattern '$matches'");
132+
}
133+
134+
return $this;
135+
}
136+
137+
138+
/**
139+
* Asserts HTTP response body matches expectation.
140+
*/
141+
public function expectBody(
142+
string|\Closure|null $expected = null,
143+
?string $contains = null,
144+
?string $matches = null,
145+
): self
146+
{
147+
if (is_string($expected)) {
148+
Assert::same($expected, $this->body, 'Body should equal expected value');
149+
} elseif ($expected instanceof \Closure) {
150+
Assert::true($expected($this->body), 'Body validation failed');
151+
} elseif ($contains !== null) {
152+
Assert::contains($contains, $this->body, "Body should contain '$contains'");
153+
} elseif ($matches !== null) {
154+
Assert::match($matches, $this->body, "Body should match pattern '$matches'");
155+
}
156+
157+
return $this;
158+
}
159+
160+
161+
/**
162+
* Asserts HTTP response code does not match expectation.
163+
*/
164+
public function denyCode(int|\Closure $expected): self
165+
{
166+
if ($expected instanceof \Closure) {
167+
Assert::false($expected($this->code), 'HTTP status code should not match condition');
168+
} else {
169+
Assert::notSame($expected, $this->code, "HTTP status code should not be $expected");
170+
}
171+
172+
return $this;
173+
}
174+
175+
176+
/**
177+
* Asserts HTTP response header does not match expectation.
178+
*/
179+
public function denyHeader(
180+
string $name,
181+
string|\Closure|null $expected = null,
182+
?string $contains = null,
183+
?string $matches = null,
184+
): self
185+
{
186+
$headerValue = $this->headers[strtolower($name)] ?? null;
187+
if (!isset($headerValue)) {
188+
return $this;
189+
}
190+
191+
if (is_string($expected)) {
192+
Assert::notSame($expected, $headerValue, "Header '$name' should not equal '$expected'");
193+
} elseif ($expected instanceof \Closure) {
194+
Assert::falsey($expected($headerValue), "Header '$name' should not match condition");
195+
} elseif ($contains !== null) {
196+
Assert::notContains($contains, $headerValue, "Header '$name' should not contain '$contains'");
197+
} elseif ($matches !== null) {
198+
Assert::notMatch($matches, $headerValue, "Header '$name' should not match pattern '$matches'");
199+
}
200+
201+
return $this;
202+
}
203+
204+
205+
/**
206+
* Asserts HTTP response body does not match expectation.
207+
*/
208+
public function denyBody(
209+
string|\Closure|null $expected = null,
210+
?string $contains = null,
211+
?string $matches = null,
212+
): self
213+
{
214+
if (is_string($expected)) {
215+
Assert::notSame($expected, $this->body, 'Body should not equal expected value');
216+
} elseif ($expected instanceof \Closure) {
217+
Assert::falsey($expected($this->body), 'Body should not match condition');
218+
} elseif ($contains !== null) {
219+
Assert::notContains($contains, $this->body, "Body should not contain '$contains'");
220+
} elseif ($matches !== null) {
221+
Assert::notMatch($matches, $this->body, "Body should not match pattern '$matches'");
222+
}
223+
return $this;
224+
}
225+
}

src/bootstrap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require __DIR__ . '/Framework/TestCase.php';
1717
require __DIR__ . '/Framework/FileMutator.php';
1818
require __DIR__ . '/Framework/Expect.php';
19+
require __DIR__ . '/Framework/HttpAssert.php';
1920
require __DIR__ . '/CodeCoverage/Collector.php';
2021
require __DIR__ . '/Runner/Job.php';
2122

0 commit comments

Comments
 (0)