| 
 | 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 | +}  | 
0 commit comments