Skip to content

Commit 2d085f3

Browse files
carlbennettclaude
andcommitted
Add PHPUnit test suite and update CI
- Add phpunit.xml config bootstrapping lib/autoload.php - Add autoload-dev PSR-4 mapping for BNETDocs\Tests\ -> tests/ - Add tests for StringProcessor, HttpCode, and IP (45 tests) - HttpCodeTest includes a failing test documenting the PAYLOAD_TOO_LARGE semicolon bug in HttpCode::codeFromString() - Update php-linter workflow to install php-mbstring and php-xml, lint test files alongside src/, and run PHPUnit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a407a5a commit 2d085f3

6 files changed

Lines changed: 365 additions & 2 deletions

File tree

.github/workflows/php-linter.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
run: sudo apt-get update
2929

3030
- name: Install php ${{ env.PHP_VERSION }}
31-
run: sudo apt-get install php${{ env.PHP_VERSION }}-cli
31+
run: sudo apt-get install php${{ env.PHP_VERSION }}-cli php${{ env.PHP_VERSION }}-mbstring php${{ env.PHP_VERSION }}-xml
3232

3333
- name: Validate composer.json and composer.lock
3434
run: composer validate --strict
@@ -46,4 +46,7 @@ jobs:
4646
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
4747

4848
- name: Validate PHP syntax
49-
run: bash -c 'set -e;for file in $(find ./src -type f -regex ".*\.\(php\|phtml\)" -print); do php -e -l -f "$file"; done'
49+
run: bash -c 'set -e;for file in $(find ./src ./tests -type f -regex ".*\.\(php\|phtml\)" -print); do php -e -l -f "$file"; done'
50+
51+
- name: Run PHPUnit tests
52+
run: lib/bin/phpunit --testdox

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
"php-64bit": "^8.3",
4747
"phpmailer/phpmailer": "^6.0"
4848
},
49+
"autoload-dev": {
50+
"psr-4": {
51+
"BNETDocs\\Tests\\": "tests/"
52+
}
53+
},
4954
"require-dev": {
5055
"phpunit/phpunit": "^10.0"
5156
}

phpunit.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
4+
bootstrap="lib/autoload.php"
5+
colors="true">
6+
<testsuites>
7+
<testsuite name="BNETDocs">
8+
<directory>tests</directory>
9+
</testsuite>
10+
</testsuites>
11+
</phpunit>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace BNETDocs\Tests\Libraries\Core;
4+
5+
use \BNETDocs\Libraries\Core\HttpCode;
6+
use \OutOfBoundsException;
7+
use \PHPUnit\Framework\TestCase;
8+
use \UnexpectedValueException;
9+
10+
class HttpCodeTest extends TestCase
11+
{
12+
// codeFromInt
13+
14+
public function testCodeFromIntOk(): void
15+
{
16+
$this->assertSame('Ok', HttpCode::codeFromInt(200));
17+
}
18+
19+
public function testCodeFromIntNotFound(): void
20+
{
21+
$this->assertSame('Not Found', HttpCode::codeFromInt(404));
22+
}
23+
24+
public function testCodeFromIntInternalServerError(): void
25+
{
26+
$this->assertSame('Internal Server Error', HttpCode::codeFromInt(500));
27+
}
28+
29+
public function testCodeFromIntUnknownThrows(): void
30+
{
31+
$this->expectException(UnexpectedValueException::class);
32+
HttpCode::codeFromInt(999);
33+
}
34+
35+
// codeFromString
36+
37+
public function testCodeFromStringOk(): void
38+
{
39+
$this->assertSame(200, HttpCode::codeFromString('OK'));
40+
}
41+
42+
public function testCodeFromStringNotFound(): void
43+
{
44+
$this->assertSame(404, HttpCode::codeFromString('NOT_FOUND'));
45+
}
46+
47+
public function testCodeFromStringStripsHttpPrefix(): void
48+
{
49+
$this->assertSame(403, HttpCode::codeFromString('HTTP_FORBIDDEN'));
50+
}
51+
52+
public function testCodeFromStringAlias(): void
53+
{
54+
$this->assertSame(403, HttpCode::codeFromString('ACCESS_DENIED'));
55+
}
56+
57+
public function testCodeFromStringUnknownThrows(): void
58+
{
59+
$this->expectException(UnexpectedValueException::class);
60+
HttpCode::codeFromString('MADE_UP_CODE');
61+
}
62+
63+
/**
64+
* HttpCode.php:173 has a semicolon instead of a colon after the case label,
65+
* making 'PAYLOAD_TOO_LARGE' a no-op that falls through to default.
66+
* This test documents the correct expected behavior and will fail until fixed.
67+
*/
68+
public function testCodeFromStringPayloadTooLarge(): void
69+
{
70+
$this->assertSame(413, HttpCode::codeFromString('PAYLOAD_TOO_LARGE'));
71+
}
72+
73+
// isRedirectFromCode
74+
75+
public function testIsRedirectMovedPermanently(): void
76+
{
77+
$this->assertTrue(HttpCode::isRedirectFromCode(301));
78+
}
79+
80+
public function testIsRedirectFound(): void
81+
{
82+
$this->assertTrue(HttpCode::isRedirectFromCode(302));
83+
}
84+
85+
public function testIsRedirectPermanentRedirect(): void
86+
{
87+
$this->assertTrue(HttpCode::isRedirectFromCode(308));
88+
}
89+
90+
public function testIsRedirectOkIsFalse(): void
91+
{
92+
$this->assertFalse(HttpCode::isRedirectFromCode(200));
93+
}
94+
95+
public function testIsRedirectNotFoundIsFalse(): void
96+
{
97+
$this->assertFalse(HttpCode::isRedirectFromCode(404));
98+
}
99+
100+
public function testIsRedirectRangeCheck(): void
101+
{
102+
$this->assertTrue(HttpCode::isRedirectFromCode(350));
103+
}
104+
105+
public function testIsRedirectRangeCheckDisabled(): void
106+
{
107+
$this->assertFalse(HttpCode::isRedirectFromCode(350, false));
108+
}
109+
110+
// setCode / constructor
111+
112+
public function testConstructorWithInt(): void
113+
{
114+
$http = new HttpCode(200);
115+
$this->assertSame(200, $http->getCode());
116+
}
117+
118+
public function testConstructorWithString(): void
119+
{
120+
$http = new HttpCode('OK');
121+
$this->assertSame(200, $http->getCode());
122+
}
123+
124+
public function testSetCodeOutOfBoundsThrows(): void
125+
{
126+
$this->expectException(OutOfBoundsException::class);
127+
new HttpCode(99);
128+
}
129+
}

