diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index d5f843b9..fcd49604 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -1219,4 +1219,104 @@ final class Expectation return $this; } + + /** + * Asserts that the value is an IP address. + * + * @return self + */ + public function toBeIpAddress(string $message = ''): self + { + if (! is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + + Assert::assertTrue((bool) filter_var($this->value, FILTER_VALIDATE_IP), $message); + + return $this; + } + + /** + * Asserts that the value is a MAC address. + * + * @return self + */ + public function toBeMacAddress(string $message = ''): self + { + if (! is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + + Assert::assertTrue((bool) filter_var($this->value, FILTER_VALIDATE_MAC), $message); + + return $this; + } + + /** + * Asserts that the value is a hostname. + * + * @return self + */ + public function toBeHostname(string $message = ''): self + { + if (! is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + + Assert::assertTrue((bool) filter_var($this->value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME), $message); + + return $this; + } + + /** + * Asserts that the value is a domain name. + * + * @return self + */ + public function toBeDomain(string $message = ''): self + { + if (! is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + + $isValid = filter_var($this->value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false + && str_contains($this->value, '.'); + + Assert::assertTrue($isValid, $message); + + return $this; + } + + /** + * Asserts that the value is a base64-encoded string. + * + * @return self + */ + public function toBeBase64(string $message = ''): self + { + if (! is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + + $decoded = base64_decode($this->value, true); + Assert::assertTrue($decoded !== false && base64_encode($decoded) === $this->value, $message); + + return $this; + } + + /** + * Asserts that the value is a hexadecimal string. + * + * @return self + */ + public function toBeHexadecimal(string $message = ''): self + { + if (! is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + + Assert::assertTrue(ctype_xdigit($this->value), $message); + + return $this; + } } diff --git a/tests/Features/Expect/toBeBase64.php b/tests/Features/Expect/toBeBase64.php new file mode 100644 index 00000000..06bfe965 --- /dev/null +++ b/tests/Features/Expect/toBeBase64.php @@ -0,0 +1,42 @@ +toBeBase64() // 'foo' (no padding) + ->and('Zm9vYg==')->toBeBase64() // 'foob' (with padding) + ->and('Zm9vYmE=')->toBeBase64() // 'fooba' (with padding) + ->and('Zm9vYmFy')->toBeBase64() // 'foobar' (no padding) + ->and('')->toBeBase64(); // empty string +}); + +test('failures', function (): void { + expect('not-base64!')->toBeBase64(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid padding', function (): void { + expect('AAA')->toBeBase64(); +})->throws(ExpectationFailedException::class); + +test('failures with padding only', function (): void { + expect('====')->toBeBase64(); +})->throws(ExpectationFailedException::class); + +test('failures with malformed input', function (): void { + expect('!!invalid!!')->toBeBase64(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid type', function (): void { + expect([])->toBeBase64(); +})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); + +test('failures with custom message', function (): void { + expect('!!invalid!!')->toBeBase64('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('not failures', function (): void { + expect('Zm9v')->not->toBeBase64(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeDomain.php b/tests/Features/Expect/toBeDomain.php new file mode 100644 index 00000000..694dcf34 --- /dev/null +++ b/tests/Features/Expect/toBeDomain.php @@ -0,0 +1,33 @@ +toBeDomain() // standard domain + ->and('sub.example.com')->toBeDomain() // subdomain + ->and('my-host.io')->toBeDomain() // with hyphen + ->and('example.co.uk')->toBeDomain(); // multi-level TLD +}); + +test('failures', function (): void { + expect('example')->toBeDomain(); +})->throws(ExpectationFailedException::class); + +test('failures with leading dot', function (): void { + expect('.example.com')->toBeDomain(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid type', function (): void { + expect([])->toBeDomain(); +})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); + +test('failures with custom message', function (): void { + expect('example')->toBeDomain('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('not failures', function (): void { + expect('example.com')->not->toBeDomain(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeHexadecimal.php b/tests/Features/Expect/toBeHexadecimal.php new file mode 100644 index 00000000..aeb7eef7 --- /dev/null +++ b/tests/Features/Expect/toBeHexadecimal.php @@ -0,0 +1,42 @@ +toBeHexadecimal() // lowercase + ->and('ABCDEF')->toBeHexadecimal() // uppercase + ->and('aBcDeF')->toBeHexadecimal() // mixed case + ->and('1234567890')->toBeHexadecimal() // numeric + ->and('deadbeef')->toBeHexadecimal(); // alphanumeric +}); + +test('failures', function (): void { + expect('xyz')->toBeHexadecimal(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid characters', function (): void { + expect('ghijkl')->toBeHexadecimal(); +})->throws(ExpectationFailedException::class); + +test('failures with zero-x prefix', function (): void { + expect('0x1a')->toBeHexadecimal(); +})->throws(ExpectationFailedException::class); + +test('failures with empty string', function (): void { + expect('')->toBeHexadecimal(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid type', function (): void { + expect([])->toBeHexadecimal(); +})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); + +test('failures with custom message', function (): void { + expect('xyz')->toBeHexadecimal('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('not failures', function (): void { + expect('abcdef')->not->toBeHexadecimal(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeHostname.php b/tests/Features/Expect/toBeHostname.php new file mode 100644 index 00000000..3c542df1 --- /dev/null +++ b/tests/Features/Expect/toBeHostname.php @@ -0,0 +1,33 @@ +toBeHostname() // single label + ->and('example.com')->toBeHostname() // multiple labels + ->and('sub.example.com')->toBeHostname() // subdomain + ->and('my-host')->toBeHostname(); // with hyphen +}); + +test('failures', function (): void { + expect('-example')->toBeHostname(); +})->throws(ExpectationFailedException::class); + +test('failures with trailing hyphen', function (): void { + expect('example-')->toBeHostname(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid type', function (): void { + expect([])->toBeHostname(); +})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); + +test('failures with custom message', function (): void { + expect('-example')->toBeHostname('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('not failures', function (): void { + expect('example.com')->not->toBeHostname(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeIpAddress.php b/tests/Features/Expect/toBeIpAddress.php new file mode 100644 index 00000000..ed4d2d0b --- /dev/null +++ b/tests/Features/Expect/toBeIpAddress.php @@ -0,0 +1,28 @@ +toBeIpAddress() + ->and('::1')->toBeIpAddress() + ->and('2001:db8::1')->toBeIpAddress(); +}); + +test('failures', function (): void { + expect('not-an-ip')->toBeIpAddress(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid type', function (): void { + expect([])->toBeIpAddress(); +})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); + +test('failures with custom message', function (): void { + expect('not-an-ip')->toBeIpAddress('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('not failures', function (): void { + expect('192.168.1.1')->not->toBeIpAddress(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeMacAddress.php b/tests/Features/Expect/toBeMacAddress.php new file mode 100644 index 00000000..012c0b36 --- /dev/null +++ b/tests/Features/Expect/toBeMacAddress.php @@ -0,0 +1,28 @@ +toBeMacAddress() // colon-separated + ->and('00-1a-2b-3c-4d-5e')->toBeMacAddress() // hyphen-separated + ->and('ff:ff:ff:ff:ff:ff')->toBeMacAddress(); +}); + +test('failures', function (): void { + expect('not-a-mac')->toBeMacAddress(); +})->throws(ExpectationFailedException::class); + +test('failures with invalid type', function (): void { + expect([])->toBeMacAddress(); +})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); + +test('failures with custom message', function (): void { + expect('not-a-mac')->toBeMacAddress('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('not failures', function (): void { + expect('00:1a:2b:3c:4d:5e')->not->toBeMacAddress(); +})->throws(ExpectationFailedException::class);