IpAddressTest.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Ip;
  4. use App\Domain\Ip\InvalidIpException;
  5. use App\Domain\Ip\IpAddress;
  6. use PHPUnit\Framework\Attributes\DataProvider;
  7. use PHPUnit\Framework\TestCase;
  8. final class IpAddressTest extends TestCase
  9. {
  10. public function testParsesSimpleIpv4(): void
  11. {
  12. $ip = IpAddress::fromString('203.0.113.42');
  13. $this->assertTrue($ip->isIpv4());
  14. $this->assertSame('203.0.113.42', $ip->text());
  15. $this->assertSame(16, strlen($ip->binary()));
  16. $this->assertSame(
  17. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xcb\x00\x71\x2a",
  18. $ip->binary()
  19. );
  20. }
  21. public function testParsesIpv4Zero(): void
  22. {
  23. $ip = IpAddress::fromString('0.0.0.0');
  24. $this->assertTrue($ip->isIpv4());
  25. $this->assertSame('0.0.0.0', $ip->text());
  26. $this->assertSame(
  27. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00",
  28. $ip->binary()
  29. );
  30. }
  31. public function testParsesIpv4Max(): void
  32. {
  33. $ip = IpAddress::fromString('255.255.255.255');
  34. $this->assertTrue($ip->isIpv4());
  35. $this->assertSame('255.255.255.255', $ip->text());
  36. }
  37. public function testParsesFullIpv6(): void
  38. {
  39. $ip = IpAddress::fromString('2001:0db8:0000:0000:0000:0000:0000:0001');
  40. $this->assertFalse($ip->isIpv4());
  41. $this->assertSame('2001:db8::1', $ip->text());
  42. $this->assertSame(16, strlen($ip->binary()));
  43. }
  44. public function testParsesZeroCompressedIpv6(): void
  45. {
  46. $ip = IpAddress::fromString('2001:db8::1');
  47. $this->assertFalse($ip->isIpv4());
  48. $this->assertSame('2001:db8::1', $ip->text());
  49. }
  50. public function testParsesIpv6Loopback(): void
  51. {
  52. $ip = IpAddress::fromString('::1');
  53. $this->assertFalse($ip->isIpv4());
  54. $this->assertSame('::1', $ip->text());
  55. }
  56. public function testParsesIpv6Unspecified(): void
  57. {
  58. $ip = IpAddress::fromString('::');
  59. $this->assertFalse($ip->isIpv4());
  60. $this->assertSame('::', $ip->text());
  61. }
  62. public function testParsesV4MappedIpv6KeepsIpv6Form(): void
  63. {
  64. $ip = IpAddress::fromString('::ffff:1.2.3.4');
  65. $this->assertFalse($ip->isIpv4(), 'v4-mapped passed in IPv6 form should report as IPv6');
  66. $this->assertSame(
  67. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x03\x04",
  68. $ip->binary()
  69. );
  70. }
  71. public function testIpv4MapsIntoSameBinaryAsV4MappedIpv6(): void
  72. {
  73. $v4 = IpAddress::fromString('1.2.3.4');
  74. $v6 = IpAddress::fromString('::ffff:1.2.3.4');
  75. $this->assertSame($v4->binary(), $v6->binary());
  76. }
  77. public function testIpv6IsLowercased(): void
  78. {
  79. $ip = IpAddress::fromString('2001:DB8:ABCD::FFFF');
  80. $this->assertSame('2001:db8:abcd::ffff', $ip->text());
  81. }
  82. public function testIpv6FullyExpandedCanonicalizes(): void
  83. {
  84. $ip = IpAddress::fromString('fe80:0000:0000:0000:0000:0000:0000:0001');
  85. $this->assertSame('fe80::1', $ip->text());
  86. }
  87. public function testIpv6LinkLocalLowercased(): void
  88. {
  89. $ip = IpAddress::fromString('FE80::1');
  90. $this->assertSame('fe80::1', $ip->text());
  91. }
  92. public function testFromBinaryRoundtripIpv4(): void
  93. {
  94. $original = IpAddress::fromString('10.20.30.40');
  95. $copy = IpAddress::fromBinary($original->binary());
  96. $this->assertTrue($copy->isIpv4());
  97. $this->assertSame('10.20.30.40', $copy->text());
  98. }
  99. public function testFromBinaryRoundtripIpv6(): void
  100. {
  101. $original = IpAddress::fromString('2001:db8::dead:beef');
  102. $copy = IpAddress::fromBinary($original->binary());
  103. $this->assertFalse($copy->isIpv4());
  104. $this->assertSame('2001:db8::dead:beef', $copy->text());
  105. }
  106. public function testFromBinaryRejectsWrongLength(): void
  107. {
  108. $this->expectException(InvalidIpException::class);
  109. IpAddress::fromBinary("\x01\x02\x03");
  110. }
  111. public function testEqualsCompareByBinary(): void
  112. {
  113. $a = IpAddress::fromString('1.2.3.4');
  114. $b = IpAddress::fromString('::ffff:1.2.3.4');
  115. $c = IpAddress::fromString('1.2.3.5');
  116. $this->assertTrue($a->equals($b));
  117. $this->assertFalse($a->equals($c));
  118. }
  119. /**
  120. * @return iterable<string, array{0: string}>
  121. */
  122. public static function invalidProvider(): iterable
  123. {
  124. yield 'empty' => [''];
  125. yield 'whitespace only' => [' '];
  126. yield 'leading space' => [' 1.2.3.4'];
  127. yield 'trailing space' => ['1.2.3.4 '];
  128. yield 'inner whitespace' => ['1.2. 3.4'];
  129. yield 'integer-as-string' => ['1234567890'];
  130. yield 'three octets' => ['1.2.3'];
  131. yield 'five octets' => ['1.2.3.4.5'];
  132. yield 'octet > 255' => ['1.2.3.256'];
  133. yield 'negative octet' => ['1.2.3.-1'];
  134. yield 'leading zero v4' => ['010.0.0.1'];
  135. yield 'leading zero in last octet' => ['10.0.0.01'];
  136. yield 'empty octet' => ['10..0.1'];
  137. yield 'hex octet' => ['0x10.0.0.1'];
  138. yield 'alpha octet' => ['a.b.c.d'];
  139. yield 'just dots' => ['...'];
  140. yield 'incomplete v6' => ['2001:db8'];
  141. yield 'too many groups v6' => ['1:2:3:4:5:6:7:8:9'];
  142. yield 'invalid hex group v6' => ['ggggg::1'];
  143. yield 'double :: v6' => ['1::2::3'];
  144. yield 'bracketed v6' => ['[::1]'];
  145. yield 'zone id v6' => ['fe80::1%eth0'];
  146. yield 'random garbage' => ['hello world'];
  147. yield 'sql injection-ish' => ["1.2.3.4'; DROP TABLE--"];
  148. yield 'newline embedded' => ["1.2.3.4\n"];
  149. yield 'tab embedded' => ["1.2.3.4\t"];
  150. yield 'just slash' => ['/'];
  151. yield 'with prefix' => ['1.2.3.4/24'];
  152. yield 'just colon' => [':'];
  153. yield 'just dot' => ['.'];
  154. yield 'numeric overflow octet' => ['1.2.3.99999'];
  155. }
  156. #[DataProvider('invalidProvider')]
  157. public function testRejectsInvalidInput(string $input): void
  158. {
  159. $this->expectException(InvalidIpException::class);
  160. IpAddress::fromString($input);
  161. }
  162. }