tests/Libraries/Core/IPTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace BNETDocs\Tests\Libraries\Core;
4+
5+
use \BNETDocs\Libraries\Core\IP;
6+
use \PHPUnit\Framework\TestCase;
7+
use \UnexpectedValueException;
8+
9+
class IPTest extends TestCase
10+
{
11+
// checkCIDRv4
12+
13+
public function testCheckCIDRv4Match(): void
14+
{
15+
$this->assertTrue(IP::checkCIDRv4('192.168.1.100', '192.168.1.0/24'));
16+
}
17+
18+
public function testCheckCIDRv4NoMatch(): void
19+
{
20+
$this->assertFalse(IP::checkCIDRv4('10.0.0.1', '192.168.1.0/24'));
21+
}
22+
23+
public function testCheckCIDRv4HostRoute(): void
24+
{
25+
$this->assertTrue(IP::checkCIDRv4('192.168.1.1', '192.168.1.1/32'));
26+
}
27+
28+
public function testCheckCIDRv4HostRouteNoMatch(): void
29+
{
30+
$this->assertFalse(IP::checkCIDRv4('192.168.1.2', '192.168.1.1/32'));
31+
}
32+
33+
// checkCIDRv6
34+
35+
public function testCheckCIDRv6Match(): void
36+
{
37+
$this->assertTrue(IP::checkCIDRv6('2001:db8::1', '2001:db8::/32'));
38+
}
39+
40+
public function testCheckCIDRv6NoMatch(): void
41+
{
42+
$this->assertFalse(IP::checkCIDRv6('2001:db9::1', '2001:db8::/32'));
43+
}
44+
45+
public function testCheckCIDRv6HostRoute(): void
46+
{
47+
$this->assertTrue(IP::checkCIDRv6('::1', '::1/128'));
48+
}
49+
50+
// checkCIDR
51+
52+
public function testCheckCIDRRoutesV4(): void
53+
{
54+
$this->assertTrue(IP::checkCIDR('10.0.0.1', '10.0.0.0/8'));
55+
}
56+
57+
public function testCheckCIDRRoutesV6(): void
58+
{
59+
$this->assertTrue(IP::checkCIDR('2001:db8::1', '2001:db8::/32'));
60+
}
61+
62+
public function testCheckCIDRMixedVersionThrows(): void
63+
{
64+
$this->expectException(UnexpectedValueException::class);
65+
IP::checkCIDR('192.168.1.1', '2001:db8::/32');
66+
}
67+
68+
// checkCIDRArray
69+
70+
public function testCheckCIDRArrayMatchesFirst(): void
71+
{
72+
$cidrs = ['10.0.0.0/8', '192.168.0.0/16'];
73+
$this->assertTrue(IP::checkCIDRArray('10.1.2.3', $cidrs));
74+
}
75+
76+
public function testCheckCIDRArrayMatchesSecond(): void
77+
{
78+
$cidrs = ['10.0.0.0/8', '192.168.0.0/16'];
79+
$this->assertTrue(IP::checkCIDRArray('192.168.1.1', $cidrs));
80+
}
81+
82+
public function testCheckCIDRArrayNoMatch(): void
83+
{
84+
$cidrs = ['10.0.0.0/8', '192.168.0.0/16'];
85+
$this->assertFalse(IP::checkCIDRArray('172.16.0.1', $cidrs));
86+
}
87+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
namespace BNETDocs\Tests\Libraries\Core;
4+
5+
use \BNETDocs\Libraries\Core\StringProcessor;
6+
use \PHPUnit\Framework\TestCase;
7+
8+
class StringProcessorTest extends TestCase
9+
{
10+
// fuzzyMatch
11+
12+
public function testFuzzyMatchWildcardSuffix(): void
13+
{
14+
$this->assertTrue(StringProcessor::fuzzyMatch('api.*', 'api.enabled'));
15+
}
16+
17+
public function testFuzzyMatchWildcardSuffixNoMatch(): void
18+
{
19+
$this->assertFalse(StringProcessor::fuzzyMatch('api.*', 'other.enabled'));
20+
}
21+
22+
public function testFuzzyMatchExact(): void
23+
{
24+
$this->assertTrue(StringProcessor::fuzzyMatch('exact', 'exact'));
25+
}
26+
27+
public function testFuzzyMatchCaseInsensitive(): void
28+
{
29+
$this->assertTrue(StringProcessor::fuzzyMatch('exact', 'EXACT'));
30+
}
31+
32+
public function testFuzzyMatchWildcardMiddle(): void
33+
{
34+
$this->assertTrue(StringProcessor::fuzzyMatch('foo.*.bar', 'foo.baz.bar'));
35+
}
36+
37+
// isBrowser
38+
39+
public function testIsBrowserFirefox(): void
40+
{
41+
$this->assertTrue(StringProcessor::isBrowser('Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0'));
42+
}
43+
44+
public function testIsBrowserCurl(): void
45+
{
46+
$this->assertFalse(StringProcessor::isBrowser('curl/7.88.1'));
47+
}
48+
49+
public function testIsBrowserEmpty(): void
50+
{
51+
$this->assertFalse(StringProcessor::isBrowser(''));
52+
}
53+
54+
// sanitizeForUrl
55+
56+
public function testSanitizeForUrlBasic(): void
57+
{
58+
$this->assertSame('hello-world', StringProcessor::sanitizeForUrl('Hello World!'));
59+
}
60+
61+
public function testSanitizeForUrlTrimsLeadingTrailingDashes(): void
62+
{
63+
$this->assertSame('hello', StringProcessor::sanitizeForUrl(' hello '));
64+
}
65+
66+
public function testSanitizeForUrlCollapsesMultipleSeparators(): void
67+
{
68+
$this->assertSame('foo-bar', StringProcessor::sanitizeForUrl('foo---bar'));
69+
}
70+
71+
public function testSanitizeForUrlLowercaseFalse(): void
72+
{
73+
$this->assertSame('Hello-World', StringProcessor::sanitizeForUrl('Hello World', false));
74+
}
75+
76+
public function testSanitizeForUrlArray(): void
77+
{
78+
$this->assertSame(['hello-world', 'foo-bar'], StringProcessor::sanitizeForUrl(['Hello World', 'Foo Bar']));
79+
}
80+
81+
// stripExcessLines
82+
83+
public function testStripExcessLinesCollapsesMultiple(): void
84+
{
85+
$this->assertSame("a\n\nb", StringProcessor::stripExcessLines("a\n\n\n\nb"));
86+
}
87+
88+
public function testStripExcessLinesLeavesDoubleNewlineAlone(): void
89+
{
90+
$this->assertSame("a\n\nb", StringProcessor::stripExcessLines("a\n\nb"));
91+
}
92+
93+
public function testStripExcessLinesSingleNewlineUnchanged(): void
94+
{
95+
$this->assertSame("a\nb", StringProcessor::stripExcessLines("a\nb"));
96+
}
97+
98+
// stripLeftPattern
99+
100+
public function testStripLeftPatternRemovesPrefix(): void
101+
{
102+
$this->assertSame('bar', StringProcessor::stripLeftPattern('foobar', 'foo'));
103+
}
104+
105+
public function testStripLeftPatternNoMatchReturnsOriginal(): void
106+
{
107+
$this->assertSame('foobar', StringProcessor::stripLeftPattern('foobar', 'baz'));
108+
}
109+
110+
public function testStripLeftPatternEmptyNeedle(): void
111+
{
112+
$this->assertSame('foobar', StringProcessor::stripLeftPattern('foobar', ''));
113+
}
114+
115+
// stripToSnippet
116+
117+
public function testStripToSnippetShortStringUnchanged(): void
118+
{
119+
$this->assertSame('short', StringProcessor::stripToSnippet('short', 100));
120+
}
121+
122+
public function testStripToSnippetTruncatesAtWordBoundary(): void
123+
{
124+
$result = StringProcessor::stripToSnippet('Hello world foo', 8);
125+
$this->assertStringEndsWith('...', $result);
126+
$this->assertLessThanOrEqual(8, strlen($result));
127+
}
128+
}

0 commit comments

Comments
 (0)