diff --git a/tests/Unit/AdminHelpersTest.php b/tests/Unit/AdminHelpersTest.php new file mode 100644 index 00000000..eecd9fe5 --- /dev/null +++ b/tests/Unit/AdminHelpersTest.php @@ -0,0 +1,83 @@ +assertTrue(AdminHelpers::validateCIDR('192.168.1.1')); + $this->assertTrue(AdminHelpers::validateCIDR('192.168.1.0/24')); + $this->assertTrue(AdminHelpers::validateCIDR('::1')); + } + + public function testValidateCidrRejectsInvalidInput() { + $this->assertFalse(AdminHelpers::validateCIDR('not-an-ip')); + $this->assertFalse(AdminHelpers::validateCIDR('10.0.0.0/33')); + $this->assertFalse(AdminHelpers::validateCIDR('2001:db8::/129')); + } + + public function testRoundUpToAny() { + $this->assertEquals(10, AdminHelpers::roundUpToAny(7, 5)); + $this->assertEquals(15, AdminHelpers::roundUpToAny(13, 5)); + $this->assertEquals(5, AdminHelpers::roundUpToAny(3, 5)); + } + + public function testGenerateStringLengthAndUnambiguousCharset() { + $charset = '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'; + $s = AdminHelpers::generateString(16); + $this->assertSame(16, strlen($s)); + $this->assertSame(strlen($s), strspn($s, $charset)); + } + + public function testSortArrayByArray() { + $this->assertSame( + array('c', 'b', 'a'), + AdminHelpers::sortArrayByArray(array('a', 'b', 'c'), array('c', 'b', 'a')) + ); + } + + public function testGetBarColourThresholds() { + $this->assertSame('bg-danger', AdminHelpers::getBarColour(80)); + $this->assertSame('bg-danger', AdminHelpers::getBarColour(75)); + $this->assertSame('bg-warning', AdminHelpers::getBarColour(50)); + $this->assertSame('bg-success', AdminHelpers::getBarColour(10)); + } + + public function testFormatUptime() { + $this->assertSame('01h 01m 01s', AdminHelpers::formatUptime(3661)); + $this->assertSame('01d 01h 00m', AdminHelpers::formatUptime(90000)); + } + + public function testFilterIdsKeepsOnlyAllowedPositiveInts() { + $this->assertSame( + array(1, 2), + AdminHelpers::filterIDs(array('1', '2', 'x', 5), array(1, 2, 3)) + ); + } + + public function testGetPageFromUrl() { + $this->assertSame('bouquet', AdminHelpers::getPageFromURL('http://host/admin/bouquet.php')); + $this->assertNull(AdminHelpers::getPageFromURL('')); + } + + public function testProtocolDetectionFromServerVars() { + $saved = $_SERVER; + + $_SERVER['HTTPS'] = 'on'; + $this->assertTrue(AdminHelpers::issecure()); + $this->assertSame('https', AdminHelpers::getProtocol()); + + unset($_SERVER['HTTPS']); + $_SERVER['SERVER_PORT'] = 80; + $this->assertFalse(AdminHelpers::issecure()); + $this->assertSame('http', AdminHelpers::getProtocol()); + + $_SERVER['SERVER_PORT'] = 443; + $this->assertTrue(AdminHelpers::issecure()); + + $_SERVER = $saved; + } +} diff --git a/tests/Unit/CoreEnumTest.php b/tests/Unit/CoreEnumTest.php new file mode 100644 index 00000000..cb2ac273 --- /dev/null +++ b/tests/Unit/CoreEnumTest.php @@ -0,0 +1,34 @@ +assertSame('minimal', BootContext::Minimal->value); + $this->assertSame('cli', BootContext::Cli->value); + $this->assertSame('stream', BootContext::Stream->value); + $this->assertSame('admin', BootContext::Admin->value); + } + + public function testBootContextFromString() { + $this->assertSame(BootContext::Admin, BootContext::from('admin')); + $this->assertNull(BootContext::tryFrom('nope')); + $this->assertCount(4, BootContext::cases()); + } + + public function testServerEnvironmentValues() { + $this->assertSame('main', ServerEnvironment::Main->value); + $this->assertSame('lb', ServerEnvironment::LoadBalancer->value); + } + + public function testServerEnvironmentFromString() { + $this->assertSame(ServerEnvironment::LoadBalancer, ServerEnvironment::from('lb')); + $this->assertNull(ServerEnvironment::tryFrom('xxx')); + $this->assertCount(2, ServerEnvironment::cases()); + } +} diff --git a/tests/Unit/EncryptionTest.php b/tests/Unit/EncryptionTest.php new file mode 100644 index 00000000..b85978e4 --- /dev/null +++ b/tests/Unit/EncryptionTest.php @@ -0,0 +1,72 @@ +assertNotSame($plain, $cipher); + $this->assertSame($plain, Encryption::decrypt($cipher, 'secret-key', 'device-1')); + } + + public function testEncryptIsDeterministicForSameInputs() { + // Same data/key/device → same ciphertext (derived key + IV are deterministic). + $a = Encryption::encrypt('data', 'k', 'd'); + $b = Encryption::encrypt('data', 'k', 'd'); + $this->assertSame($a, $b); + } + + public function testDecryptWithWrongKeyFails() { + $cipher = Encryption::encrypt('secret', 'right-key', 'device'); + $this->assertFalse(Encryption::decrypt($cipher, 'wrong-key', 'device')); + } + + public function testDecryptWithWrongDeviceFails() { + $cipher = Encryption::encrypt('secret', 'key', 'device-a'); + $this->assertFalse(Encryption::decrypt($cipher, 'key', 'device-b')); + } + + public function testBase64urlIsUrlSafeAndUnpadded() { + $raw = "\xff\xfe\xfd\x00binary?"; + $encoded = Encryption::base64urlEncode($raw); + + $this->assertStringNotContainsString('+', $encoded); + $this->assertStringNotContainsString('/', $encoded); + $this->assertStringNotContainsString('=', $encoded); + $this->assertSame($raw, Encryption::base64urlDecode($encoded)); + } + + public function testRandomStringLengthAndCharset() { + $s = Encryption::randomString(40); + $this->assertSame(40, strlen($s)); + $this->assertMatchesRegularExpression('/^[A-Za-z0-9]+$/', $s); + } + + public function testRandomTokenLengthIsHex() { + $token = Encryption::randomToken(16); + $this->assertSame(32, strlen($token)); + $this->assertMatchesRegularExpression('/^[0-9a-f]+$/', $token); + } + + public function testGenerateKeyByteLength() { + $this->assertSame(16, strlen(Encryption::generateKey(128))); + $this->assertSame(32, strlen(Encryption::generateKey(256))); + } + + public function testGenerateIvLengthForAes128() { + $this->assertSame(16, strlen(Encryption::generateIV('AES-128-CBC'))); + } + + public function testGenerateUniqueCodeIsStable15Chars() { + $code = Encryption::generateUniqueCode('pass'); + $this->assertSame(15, strlen($code)); + $this->assertSame($code, Encryption::generateUniqueCode('pass')); + $this->assertNotSame($code, Encryption::generateUniqueCode('other')); + } +} diff --git a/tests/Unit/ImageUtilsTest.php b/tests/Unit/ImageUtilsTest.php new file mode 100644 index 00000000..72237f2c --- /dev/null +++ b/tests/Unit/ImageUtilsTest.php @@ -0,0 +1,34 @@ +assertEquals(100, $size['width']); + $this->assertEquals(50, $size['height']); + } + + public function testKeepAspectRatioDoesNotUpscale() { + $size = ImageUtils::getImageSizeKeepAspectRatio(50, 50, 100, 100); + $this->assertEquals(50, $size['width']); + $this->assertEquals(50, $size['height']); + } + + public function testKeepAspectRatioTreatsZeroMaxAsUnbounded() { + $size = ImageUtils::getImageSizeKeepAspectRatio(200, 100, 0, 50); + $this->assertEquals(100, $size['width']); + $this->assertEquals(50, $size['height']); + } + + public function testIsAbsoluteUrl() { + $this->assertTrue(ImageUtils::isAbsoluteUrl('http://example.com/a.png')); + $this->assertTrue(ImageUtils::isAbsoluteUrl('https://example.com/a.png')); + $this->assertFalse(ImageUtils::isAbsoluteUrl('/relative/a.png')); + $this->assertFalse(ImageUtils::isAbsoluteUrl('a.png')); + } +} diff --git a/tests/Unit/QueryHelperTest.php b/tests/Unit/QueryHelperTest.php new file mode 100644 index 00000000..8ba92ae0 --- /dev/null +++ b/tests/Unit/QueryHelperTest.php @@ -0,0 +1,40 @@ +assertSame('foobar23', QueryHelper::prepareColumn('Foo Bar!23')); + $this->assertSame('user_id', QueryHelper::prepareColumn('user_id')); + $this->assertSame('droptable', QueryHelper::prepareColumn('drop;table')); + } + + public function testPrepareArrayBuildsColumnsPlaceholdersAndData() { + $result = QueryHelper::prepareArray(array('name' => 'x', 'age' => 5)); + + $this->assertSame('`name`,`age`', $result['columns']); + $this->assertSame('?,?', $result['placeholder']); + $this->assertSame(array('x', 5), $result['data']); + $this->assertSame('`name` = ?,`age` = ?', $result['update']); + } + + public function testPrepareArrayJsonEncodesArrayValues() { + $result = QueryHelper::prepareArray(array('tags' => array(1, 2, 3))); + $this->assertSame('[1,2,3]', $result['data'][0]); + } + + public function testPrepareArrayNormalizesNullValues() { + $result = QueryHelper::prepareArray(array('a' => null, 'b' => 'null')); + $this->assertNull($result['data'][0]); + $this->assertNull($result['data'][1]); + } + + public function testPrepareArraySanitizesColumnNames() { + $result = QueryHelper::prepareArray(array('na me!' => 'v')); + $this->assertSame('`name`', $result['columns']); + } +} diff --git a/tests/Unit/StreamUtilsTest.php b/tests/Unit/StreamUtilsTest.php new file mode 100644 index 00000000..211232f1 --- /dev/null +++ b/tests/Unit/StreamUtilsTest.php @@ -0,0 +1,22 @@ +assertSame(-1, StreamUtils::customOrder('-i input.ts', 'something')); + $this->assertSame(1, StreamUtils::customOrder('-c:v libx264', '-i input.ts')); + } + + public function testDetectXcVmMatchesKnownStreamPaths() { + $this->assertTrue(StreamUtils::detectXC_VM('http://host/live/user/123')); + } + + public function testDetectXcVmRejectsUnrelatedPaths() { + $this->assertFalse(StreamUtils::detectXC_VM('http://host/dashboard')); + } +} diff --git a/tests/Unit/TimeUtilsTest.php b/tests/Unit/TimeUtilsTest.php new file mode 100644 index 00000000..b19a9b16 --- /dev/null +++ b/tests/Unit/TimeUtilsTest.php @@ -0,0 +1,54 @@ +assertSame('1h 1m 1s', TimeUtils::secondsToTime(3661)); + $this->assertSame('1d 0s', TimeUtils::secondsToTime(86400)); + $this->assertSame('1m 30s', TimeUtils::secondsToTime(90)); + $this->assertSame('0s', TimeUtils::secondsToTime(0)); + } + + public function testSecondsToTimeWithoutSeconds() { + $this->assertSame('1h 1m', TimeUtils::secondsToTime(3661, false)); + $this->assertSame('1d', TimeUtils::secondsToTime(86400, false)); + } + + public function testDurationToSecondsThreeParts() { + $this->assertSame(3661, TimeUtils::durationToSeconds('01:01:01')); + $this->assertSame(7200, TimeUtils::durationToSeconds('02:00:00')); + } + + public function testDurationToSecondsTwoParts() { + $this->assertSame(90, TimeUtils::durationToSeconds('01:30')); + } + + public function testDurationToSecondsPlainNumber() { + $this->assertSame(42, TimeUtils::durationToSeconds('42')); + } + + public function testTimeAgoBuckets() { + $now = time(); + $this->assertStringEndsWith('s ago', TimeUtils::timeAgo($now - 5)); + $this->assertStringEndsWith('m ago', TimeUtils::timeAgo($now - 120)); + $this->assertStringEndsWith('h ago', TimeUtils::timeAgo($now - 7200)); + $this->assertStringEndsWith('d ago', TimeUtils::timeAgo($now - 172800)); + } + + public function testNowMatchesFormat() { + $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', TimeUtils::now()); + $this->assertMatchesRegularExpression('/^\d{4}$/', TimeUtils::now('Y')); + } + + public function testGetDiffTimezoneReturnsInteger() { + $diff = TimeUtils::getDiffTimezone('UTC'); + $this->assertIsInt($diff); + // Both anchors are "now in UTC"; allow ±1s for a second-boundary race. + $this->assertLessThanOrEqual(1, abs($diff)); + } +}