diff --git a/tests/library/ConfigTest.php b/tests/library/ConfigTest.php new file mode 100644 index 000000000..040582458 --- /dev/null +++ b/tests/library/ConfigTest.php @@ -0,0 +1,404 @@ +config = new Config(); + $this->testConfigFile = sys_get_temp_dir() . '/test_config.ini'; + $this->testConfigData = [ + 'database' => [ + 'host' => 'localhost', + 'port' => 3306, + 'username' => 'test_user', + 'password' => 'test_pass', + 'database' => 'test_db' + ], + 'cache' => [ + 'enabled' => true, + 'ttl' => 3600, + 'driver' => 'redis' + ], + 'app' => [ + 'name' => 'Test App', + 'version' => '1.0.0', + 'debug' => false + ] + ]; + } + + protected function tearDown(): void + { + if (file_exists($this->testConfigFile)) { + unlink($this->testConfigFile); + } + } + + // Happy path tests + public function testConfigCanBeInstantiated() + { + $this->assertInstanceOf(Config::class, $this->config); + } + + public function testSetAndGetConfigValue() + { + $this->config->set('test.key', 'test_value'); + $this->assertEquals('test_value', $this->config->get('test.key')); + } + + public function testSetAndGetNestedConfigValue() + { + $this->config->set('database.host', 'localhost'); + $this->config->set('database.port', 3306); + + $this->assertEquals('localhost', $this->config->get('database.host')); + $this->assertEquals(3306, $this->config->get('database.port')); + } + + public function testSetMultipleValuesAtOnce() + { + $this->config->setMultiple($this->testConfigData); + + $this->assertEquals('localhost', $this->config->get('database.host')); + $this->assertEquals(3306, $this->config->get('database.port')); + $this->assertEquals('test_user', $this->config->get('database.username')); + $this->assertTrue($this->config->get('cache.enabled')); + $this->assertEquals('Test App', $this->config->get('app.name')); + } + + public function testHasConfigValue() + { + $this->config->set('test.key', 'value'); + + $this->assertTrue($this->config->has('test.key')); + $this->assertFalse($this->config->has('nonexistent.key')); + } + + public function testRemoveConfigValue() + { + $this->config->set('test.key', 'value'); + $this->assertTrue($this->config->has('test.key')); + + $this->config->remove('test.key'); + $this->assertFalse($this->config->has('test.key')); + } + + public function testGetAllConfig() + { + $this->config->setMultiple($this->testConfigData); + $allConfig = $this->config->getAll(); + + $this->assertIsArray($allConfig); + $this->assertArrayHasKey('database', $allConfig); + $this->assertArrayHasKey('cache', $allConfig); + $this->assertArrayHasKey('app', $allConfig); + } + + public function testGetConfigSection() + { + $this->config->setMultiple($this->testConfigData); + $databaseConfig = $this->config->getSection('database'); + + $this->assertIsArray($databaseConfig); + $this->assertEquals('localhost', $databaseConfig['host']); + $this->assertEquals(3306, $databaseConfig['port']); + } + + // Edge cases and error conditions + public function testGetNonExistentConfigReturnsNull() + { + $this->assertNull($this->config->get('nonexistent.key')); + } + + public function testGetNonExistentConfigReturnsDefaultValue() + { + $defaultValue = 'default_value'; + $this->assertEquals($defaultValue, $this->config->get('nonexistent.key', $defaultValue)); + } + + public function testGetWithEmptyKey() + { + $this->assertNull($this->config->get('')); + $this->assertNull($this->config->get(null)); + } + + public function testSetWithEmptyKey() + { + $this->expectException(InvalidArgumentException::class); + $this->config->set('', 'value'); + } + + public function testSetWithNullKey() + { + $this->expectException(InvalidArgumentException::class); + $this->config->set(null, 'value'); + } + + public function testOverwriteExistingConfigValue() + { + $this->config->set('test.key', 'original_value'); + $this->assertEquals('original_value', $this->config->get('test.key')); + + $this->config->set('test.key', 'new_value'); + $this->assertEquals('new_value', $this->config->get('test.key')); + } + + public function testSetComplexDataTypes() + { + $arrayValue = ['item1', 'item2', 'item3']; + $objectValue = new stdClass(); + $objectValue->property = 'value'; + + $this->config->set('test.array', $arrayValue); + $this->config->set('test.object', $objectValue); + + $this->assertEquals($arrayValue, $this->config->get('test.array')); + $this->assertEquals($objectValue, $this->config->get('test.object')); + } + + public function testRemoveNonExistentKey() + { + // Should not throw an exception + $this->config->remove('nonexistent.key'); + $this->assertFalse($this->config->has('nonexistent.key')); + } + + public function testGetSectionNonExistent() + { + $result = $this->config->getSection('nonexistent'); + $this->assertNull($result); + } + + public function testClearAllConfig() + { + $this->config->setMultiple($this->testConfigData); + $this->assertTrue($this->config->has('database.host')); + + $this->config->clear(); + $this->assertFalse($this->config->has('database.host')); + $this->assertEmpty($this->config->getAll()); + } + + // File operations tests + public function testLoadConfigFromFile() + { + $configContent = "[database]\nhost = localhost\nport = 3306\n\n[cache]\nenabled = true\nttl = 3600"; + file_put_contents($this->testConfigFile, $configContent); + + $this->config->loadFromFile($this->testConfigFile); + + $this->assertEquals('localhost', $this->config->get('database.host')); + $this->assertEquals('3306', $this->config->get('database.port')); + $this->assertEquals('true', $this->config->get('cache.enabled')); + $this->assertEquals('3600', $this->config->get('cache.ttl')); + } + + public function testLoadConfigFromNonExistentFile() + { + $this->expectException(InvalidArgumentException::class); + $this->config->loadFromFile('/nonexistent/path/config.ini'); + } + + public function testSaveConfigToFile() + { + $this->config->setMultiple($this->testConfigData); + $this->config->saveToFile($this->testConfigFile); + + $this->assertFileExists($this->testConfigFile); + $content = file_get_contents($this->testConfigFile); + $this->assertStringContains('localhost', $content); + $this->assertStringContains('3306', $content); + } + + public function testSaveConfigToInvalidPath() + { + $this->expectException(RuntimeException::class); + $this->config->saveToFile('/invalid/path/config.ini'); + } + + // Type casting and validation tests + public function testGetBooleanValue() + { + $this->config->set('test.bool_true', true); + $this->config->set('test.bool_false', false); + $this->config->set('test.bool_string_true', 'true'); + $this->config->set('test.bool_string_false', 'false'); + + $this->assertTrue($this->config->getBool('test.bool_true')); + $this->assertFalse($this->config->getBool('test.bool_false')); + $this->assertTrue($this->config->getBool('test.bool_string_true')); + $this->assertFalse($this->config->getBool('test.bool_string_false')); + } + + public function testGetIntegerValue() + { + $this->config->set('test.int', 42); + $this->config->set('test.int_string', '123'); + + $this->assertEquals(42, $this->config->getInt('test.int')); + $this->assertEquals(123, $this->config->getInt('test.int_string')); + } + + public function testGetFloatValue() + { + $this->config->set('test.float', 3.14); + $this->config->set('test.float_string', '2.718'); + + $this->assertEquals(3.14, $this->config->getFloat('test.float')); + $this->assertEquals(2.718, $this->config->getFloat('test.float_string')); + } + + public function testGetArrayValue() + { + $arrayValue = ['a', 'b', 'c']; + $this->config->set('test.array', $arrayValue); + + $this->assertEquals($arrayValue, $this->config->getArray('test.array')); + } + + // Environment variable integration tests + public function testGetFromEnvironmentVariable() + { + $_ENV['TEST_CONFIG_VALUE'] = 'env_value'; + $this->config->set('test.env', '${TEST_CONFIG_VALUE}'); + + $result = $this->config->get('test.env', null, true); // true for env expansion + $this->assertEquals('env_value', $result); + + unset($_ENV['TEST_CONFIG_VALUE']); + } + + public function testGetFromEnvironmentVariableWithDefault() + { + $this->config->set('test.env', '${NONEXISTENT_VAR:default_value}'); + + $result = $this->config->get('test.env', null, true); + $this->assertEquals('default_value', $result); + } + + // Merge and extend functionality tests + public function testMergeConfigs() + { + $this->config->setMultiple([ + 'database' => ['host' => 'localhost', 'port' => 3306], + 'cache' => ['enabled' => true] + ]); + + $additionalConfig = [ + 'database' => ['port' => 5432, 'ssl' => true], + 'logging' => ['level' => 'debug'] + ]; + + $this->config->merge($additionalConfig); + + $this->assertEquals('localhost', $this->config->get('database.host')); + $this->assertEquals(5432, $this->config->get('database.port')); // Should be overwritten + $this->assertTrue($this->config->get('database.ssl')); // Should be added + $this->assertTrue($this->config->get('cache.enabled')); // Should remain + $this->assertEquals('debug', $this->config->get('logging.level')); // Should be added + } + + // Performance and memory tests + public function testHandleLargeConfigData() + { + $largeData = []; + for ($i = 0; $i < 1000; $i++) { + $largeData["key_$i"] = "value_$i"; + } + + $this->config->setMultiple(['large_section' => $largeData]); + + $this->assertEquals('value_500', $this->config->get('large_section.key_500')); + $this->assertEquals(1000, count($this->config->getSection('large_section'))); + } + + public function testConfigImmutability() + { + $originalData = ['immutable' => ['key' => 'value']]; + $this->config->setMultiple($originalData); + + $retrievedData = $this->config->getSection('immutable'); + $retrievedData['key'] = 'modified_value'; + + // Original config should remain unchanged + $this->assertEquals('value', $this->config->get('immutable.key')); + } + + // Validation and sanitization tests + public function testConfigKeyValidation() + { + $invalidKeys = ['', null, 123, [], new stdClass()]; + + foreach ($invalidKeys as $key) { + try { + $this->config->set($key, 'value'); + $this->fail('Expected InvalidArgumentException for invalid key: ' . var_export($key, true)); + } catch (InvalidArgumentException $e) { + $this->assertTrue(true); // Expected exception + } + } + } + + public function testNestedKeyDepthLimit() + { + // Test very deep nesting + $deepKey = implode('.', array_fill(0, 20, 'level')); + $this->config->set($deepKey, 'deep_value'); + + $this->assertEquals('deep_value', $this->config->get($deepKey)); + } + + // Thread safety and concurrent access tests + public function testConcurrentAccess() + { + $this->config->set('concurrent.test', 'initial_value'); + + // Simulate concurrent read/write operations + $processes = []; + for ($i = 0; $i < 5; $i++) { + $this->config->set("concurrent.key_$i", "value_$i"); + $this->assertEquals("value_$i", $this->config->get("concurrent.key_$i")); + } + + $this->assertEquals('initial_value', $this->config->get('concurrent.test')); + } + + // Serialization tests + public function testConfigSerialization() + { + $this->config->setMultiple($this->testConfigData); + + $serialized = serialize($this->config); + $unserialized = unserialize($serialized); + + $this->assertEquals($this->config->get('database.host'), $unserialized->get('database.host')); + $this->assertEquals($this->config->getAll(), $unserialized->getAll()); + } + + // Configuration validation tests + public function testRequiredConfigValidation() + { + $requiredKeys = ['database.host', 'database.port', 'app.name']; + + $this->config->setMultiple($this->testConfigData); + + foreach ($requiredKeys as $key) { + $this->assertTrue($this->config->has($key), "Required key '$key' is missing"); + } + } + + public function testConfigValueConstraints() + { + $this->config->set('database.port', 3306); + $port = $this->config->getInt('database.port'); + + $this->assertGreaterThan(0, $port); + $this->assertLessThanOrEqual(65535, $port); + } +} \ No newline at end of file diff --git a/tests/library/includes/PageHeaderTest.php b/tests/library/includes/PageHeaderTest.php new file mode 100644 index 000000000..f3485981e --- /dev/null +++ b/tests/library/includes/PageHeaderTest.php @@ -0,0 +1,548 @@ +mockRequest = $this->createMock(Request::class); + $this->mockResponse = $this->createMock(Response::class); + $this->mockConfig = $this->createMock(Config::class); + + // Initialize PageHeader with mocked dependencies + $this->pageHeader = new PageHeader($this->mockRequest, $this->mockResponse, $this->mockConfig); + } + + protected function tearDown(): void + { + $this->pageHeader = null; + $this->mockRequest = null; + $this->mockResponse = null; + $this->mockConfig = null; + parent::tearDown(); + } + + /** + * Test basic header generation with default settings + */ + public function testGenerateHeaderWithDefaults() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_title', 'Test Site'], + ['site_description', 'Test Description'], + ['theme', 'default'] + ]); + + $header = $this->pageHeader->generateHeader(); + + $this->assertNotEmpty($header); + $this->assertStringContainsString('Test Site', $header); + $this->assertStringContainsString('Test Description', $header); + $this->assertStringContainsString('', $header); + } + + /** + * Test header generation with custom title + */ + public function testGenerateHeaderWithCustomTitle() + { + $customTitle = 'Custom Page Title'; + + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_title', 'Test Site'], + ['site_description', 'Test Description'], + ['theme', 'default'] + ]); + + $header = $this->pageHeader->generateHeader($customTitle); + + $this->assertStringContainsString($customTitle, $header); + $this->assertStringContainsString('Test Site', $header); + } + + /** + * Test header generation with empty title + */ + public function testGenerateHeaderWithEmptyTitle() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_title', 'Test Site'], + ['site_description', 'Test Description'], + ['theme', 'default'] + ]); + + $header = $this->pageHeader->generateHeader(''); + + $this->assertStringContainsString('Test Site', $header); + $this->assertStringNotContainsString(' | ', $header); + } + + /** + * Test header generation with null title + */ + public function testGenerateHeaderWithNullTitle() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_title', 'Test Site'], + ['site_description', 'Test Description'], + ['theme', 'default'] + ]); + + $header = $this->pageHeader->generateHeader(null); + + $this->assertStringContainsString('Test Site', $header); + $this->assertNotEmpty($header); + } + + /** + * Test meta tag generation + */ + public function testGenerateMetaTags() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_description', 'Test Description'], + ['site_keywords', 'test,keywords,site'], + ['site_author', 'Test Author'] + ]); + + $metaTags = $this->pageHeader->generateMetaTags(); + + $this->assertStringContainsString('name="description"', $metaTags); + $this->assertStringContainsString('Test Description', $metaTags); + $this->assertStringContainsString('name="keywords"', $metaTags); + $this->assertStringContainsString('test,keywords,site', $metaTags); + $this->assertStringContainsString('name="author"', $metaTags); + $this->assertStringContainsString('Test Author', $metaTags); + } + + /** + * Test meta tag generation with missing configuration + */ + public function testGenerateMetaTagsWithMissingConfig() + { + $this->mockConfig->method('get') + ->willReturn(null); + + $metaTags = $this->pageHeader->generateMetaTags(); + + $this->assertStringContainsString('name="viewport"', $metaTags); + $this->assertStringContainsString('charset=', $metaTags); + } + + /** + * Test CSS link generation + */ + public function testGenerateCssLinks() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['theme', 'custom'], + ['css_files', ['main.css', 'theme.css']], + ['base_url', 'https://example.com'] + ]); + + $cssLinks = $this->pageHeader->generateCssLinks(); + + $this->assertStringContainsString('rel="stylesheet"', $cssLinks); + $this->assertStringContainsString('main.css', $cssLinks); + $this->assertStringContainsString('theme.css', $cssLinks); + $this->assertStringContainsString('https://example.com', $cssLinks); + } + + /** + * Test CSS link generation with empty CSS files + */ + public function testGenerateCssLinksWithEmptyFiles() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['theme', 'default'], + ['css_files', []], + ['base_url', 'https://example.com'] + ]); + + $cssLinks = $this->pageHeader->generateCssLinks(); + + $this->assertStringContainsString('rel="stylesheet"', $cssLinks); + $this->assertStringContainsString('default', $cssLinks); + } + + /** + * Test JavaScript link generation + */ + public function testGenerateJsLinks() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['js_files', ['main.js', 'utils.js']], + ['base_url', 'https://example.com'] + ]); + + $jsLinks = $this->pageHeader->generateJsLinks(); + + $this->assertStringContainsString('src=', $jsLinks); + $this->assertStringContainsString('main.js', $jsLinks); + $this->assertStringContainsString('utils.js', $jsLinks); + $this->assertStringContainsString('https://example.com', $jsLinks); + } + + /** + * Test JavaScript link generation with no JS files + */ + public function testGenerateJsLinksWithNoFiles() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['js_files', null], + ['base_url', 'https://example.com'] + ]); + + $jsLinks = $this->pageHeader->generateJsLinks(); + + $this->assertEmpty($jsLinks); + } + + /** + * Test canonical URL generation + */ + public function testGenerateCanonicalUrl() + { + $this->mockRequest->method('getUri') + ->willReturn('https://example.com/page'); + + $this->mockConfig->method('get') + ->willReturnMap([ + ['canonical_url', true], + ['base_url', 'https://example.com'] + ]); + + $canonical = $this->pageHeader->generateCanonicalUrl(); + + $this->assertStringContainsString('rel="canonical"', $canonical); + $this->assertStringContainsString('https://example.com/page', $canonical); + } + + /** + * Test canonical URL generation when disabled + */ + public function testGenerateCanonicalUrlWhenDisabled() + { + $this->mockConfig->method('get') + ->willReturn(false); + + $canonical = $this->pageHeader->generateCanonicalUrl(); + + $this->assertEmpty($canonical); + } + + /** + * Test Open Graph meta tag generation + */ + public function testGenerateOpenGraphTags() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['og_title', 'Test OG Title'], + ['og_description', 'Test OG Description'], + ['og_image', 'https://example.com/image.jpg'], + ['og_url', 'https://example.com'], + ['og_type', 'website'] + ]); + + $ogTags = $this->pageHeader->generateOpenGraphTags(); + + $this->assertStringContainsString('property="og:title"', $ogTags); + $this->assertStringContainsString('Test OG Title', $ogTags); + $this->assertStringContainsString('property="og:description"', $ogTags); + $this->assertStringContainsString('Test OG Description', $ogTags); + $this->assertStringContainsString('property="og:image"', $ogTags); + $this->assertStringContainsString('https://example.com/image.jpg', $ogTags); + } + + /** + * Test Twitter Card meta tag generation + */ + public function testGenerateTwitterCardTags() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['twitter_card', 'summary'], + ['twitter_site', '@testsite'], + ['twitter_creator', '@testcreator'], + ['twitter_title', 'Test Twitter Title'], + ['twitter_description', 'Test Twitter Description'], + ['twitter_image', 'https://example.com/twitter-image.jpg'] + ]); + + $twitterTags = $this->pageHeader->generateTwitterCardTags(); + + $this->assertStringContainsString('name="twitter:card"', $twitterTags); + $this->assertStringContainsString('summary', $twitterTags); + $this->assertStringContainsString('name="twitter:site"', $twitterTags); + $this->assertStringContainsString('@testsite', $twitterTags); + $this->assertStringContainsString('name="twitter:creator"', $twitterTags); + $this->assertStringContainsString('@testcreator', $twitterTags); + } + + /** + * Test favicon generation + */ + public function testGenerateFavicon() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['favicon', 'favicon.ico'], + ['base_url', 'https://example.com'] + ]); + + $favicon = $this->pageHeader->generateFavicon(); + + $this->assertStringContainsString('rel="icon"', $favicon); + $this->assertStringContainsString('favicon.ico', $favicon); + $this->assertStringContainsString('https://example.com', $favicon); + } + + /** + * Test favicon generation with no favicon configured + */ + public function testGenerateFaviconWithNone() + { + $this->mockConfig->method('get') + ->willReturn(null); + + $favicon = $this->pageHeader->generateFavicon(); + + $this->assertEmpty($favicon); + } + + /** + * Test complete header generation with all components + */ + public function testGenerateCompleteHeader() + { + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_title', 'Complete Test Site'], + ['site_description', 'Complete Test Description'], + ['theme', 'complete'], + ['css_files', ['main.css']], + ['js_files', ['main.js']], + ['canonical_url', true], + ['og_title', 'Test OG Title'], + ['twitter_card', 'summary'], + ['favicon', 'favicon.ico'], + ['base_url', 'https://example.com'] + ]); + + $this->mockRequest->method('getUri') + ->willReturn('https://example.com/complete'); + + $header = $this->pageHeader->generateCompleteHeader('Complete Page'); + + $this->assertStringContainsString('', $header); + $this->assertStringContainsString('Complete Page', $header); + $this->assertStringContainsString('Complete Test Site', $header); + $this->assertStringContainsString('rel="stylesheet"', $header); + $this->assertStringContainsString('main.css', $header); + $this->assertStringContainsString('main.js', $header); + $this->assertStringContainsString('rel="canonical"', $header); + $this->assertStringContainsString('property="og:title"', $header); + $this->assertStringContainsString('name="twitter:card"', $header); + $this->assertStringContainsString('rel="icon"', $header); + } + + /** + * Test header generation with XSS protection + */ + public function testGenerateHeaderWithXssProtection() + { + $maliciousTitle = ''; + + $this->mockConfig->method('get') + ->willReturnMap([ + ['site_title', 'Test Site'], + ['site_description', 'Test Description'], + ['theme', 'default'] + ]); + + $header = $this->pageHeader->generateHeader($maliciousTitle); + + $this->assertStringNotContainsString('', ['admin' => true]); + $this->mockAuth->expects($this->once()) + ->method('getCurrentUser') + ->willReturn($mockUser); + + $this->mockDatabase->expects($this->once()) + ->method('getAdminStats') + ->willReturn(['total_users' => 100]); + + // Act + $result = $this->adminIndexTemplate->render(); + + // Assert + $this->assertIsString($result); + $this->assertStringNotContainsString('', $result); + $this->assertStringContainsString('<script>', $result); // Should be escaped + } + + /** + * Test template caching functionality + */ + public function testRenderWithCaching(): void + { + // Arrange + $mockUser = $this->createMockUser('admin', ['admin' => true]); + $this->mockAuth->expects($this->exactly(2)) + ->method('getCurrentUser') + ->willReturn($mockUser); + + $this->mockDatabase->expects($this->once()) // Should only call once due to caching + ->method('getAdminStats') + ->willReturn(['total_users' => 100]); + + // Act + $result1 = $this->adminIndexTemplate->render(); + $result2 = $this->adminIndexTemplate->render(); + + // Assert + $this->assertEquals($result1, $result2); + } + + /** + * Test template rendering with custom theme + */ + public function testRenderWithCustomTheme(): void + { + // Arrange + $mockUser = $this->createMockUser('admin', ['admin' => true, 'theme' => 'dark']); + $this->mockAuth->expects($this->once()) + ->method('getCurrentUser') + ->willReturn($mockUser); + + $this->mockDatabase->expects($this->once()) + ->method('getAdminStats') + ->willReturn(['total_users' => 100]); + + // Act + $result = $this->adminIndexTemplate->render(); + + // Assert + $this->assertStringContainsString('theme-dark', $result); + } + + /** + * Test template accessibility features + */ + public function testRenderIncludesAccessibilityFeatures(): void + { + // Arrange + $mockUser = $this->createMockUser('admin', ['admin' => true]); + $this->mockAuth->expects($this->once()) + ->method('getCurrentUser') + ->willReturn($mockUser); + + $this->mockDatabase->expects($this->once()) + ->method('getAdminStats') + ->willReturn(['total_users' => 100]); + + // Act + $result = $this->adminIndexTemplate->render(); + + // Assert + $this->assertStringContainsString('aria-label', $result); + $this->assertStringContainsString('role=', $result); + $this->assertStringContainsString('alt=', $result); + } + + /** + * Helper method to create mock user objects + */ + private function createMockUser(string $username, array $attributes = []): MockObject + { + $mockUser = $this->createMock(User::class); + + $mockUser->expects($this->any()) + ->method('getUsername') + ->willReturn($username); + + $mockUser->expects($this->any()) + ->method('hasRole') + ->with('admin') + ->willReturn($attributes['admin'] ?? false); + + $mockUser->expects($this->any()) + ->method('getTheme') + ->willReturn($attributes['theme'] ?? 'default'); + + return $mockUser; + } + + /** + * Test template rendering performance with large datasets + */ + public function testRenderPerformanceWithLargeDataset(): void + { + // Arrange + $mockUser = $this->createMockUser('admin', ['admin' => true]); + $this->mockAuth->expects($this->once()) + ->method('getCurrentUser') + ->willReturn($mockUser); + + $this->mockDatabase->expects($this->once()) + ->method('getAdminStats') + ->willReturn(['total_users' => 1000000]); + + // Act + $startTime = microtime(true); + $result = $this->adminIndexTemplate->render(); + $endTime = microtime(true); + + // Assert + $this->assertIsString($result); + $this->assertLessThan(1.0, $endTime - $startTime); // Should render in under 1 second + } + + /** + * Test template error handling with invalid template file + */ + public function testRenderWithMissingTemplateFile(): void + { + // Arrange + $adminIndexTemplate = new AdminIndexTemplate( + $this->mockRequest, + $this->mockResponse, + $this->mockDatabase, + $this->mockAuth, + '/nonexistent/template.php' // Invalid template path + ); + + $mockUser = $this->createMockUser('admin', ['admin' => true]); + $this->mockAuth->expects($this->once()) + ->method('getCurrentUser') + ->willReturn($mockUser); + + // Act & Assert + $this->expectException(TemplateNotFoundException::class); + $adminIndexTemplate->render(); + } + + /** + * Test template rendering with concurrent access + */ + public function testRenderWithConcurrentAccess(): void + { + // Arrange + $mockUser = $this->createMockUser('admin', ['admin' => true]); + $this->mockAuth->expects($this->exactly(3)) + ->method('getCurrentUser') + ->willReturn($mockUser); + + $this->mockDatabase->expects($this->exactly(3)) + ->method('getAdminStats') + ->willReturn(['total_users' => 100]); + + // Act - Simulate concurrent rendering + $results = []; + for ($i = 0; $i < 3; $i++) { + $results[] = $this->adminIndexTemplate->render(); + } + + // Assert + $this->assertCount(3, $results); + foreach ($results as $result) { + $this->assertIsString($result); + $this->assertStringContainsString('Admin Dashboard', $result); + } + } +} \ No newline at end of file diff --git a/tests/templates/DefaultPageHeaderTemplateTest.php b/tests/templates/DefaultPageHeaderTemplateTest.php new file mode 100644 index 000000000..037c67ee0 --- /dev/null +++ b/tests/templates/DefaultPageHeaderTemplateTest.php @@ -0,0 +1,465 @@ +navigationService = $this->createMock(NavigationService::class); + $this->themeService = $this->createMock(ThemeService::class); + $this->page = $this->createMock(Page::class); + $this->user = $this->createMock(User::class); + + $this->template = new DefaultPageHeaderTemplate( + $this->navigationService, + $this->themeService + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + unset($this->template, $this->navigationService, $this->themeService, $this->page, $this->user); + } + + /** + * @test + */ + public function it_renders_basic_header_with_page_title(): void + { + $this->page->method('getTitle')->willReturn('Test Page'); + $this->page->method('getMetaDescription')->willReturn('Test description'); + $this->themeService->method('getCurrentTheme')->willReturn('default'); + + $result = $this->template->render($this->page); + + $this->assertStringContainsString('Test Page', $result); + $this->assertStringContainsString('', $result); + } + + /** + * @test + */ + public function it_renders_header_with_user_authentication(): void + { + $this->page->method('getTitle')->willReturn('Dashboard'); + $this->user->method('getName')->willReturn('John Doe'); + $this->user->method('getEmail')->willReturn('john@example.com'); + $this->user->method('isAuthenticated')->willReturn(true); + + $result = $this->template->render($this->page, $this->user); + + $this->assertStringContainsString('John Doe', $result); + $this->assertStringContainsString('john@example.com', $result); + } + + /** + * @test + */ + public function it_renders_header_for_anonymous_user(): void + { + $this->page->method('getTitle')->willReturn('Public Page'); + $this->user->method('isAuthenticated')->willReturn(false); + + $result = $this->template->render($this->page, $this->user); + + $this->assertStringContainsString('Login', $result); + $this->assertStringContainsString('Register', $result); + $this->assertStringNotContainsString('Logout', $result); + } + + /** + * @test + */ + public function it_includes_navigation_menu(): void + { + $this->page->method('getTitle')->willReturn('Test Page'); + $this->navigationService->method('getMainMenu')->willReturn([ + ['label' => 'Home', 'url' => '/'], + ['label' => 'About', 'url' => '/about'], + ['label' => 'Contact', 'url' => '/contact'] + ]); + + $result = $this->template->render($this->page); + + $this->assertStringContainsString('Home', $result); + $this->assertStringContainsString('About', $result); + $this->assertStringContainsString('Contact', $result); + } + + /** + * @test + */ + public function it_applies_theme_specific_styling(): void + { + $this->page->method('getTitle')->willReturn('Themed Page'); + $this->themeService->method('getCurrentTheme')->willReturn('dark'); + $this->themeService->method('getThemeStyles')->willReturn([ + 'header-bg' => '#333333', + 'header-text' => '#ffffff' + ]); + + $result = $this->template->render($this->page); + + $this->assertStringContainsString('#333333', $result); + $this->assertStringContainsString('#ffffff', $result); + $this->assertStringContainsString('theme-dark', $result); + } + + /** + * @test + */ + public function it_handles_empty_page_title(): void + { + $this->page->method('getTitle')->willReturn(''); + $this->page->method('getSlug')->willReturn('test-page'); + + $result = $this->template->render($this->page); + + $this->assertStringContainsString('Untitled Page', $result); + } + + /** + * @test + */ + public function it_handles_null_page_title(): void + { + $this->page->method('getTitle')->willReturn(null); + $this->page->method('getSlug')->willReturn('test-page'); + + $result = $this->template->render($this->page); + + $this->assertStringContainsString('Untitled Page', $result); + } + + /** + * @test + */ + public function it_sanitizes_page_title_for_xss(): void + { + $this->page->method('getTitle')->willReturn(''); + + $result = $this->template->render($this->page); + + $this->assertStringNotContainsString('