From: Aleksey Filippov Date: Wed, 24 Dec 2025 09:31:05 +0000 (+0300) Subject: Доработка документации добавление схемы БД X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=12ad947f2160049b1496e2d52a566fd6e3180d7d;p=erp24_rep%2Fyii-erp24%2F.git Доработка документации добавление схемы БД --- diff --git a/.gitignore b/.gitignore index be398223..25f99635 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,4 @@ coordination/orchestration/* claude-flow # Removed Windows wrapper files per user request hive-mind-prompt-*.txt +/.claude/ diff --git a/erp24/tests/_data/kogort_stop_list.php b/erp24/tests/_data/kogort_stop_list.php new file mode 100644 index 00000000..836ea4ba --- /dev/null +++ b/erp24/tests/_data/kogort_stop_list.php @@ -0,0 +1,32 @@ + [ + 'id' => 1, + 'phone' => '79990000001', + 'comment' => 'test', + 'created_at' => '2024-01-01 00:00:00', + 'updated_at' => '2024-01-01 00:00:00', + 'created_by' => 1, + 'updated_by' => 1, + ], + 'stop2' => [ + 'id' => 2, + 'phone' => '79990000002', + 'comment' => 'test', + 'created_at' => '2024-01-01 00:00:00', + 'updated_at' => '2024-01-01 00:00:00', + 'created_by' => 1, + 'updated_by' => 1, + ], +]; + + + + + + + + + + diff --git a/erp24/tests/fixtures/KogortStopListFixture.php b/erp24/tests/fixtures/KogortStopListFixture.php new file mode 100644 index 00000000..f164e5d5 --- /dev/null +++ b/erp24/tests/fixtures/KogortStopListFixture.php @@ -0,0 +1,21 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // amo_inc.php tests + // ============================================================ + + /** + * Test amo_inc.php exists + */ + public function testAmoIncFileExists(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $this->assertFileExists($filePath, 'inc/amo/amo_inc.php must exist'); + } + + /** + * Test amo_inc.php is valid PHP + */ + public function testAmoIncIsValidPhp(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $output = []; + $returnCode = 0; + exec("php -l " . escapeshellarg($filePath) . " 2>&1", $output, $returnCode); + $this->assertEquals(0, $returnCode, 'amo_inc.php must be valid PHP: ' . implode("\n", $output)); + } + + /** + * Test amo_inc.php uses AMO_TOKEN_FILE_INC env + */ + public function testAmoIncUsesTokenFileEnv(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_TOKEN_FILE_INC[\'"]\s*\)/', + $content, + 'amo_inc.php must use getenv(\'AMO_TOKEN_FILE_INC\')' + ); + } + + /** + * Test amo_inc.php uses AMO_SUBDOMAIN env + */ + public function testAmoIncUsesSubdomainEnv(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_SUBDOMAIN[\'"]\s*\)/', + $content, + 'amo_inc.php must use getenv(\'AMO_SUBDOMAIN\')' + ); + } + + /** + * Test amo_inc.php has no hardcoded tokens + */ + public function testAmoIncNoHardcodedTokens(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $content = file_get_contents($filePath); + + // Should not contain hardcoded access_token assignment + $this->assertDoesNotMatchRegularExpression( + '/\$ACCESS_TOKEN\s*=\s*[\'"][a-zA-Z0-9]{20,}[\'"]/', + $content, + 'amo_inc.php must not contain hardcoded ACCESS_TOKEN' + ); + } + + /** + * Test amo_inc.php has PHP doc + */ + public function testAmoIncHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'amo_inc.php must have documentation comment' + ); + } + + /** + * Test amo_inc.php references .env.example + */ + public function testAmoIncReferencesEnvExample(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + '.env.example', + $content, + 'amo_inc.php must reference .env.example' + ); + } + + // ============================================================ + // get_token.php tests + // ============================================================ + + /** + * Test get_token.php exists + */ + public function testGetTokenFileExists(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $this->assertFileExists($filePath, 'inc/amo/get_token.php must exist'); + } + + /** + * Test get_token.php uses AMO_SECRET_PHRASE env + */ + public function testGetTokenUsesSecretPhraseEnv(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_SECRET_PHRASE[\'"]\s*\)/', + $content, + 'get_token.php must use getenv(\'AMO_SECRET_PHRASE\')' + ); + } + + /** + * Test get_token.php uses AMO_SUBDOMAIN env + */ + public function testGetTokenUsesSubdomainEnv(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_SUBDOMAIN[\'"]\s*\)/', + $content, + 'get_token.php must use getenv(\'AMO_SUBDOMAIN\')' + ); + } + + /** + * Test get_token.php uses AMO_APP_URL env + */ + public function testGetTokenUsesAppUrlEnv(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_APP_URL[\'"]\s*\)/', + $content, + 'get_token.php must use getenv(\'AMO_APP_URL\')' + ); + } + + /** + * Test get_token.php uses AMO_CLIENT_ID env + */ + public function testGetTokenUsesClientIdEnv(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_CLIENT_ID[\'"]\s*\)/', + $content, + 'get_token.php must use getenv(\'AMO_CLIENT_ID\')' + ); + } + + /** + * Test get_token.php uses AMO_CLIENT_SECRET env + */ + public function testGetTokenUsesClientSecretEnv(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_CLIENT_SECRET[\'"]\s*\)/', + $content, + 'get_token.php must use getenv(\'AMO_CLIENT_SECRET\')' + ); + } + + /** + * Test get_token.php uses AMO_TOKEN_FILE env + */ + public function testGetTokenUsesTokenFileEnv(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_TOKEN_FILE[\'"]\s*\)/', + $content, + 'get_token.php must use getenv(\'AMO_TOKEN_FILE\')' + ); + } + + /** + * Test get_token.php has no hardcoded client secrets + */ + public function testGetTokenNoHardcodedClientSecret(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + // Should not contain hardcoded CLIENT_SECRET + $this->assertDoesNotMatchRegularExpression( + '/define\s*\(\s*[\'"]CLIENT_SECRET[\'"]\s*,\s*[\'"][a-zA-Z0-9]{10,}[\'"]/', + $content, + 'get_token.php must not contain hardcoded CLIENT_SECRET' + ); + } + + /** + * Test get_token.php has no hardcoded client ID + */ + public function testGetTokenNoHardcodedClientId(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + // Should not contain hardcoded CLIENT_ID (UUID format) + $this->assertDoesNotMatchRegularExpression( + '/define\s*\(\s*[\'"]CLIENT_ID[\'"]\s*,\s*[\'"][a-f0-9-]{36}[\'"]/', + $content, + 'get_token.php must not contain hardcoded CLIENT_ID' + ); + } + + /** + * Test get_token.php has PHP doc + */ + public function testGetTokenHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'get_token.php must have documentation comment' + ); + } + + /** + * Test get_token.php references .env.example + */ + public function testGetTokenReferencesEnvExample(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + '.env.example', + $content, + 'get_token.php must reference .env.example' + ); + } + + /** + * Test get_token.php validates secret_phrase + */ + public function testGetTokenValidatesSecretPhrase(): void + { + $filePath = $this->basePath . '/inc/amo/get_token.php'; + $content = file_get_contents($filePath); + + // Should validate secret_phrase from $_GET + $this->assertMatchesRegularExpression( + '/\$_GET\s*\[\s*[\'"]secret_phrase[\'"]\s*\]/', + $content, + 'get_token.php must validate secret_phrase from GET parameter' + ); + } + + // ============================================================ + // .env.example tests for AmoCRM + // ============================================================ + + /** + * Test .env.example has AMO_SUBDOMAIN + */ + public function testEnvExampleHasAmoSubdomain(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO_SUBDOMAIN=', + $envExample, + '.env.example must contain AMO_SUBDOMAIN' + ); + } + + /** + * Test .env.example has AMO_CLIENT_ID + */ + public function testEnvExampleHasAmoClientId(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO_CLIENT_ID=', + $envExample, + '.env.example must contain AMO_CLIENT_ID' + ); + } + + /** + * Test .env.example has AMO_CLIENT_SECRET + */ + public function testEnvExampleHasAmoClientSecret(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO_CLIENT_SECRET=', + $envExample, + '.env.example must contain AMO_CLIENT_SECRET' + ); + } + + /** + * Test .env.example has AMO_SECRET_PHRASE + */ + public function testEnvExampleHasAmoSecretPhrase(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO_SECRET_PHRASE=', + $envExample, + '.env.example must contain AMO_SECRET_PHRASE' + ); + } +} diff --git a/erp24/tests/unit/config/AmoCrmTokenPathTest.php b/erp24/tests/unit/config/AmoCrmTokenPathTest.php new file mode 100644 index 00000000..1b2640ea --- /dev/null +++ b/erp24/tests/unit/config/AmoCrmTokenPathTest.php @@ -0,0 +1,410 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // inc/amo_inc.php tests + // ============================================================ + + /** + * Test amo_inc.php exists + */ + public function testAmoIncFileExists(): void + { + $filePath = $this->basePath . '/inc/amo_inc.php'; + $this->assertFileExists($filePath, 'inc/amo_inc.php must exist'); + } + + /** + * Test amo_inc.php uses AMO_TOKEN_FILE env + */ + public function testAmoIncUsesTokenFileEnv(): void + { + $filePath = $this->basePath . '/inc/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_TOKEN_FILE[\'"]\s*\)/', + $content, + 'amo_inc.php must use getenv(\'AMO_TOKEN_FILE\')' + ); + } + + /** + * Test amo_inc.php uses AMO_SUBDOMAIN env + */ + public function testAmoIncUsesSubdomainEnv(): void + { + $filePath = $this->basePath . '/inc/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_SUBDOMAIN[\'"]\s*\)/', + $content, + 'amo_inc.php must use getenv(\'AMO_SUBDOMAIN\')' + ); + } + + /** + * Test amo_inc.php has PHP doc + */ + public function testAmoIncHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'amo_inc.php must have documentation comment' + ); + } + + // ============================================================ + // inc/amo/amo_inc.php tests + // ============================================================ + + /** + * Test amo/amo_inc.php exists + */ + public function testAmoAmoIncFileExists(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $this->assertFileExists($filePath, 'inc/amo/amo_inc.php must exist'); + } + + /** + * Test amo/amo_inc.php uses AMO_TOKEN_FILE_INC env + */ + public function testAmoAmoIncUsesTokenFileEnv(): void + { + $filePath = $this->basePath . '/inc/amo/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_TOKEN_FILE_INC[\'"]\s*\)/', + $content, + 'amo/amo_inc.php must use getenv(\'AMO_TOKEN_FILE_INC\')' + ); + } + + // ============================================================ + // inc/amo2/amo_inc.php tests + // ============================================================ + + /** + * Test amo2/amo_inc.php exists + */ + public function testAmo2AmoIncFileExists(): void + { + $filePath = $this->basePath . '/inc/amo2/amo_inc.php'; + $this->assertFileExists($filePath, 'inc/amo2/amo_inc.php must exist'); + } + + /** + * Test amo2/amo_inc.php uses AMO2_TOKEN_FILE env + */ + public function testAmo2AmoIncUsesTokenFileEnv(): void + { + $filePath = $this->basePath . '/inc/amo2/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO2_TOKEN_FILE[\'"]\s*\)/', + $content, + 'amo2/amo_inc.php must use getenv(\'AMO2_TOKEN_FILE\')' + ); + } + + /** + * Test amo2/amo_inc.php has PHP doc + */ + public function testAmo2AmoIncHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/amo2/amo_inc.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'amo2/amo_inc.php must have documentation comment' + ); + } + + // ============================================================ + // inc/amo2/get_token.php tests + // ============================================================ + + /** + * Test amo2/get_token.php exists + */ + public function testAmo2GetTokenFileExists(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $this->assertFileExists($filePath, 'inc/amo2/get_token.php must exist'); + } + + /** + * Test amo2/get_token.php uses AMO2_CLIENT_ID env + */ + public function testAmo2GetTokenUsesClientIdEnv(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO2_CLIENT_ID[\'"]\s*\)/', + $content, + 'amo2/get_token.php must use getenv(\'AMO2_CLIENT_ID\')' + ); + } + + /** + * Test amo2/get_token.php uses AMO2_CLIENT_SECRET env + */ + public function testAmo2GetTokenUsesClientSecretEnv(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO2_CLIENT_SECRET[\'"]\s*\)/', + $content, + 'amo2/get_token.php must use getenv(\'AMO2_CLIENT_SECRET\')' + ); + } + + /** + * Test amo2/get_token.php uses AMO2_SECRET_PHRASE env + */ + public function testAmo2GetTokenUsesSecretPhraseEnv(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO2_SECRET_PHRASE[\'"]\s*\)/', + $content, + 'amo2/get_token.php must use getenv(\'AMO2_SECRET_PHRASE\')' + ); + } + + /** + * Test amo2/get_token.php has no hardcoded CLIENT_ID + */ + public function testAmo2GetTokenNoHardcodedClientId(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + // Known leaked CLIENT_ID + $this->assertStringNotContainsString( + 'a6156015-990f-4bbc-8fd1-309d51347407', + $content, + 'amo2/get_token.php must not contain hardcoded CLIENT_ID' + ); + } + + /** + * Test amo2/get_token.php has no hardcoded CLIENT_SECRET + */ + public function testAmo2GetTokenNoHardcodedClientSecret(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + // Known leaked CLIENT_SECRET + $this->assertStringNotContainsString( + 'Z0mo0XadAR44kCEAOw5XvECLA8QbFYeLyHnCM81b9bKC96LjzTDd80cItLZ2wzmO', + $content, + 'amo2/get_token.php must not contain hardcoded CLIENT_SECRET' + ); + } + + /** + * Test amo2/get_token.php has no hardcoded SECRET_PHRASE + */ + public function testAmo2GetTokenNoHardcodedSecretPhrase(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + // Known leaked SECRET_PHRASE + $this->assertStringNotContainsString( + 'VJJVkt467ltuXU__356XEtS', + $content, + 'amo2/get_token.php must not contain hardcoded SECRET_PHRASE' + ); + } + + /** + * Test amo2/get_token.php has PHP doc + */ + public function testAmo2GetTokenHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/amo2/get_token.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'amo2/get_token.php must have documentation comment' + ); + } + + // ============================================================ + // Token file security tests + // ============================================================ + + /** + * Test no token JSON files in repository + */ + public function testNoTokenFilesInRepo(): void + { + $tokenPatterns = [ + $this->basePath . '/inc/token_amo*.json', + $this->basePath . '/inc/amo/token_*.json', + $this->basePath . '/inc/amo2/token_*.json', + $this->basePath . '/inc/amo2/1token_*.json', + ]; + + foreach ($tokenPatterns as $pattern) { + $files = glob($pattern); + // Files may exist locally but should not be committed + // This test verifies they are listed in .gitignore + } + + // Check .gitignore includes token patterns + $gitignore = file_get_contents(dirname($this->basePath) . '/.gitignore'); + + $this->assertStringContainsString( + 'erp24/inc/amo/*.json', + $gitignore, + '.gitignore must exclude erp24/inc/amo/*.json' + ); + + $this->assertStringContainsString( + 'erp24/inc/amo2/*.json', + $gitignore, + '.gitignore must exclude erp24/inc/amo2/*.json' + ); + + $this->assertStringContainsString( + 'erp24/inc/token_amo*.json', + $gitignore, + '.gitignore must exclude erp24/inc/token_amo*.json' + ); + } + + // ============================================================ + // .env.example tests + // ============================================================ + + /** + * Test .env.example has AMO2_CLIENT_ID + */ + public function testEnvExampleHasAmo2ClientId(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO2_CLIENT_ID=', + $envExample, + '.env.example must contain AMO2_CLIENT_ID' + ); + } + + /** + * Test .env.example has AMO2_CLIENT_SECRET + */ + public function testEnvExampleHasAmo2ClientSecret(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO2_CLIENT_SECRET=', + $envExample, + '.env.example must contain AMO2_CLIENT_SECRET' + ); + } + + /** + * Test .env.example has AMO2_TOKEN_FILE + */ + public function testEnvExampleHasAmo2TokenFile(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'AMO2_TOKEN_FILE=', + $envExample, + '.env.example must contain AMO2_TOKEN_FILE' + ); + } + + /** + * Test .env.example token paths point outside repository + */ + public function testEnvExampleTokenPathsOutsideRepo(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + // Token paths should point to /var/www/secrets/ or similar + $this->assertMatchesRegularExpression( + '/AMO_TOKEN_FILE=\/var\/www\/secrets\//', + $envExample, + '.env.example AMO_TOKEN_FILE should point to /var/www/secrets/' + ); + } + + /** + * Test no leaked AmoCRM secrets in .env.example + */ + public function testEnvExampleNoLeakedAmoSecrets(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $leakedSecrets = [ + 'a6156015-990f-4bbc-8fd1-309d51347407', // CLIENT_ID + 'Z0mo0XadAR44kCEAOw5XvECLA8QbFYeLyHnCM81b9bKC96LjzTDd80cItLZ2wzmO', // CLIENT_SECRET + 'VJJVkt467ltuXU__356XEtS', // SECRET_PHRASE + ]; + + foreach ($leakedSecrets as $secret) { + $this->assertStringNotContainsString( + $secret, + $envExample, + ".env.example must not contain leaked secret: " . substr($secret, 0, 20) . "..." + ); + } + } +} diff --git a/erp24/tests/unit/config/BasicAuthSecretsTest.php b/erp24/tests/unit/config/BasicAuthSecretsTest.php new file mode 100644 index 00000000..05576e3c --- /dev/null +++ b/erp24/tests/unit/config/BasicAuthSecretsTest.php @@ -0,0 +1,288 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // florist24/index.php tests + // ============================================================ + + /** + * Test florist24/index.php exists + */ + public function testFlorist24IndexFileExists(): void + { + $filePath = $this->basePath . '/modul/florist24/index.php'; + $this->assertFileExists($filePath, 'modul/florist24/index.php must exist'); + } + + /** + * Test florist24/index.php uses BASIC_AUTH_DEMO2_USER env + */ + public function testFlorist24UsesDemo2UserEnv(): void + { + $filePath = $this->basePath . '/modul/florist24/index.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]BASIC_AUTH_DEMO2_USER[\'"]\s*\)/', + $content, + 'florist24/index.php must use getenv(\'BASIC_AUTH_DEMO2_USER\')' + ); + } + + /** + * Test florist24/index.php uses BASIC_AUTH_DEMO2_PASSWORD env + */ + public function testFlorist24UsesDemo2PasswordEnv(): void + { + $filePath = $this->basePath . '/modul/florist24/index.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]BASIC_AUTH_DEMO2_PASSWORD[\'"]\s*\)/', + $content, + 'florist24/index.php must use getenv(\'BASIC_AUTH_DEMO2_PASSWORD\')' + ); + } + + /** + * Test florist24/index.php has no hardcoded demo2 password + */ + public function testFlorist24NoHardcodedDemo2Password(): void + { + $filePath = $this->basePath . '/modul/florist24/index.php'; + $content = file_get_contents($filePath); + + // Known leaked password + $this->assertStringNotContainsString( + 'aG7hY9kR', + $content, + 'florist24/index.php must not contain hardcoded demo2 password' + ); + } + + /** + * Test florist24/index.php has no hardcoded Basic Auth in URL + */ + public function testFlorist24NoHardcodedBasicAuthInUrl(): void + { + $filePath = $this->basePath . '/modul/florist24/index.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/https:\/\/demo2:[a-zA-Z0-9]+@/', + $content, + 'florist24/index.php must not contain hardcoded Basic Auth in URL' + ); + } + + /** + * Test florist24/index.php has PHP doc + */ + public function testFlorist24HasPhpDoc(): void + { + $filePath = $this->basePath . '/modul/florist24/index.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'florist24/index.php must have documentation comment' + ); + } + + // ============================================================ + // get_orders.php tests + // ============================================================ + + /** + * Test get_orders.php exists + */ + public function testGetOrdersFileExists(): void + { + $filePath = $this->basePath . '/api1_old/orders/get_orders.php'; + $this->assertFileExists($filePath, 'api1_old/orders/get_orders.php must exist'); + } + + /** + * Test get_orders.php uses BASIC_AUTH_KASSEAR_USER env + */ + public function testGetOrdersUsesKassearUserEnv(): void + { + $filePath = $this->basePath . '/api1_old/orders/get_orders.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]BASIC_AUTH_KASSEAR_USER[\'"]\s*\)/', + $content, + 'get_orders.php must use getenv(\'BASIC_AUTH_KASSEAR_USER\')' + ); + } + + /** + * Test get_orders.php uses BASIC_AUTH_KASSEAR_PASSWORD env + */ + public function testGetOrdersUsesKassearPasswordEnv(): void + { + $filePath = $this->basePath . '/api1_old/orders/get_orders.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]BASIC_AUTH_KASSEAR_PASSWORD[\'"]\s*\)/', + $content, + 'get_orders.php must use getenv(\'BASIC_AUTH_KASSEAR_PASSWORD\')' + ); + } + + /** + * Test get_orders.php has no hardcoded kassear password + */ + public function testGetOrdersNoHardcodedKassearPassword(): void + { + $filePath = $this->basePath . '/api1_old/orders/get_orders.php'; + $content = file_get_contents($filePath); + + // Known leaked password + $this->assertStringNotContainsString( + 'mL9mN0pO7l', + $content, + 'get_orders.php must not contain hardcoded kassear password' + ); + } + + /** + * Test get_orders.php has no hardcoded Basic Auth in URL + */ + public function testGetOrdersNoHardcodedBasicAuthInUrl(): void + { + $filePath = $this->basePath . '/api1_old/orders/get_orders.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/https:\/\/kassear:[a-zA-Z0-9]+@/', + $content, + 'get_orders.php must not contain hardcoded Basic Auth in URL' + ); + } + + /** + * Test get_orders.php has PHP doc + */ + public function testGetOrdersHasPhpDoc(): void + { + $filePath = $this->basePath . '/api1_old/orders/get_orders.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'get_orders.php must have documentation comment' + ); + } + + // ============================================================ + // .env.example tests + // ============================================================ + + /** + * Test .env.example has BASIC_AUTH_KASSEAR_USER + */ + public function testEnvExampleHasKassearUser(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'BASIC_AUTH_KASSEAR_USER=', + $envExample, + '.env.example must contain BASIC_AUTH_KASSEAR_USER' + ); + } + + /** + * Test .env.example has BASIC_AUTH_KASSEAR_PASSWORD + */ + public function testEnvExampleHasKassearPassword(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'BASIC_AUTH_KASSEAR_PASSWORD=', + $envExample, + '.env.example must contain BASIC_AUTH_KASSEAR_PASSWORD' + ); + } + + /** + * Test .env.example has BASIC_AUTH_DEMO2_USER + */ + public function testEnvExampleHasDemo2User(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'BASIC_AUTH_DEMO2_USER=', + $envExample, + '.env.example must contain BASIC_AUTH_DEMO2_USER' + ); + } + + /** + * Test .env.example has BASIC_AUTH_DEMO2_PASSWORD + */ + public function testEnvExampleHasDemo2Password(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'BASIC_AUTH_DEMO2_PASSWORD=', + $envExample, + '.env.example must contain BASIC_AUTH_DEMO2_PASSWORD' + ); + } + + /** + * Test no known leaked Basic Auth passwords in .env.example + */ + public function testEnvExampleNoLeakedBasicAuthPasswords(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $leakedPasswords = [ + 'aG7hY9kR', // demo2 password + 'mL9mN0pO7l', // kassear password + ]; + + foreach ($leakedPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $envExample, + ".env.example must not contain leaked password: {$password}" + ); + } + } +} diff --git a/erp24/tests/unit/config/BonusPlusSecretsTest.php b/erp24/tests/unit/config/BonusPlusSecretsTest.php new file mode 100644 index 00000000..1e3c93a0 --- /dev/null +++ b/erp24/tests/unit/config/BonusPlusSecretsTest.php @@ -0,0 +1,135 @@ +filePath = dirname(__DIR__, 3) . '/modul/bonus/bonusplus_api.php'; + } + + /** + * Test bonusplus_api.php file exists + */ + public function testFileExists(): void + { + $this->assertFileExists( + $this->filePath, + 'bonusplus_api.php must exist' + ); + } + + /** + * Test BONUSPLUS_API_KEY uses getenv + */ + public function testApiKeyUsesEnv(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + "getenv('BONUSPLUS_API_KEY')", + $content, + 'bonusplus_api.php must use BONUSPLUS_API_KEY from env' + ); + } + + /** + * Test no hardcoded API key + */ + public function testNoHardcodedApiKey(): void + { + $content = file_get_contents($this->filePath); + + // Check that bonusPlusApiKey is not assigned a literal string (except getenv) + $this->assertDoesNotMatchRegularExpression( + '/\$bonusPlusApiKey\s*=\s*[\'"][a-zA-Z0-9_-]{20,}[\'"];/', + $content, + 'bonusplus_api.php must not have hardcoded API key' + ); + } + + /** + * Test getenv has empty string fallback + */ + public function testGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->filePath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'BONUSPLUS_API_KEY'\s*\)\s*\?:\s*''/", + $content, + 'BONUSPLUS_API_KEY must have empty string fallback' + ); + } + + /** + * Test API key is used in Authorization header with base64 + */ + public function testApiKeyUsedInAuthorizationHeader(): void + { + $content = file_get_contents($this->filePath); + + // Check that bonusPlusApiKey is passed to base64_encode for Authorization + $this->assertStringContainsString( + 'base64_encode($bonusPlusApiKey)', + $content, + 'API key must be base64 encoded for Authorization header' + ); + } + + /** + * Test Authorization header format + */ + public function testAuthorizationHeaderFormat(): void + { + $content = file_get_contents($this->filePath); + + // Check proper Authorization: ApiKey format + $this->assertStringContainsString( + "'Authorization: ApiKey '", + $content, + 'Must use Authorization: ApiKey header format' + ); + } + + /** + * Test PHPDoc documentation exists + */ + public function testPhpDocExists(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + + $this->assertStringContainsString( + 'BONUSPLUS_API_KEY', + $content, + 'PHPDoc must mention BONUSPLUS_API_KEY' + ); + } +} diff --git a/erp24/tests/unit/config/CloudPaymentsSecretsTest.php b/erp24/tests/unit/config/CloudPaymentsSecretsTest.php new file mode 100644 index 00000000..f20da000 --- /dev/null +++ b/erp24/tests/unit/config/CloudPaymentsSecretsTest.php @@ -0,0 +1,289 @@ +incPath = $basePath . '/inc/cloudpayments.php'; + $this->modulApiPath = $basePath . '/modul/api/cloudpayments.php'; + $this->modulCollationPath = $basePath . '/modul/collation/cloudpayments.php'; + } + + /** + * Test inc/cloudpayments.php file exists + */ + public function testIncFileExists(): void + { + $this->assertFileExists( + $this->incPath, + 'inc/cloudpayments.php must exist' + ); + } + + /** + * Test modul/api/cloudpayments.php file exists + */ + public function testModulApiFileExists(): void + { + $this->assertFileExists( + $this->modulApiPath, + 'modul/api/cloudpayments.php must exist' + ); + } + + /** + * Test modul/collation/cloudpayments.php file exists + */ + public function testModulCollationFileExists(): void + { + $this->assertFileExists( + $this->modulCollationPath, + 'modul/collation/cloudpayments.php must exist' + ); + } + + /** + * Test inc/cloudpayments.php is valid PHP syntax + */ + public function testIncIsValidPhp(): void + { + $output = []; + $returnCode = 0; + exec("php -l {$this->incPath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'inc/cloudpayments.php must have valid PHP syntax: ' . implode("\n", $output) + ); + } + + /** + * Test inc/cloudpayments.php uses CLOUDPAYMENTS_PUBLIC_ID from env + */ + public function testIncUsesPublicIdEnv(): void + { + $content = file_get_contents($this->incPath); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_PUBLIC_ID')", + $content, + 'inc/cloudpayments.php must use CLOUDPAYMENTS_PUBLIC_ID from env' + ); + } + + /** + * Test inc/cloudpayments.php uses CLOUDPAYMENTS_SECRET from env + */ + public function testIncUsesSecretEnv(): void + { + $content = file_get_contents($this->incPath); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_SECRET')", + $content, + 'inc/cloudpayments.php must use CLOUDPAYMENTS_SECRET from env' + ); + } + + /** + * Test inc/cloudpayments.php uses CLOUDPAYMENTS_REGION_PUBLIC_ID from env + */ + public function testIncUsesRegionPublicIdEnv(): void + { + $content = file_get_contents($this->incPath); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_REGION_PUBLIC_ID')", + $content, + 'inc/cloudpayments.php must use CLOUDPAYMENTS_REGION_PUBLIC_ID from env' + ); + } + + /** + * Test inc/cloudpayments.php uses CLOUDPAYMENTS_REGION_SECRET from env + */ + public function testIncUsesRegionSecretEnv(): void + { + $content = file_get_contents($this->incPath); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_REGION_SECRET')", + $content, + 'inc/cloudpayments.php must use CLOUDPAYMENTS_REGION_SECRET from env' + ); + } + + /** + * Test inc/cloudpayments.php has no hardcoded public ID + */ + public function testIncNoHardcodedPublicId(): void + { + $content = file_get_contents($this->incPath); + + // Pattern: pk_XXXX CloudPayments public key + $this->assertDoesNotMatchRegularExpression( + '/pk_[a-f0-9]{30,}/', + $content, + 'inc/cloudpayments.php must not contain hardcoded public ID' + ); + } + + /** + * Test modul/api/cloudpayments.php uses getenv + */ + public function testModulApiUsesGetenv(): void + { + $content = file_get_contents($this->modulApiPath); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_PUBLIC_ID')", + $content, + 'modul/api/cloudpayments.php must use CLOUDPAYMENTS_PUBLIC_ID from env' + ); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_SECRET')", + $content, + 'modul/api/cloudpayments.php must use CLOUDPAYMENTS_SECRET from env' + ); + } + + /** + * Test modul/api/cloudpayments.php has no hardcoded public ID in JS + */ + public function testModulApiNoHardcodedPublicIdInJs(): void + { + $content = file_get_contents($this->modulApiPath); + + // Check that publicId in JS widget doesn't have hardcoded value + // Old pattern: publicId: 'pk_XXXX', + $this->assertDoesNotMatchRegularExpression( + "/publicId:\s*'pk_[a-f0-9]{30,}'/", + $content, + 'modul/api/cloudpayments.php must not have hardcoded publicId in JS widget' + ); + } + + /** + * Test modul/collation/cloudpayments.php uses getenv + */ + public function testModulCollationUsesGetenv(): void + { + $content = file_get_contents($this->modulCollationPath); + + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_PUBLIC_ID')", + $content, + 'modul/collation/cloudpayments.php must use CLOUDPAYMENTS_PUBLIC_ID from env' + ); + } + + /** + * Test modul/collation/cloudpayments.php uses PHP variable in JS + */ + public function testModulCollationUsesPhpVarInJs(): void + { + $content = file_get_contents($this->modulCollationPath); + + // Check that publicId uses PHP variable with htmlspecialchars + $this->assertStringContainsString( + 'htmlspecialchars($cloudPaymentsPublicId)', + $content, + 'modul/collation/cloudpayments.php must use PHP variable with htmlspecialchars in JS' + ); + } + + /** + * Test all files have empty string fallback + */ + public function testGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->incPath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'CLOUDPAYMENTS_PUBLIC_ID'\s*\)\s*\?:\s*''/", + $content, + 'CLOUDPAYMENTS_PUBLIC_ID must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'CLOUDPAYMENTS_SECRET'\s*\)\s*\?:\s*''/", + $content, + 'CLOUDPAYMENTS_SECRET must have empty string fallback' + ); + } + + /** + * Test PHPDoc documentation exists in all files + */ + public function testPhpDocExists(): void + { + $incContent = file_get_contents($this->incPath); + $apiContent = file_get_contents($this->modulApiPath); + $collationContent = file_get_contents($this->modulCollationPath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $incContent, + 'PHPDoc must exist in inc/cloudpayments.php' + ); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $apiContent, + 'PHPDoc must exist in modul/api/cloudpayments.php' + ); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $collationContent, + 'PHPDoc must exist in modul/collation/cloudpayments.php' + ); + } + + /** + * Test region credentials support in inc/cloudpayments.php + */ + public function testRegionCredentialsSupport(): void + { + $content = file_get_contents($this->incPath); + + // Check that region condition exists + $this->assertStringContainsString( + '$setka=="region"', + $content, + 'inc/cloudpayments.php must support region credential switching' + ); + } +} diff --git a/erp24/tests/unit/config/ConfigIncSecretsTest.php b/erp24/tests/unit/config/ConfigIncSecretsTest.php new file mode 100644 index 00000000..f9186384 --- /dev/null +++ b/erp24/tests/unit/config/ConfigIncSecretsTest.php @@ -0,0 +1,473 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // config.inc.php tests + // ============================================================ + + /** + * Test config.inc.php exists + */ + public function testConfigIncFileExists(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $this->assertFileExists($filePath, 'config.inc.php must exist'); + } + + /** + * Test config.inc.php uses MYSQL_HOST env + */ + public function testConfigIncUsesMysqlHostEnv(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_HOST[\'"]\s*\)/', + $content, + 'config.inc.php must use getenv(\'MYSQL_HOST\')' + ); + } + + /** + * Test config.inc.php uses MYSQL_PASSWORD env + */ + public function testConfigIncUsesMysqlPasswordEnv(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_PASSWORD[\'"]\s*\)/', + $content, + 'config.inc.php must use getenv(\'MYSQL_PASSWORD\')' + ); + } + + /** + * Test config.inc.php uses MYSQL_CRM_PASSWORD env + */ + public function testConfigIncUsesMysqlCrmPasswordEnv(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_CRM_PASSWORD[\'"]\s*\)/', + $content, + 'config.inc.php must use getenv(\'MYSQL_CRM_PASSWORD\')' + ); + } + + /** + * Test config.inc.php uses MYSQL_COUNTER_PASSWORD env + */ + public function testConfigIncUsesMysqlCounterPasswordEnv(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_COUNTER_PASSWORD[\'"]\s*\)/', + $content, + 'config.inc.php must use getenv(\'MYSQL_COUNTER_PASSWORD\')' + ); + } + + /** + * Test config.inc.php has no hardcoded MySQL password + */ + public function testConfigIncNoHardcodedMysqlPassword(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + // Known leaked password + $this->assertStringNotContainsString( + 'JVJruro_Xdg456o3ir', + $content, + 'config.inc.php must not contain hardcoded MySQL password' + ); + } + + /** + * Test config.inc.php has no hardcoded counter password + */ + public function testConfigIncNoHardcodedCounterPassword(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + // Known leaked password + $this->assertStringNotContainsString( + '{W~UtN8b}Mm3', + $content, + 'config.inc.php must not contain hardcoded counter password' + ); + } + + /** + * Test config.inc.php has no hardcoded pass_dell_shop + */ + public function testConfigIncNoHardcodedPassDellShop(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/\[.pass_dell_shop.\]\s*=\s*["\']Olidoell341["\']/', + $content, + 'config.inc.php must not contain hardcoded pass_dell_shop' + ); + } + + /** + * Test config.inc.php uses SHOP_DELETE_PASSWORD env + */ + public function testConfigIncUsesShopDeletePasswordEnv(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]SHOP_DELETE_PASSWORD[\'"]\s*\)/', + $content, + 'config.inc.php must use getenv(\'SHOP_DELETE_PASSWORD\')' + ); + } + + /** + * Test config.inc.php has PHP doc + */ + public function testConfigIncHasPhpDoc(): void + { + $filePath = $this->basePath . '/config.inc.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'config.inc.php must have documentation comment' + ); + } + + // ============================================================ + // db_bz24.php tests + // ============================================================ + + /** + * Test db_bz24.php exists + */ + public function testDbBz24FileExists(): void + { + $filePath = $this->basePath . '/inc/db_bz24.php'; + $this->assertFileExists($filePath, 'inc/db_bz24.php must exist'); + } + + /** + * Test db_bz24.php uses MYSQL_BZ24_PASSWORD env + */ + public function testDbBz24UsesMysqlBz24PasswordEnv(): void + { + $filePath = $this->basePath . '/inc/db_bz24.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_BZ24_PASSWORD[\'"]\s*\)/', + $content, + 'db_bz24.php must use getenv(\'MYSQL_BZ24_PASSWORD\')' + ); + } + + /** + * Test db_bz24.php has no hardcoded password + */ + public function testDbBz24NoHardcodedPassword(): void + { + $filePath = $this->basePath . '/inc/db_bz24.php'; + $content = file_get_contents($filePath); + + $this->assertStringNotContainsString( + 'JVJruro_Xdg456o3ir', + $content, + 'db_bz24.php must not contain hardcoded password' + ); + } + + /** + * Test db_bz24.php has conditional DB init + */ + public function testDbBz24HasConditionalInit(): void + { + $filePath = $this->basePath . '/inc/db_bz24.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/if\s*\(\s*!empty\s*\(\s*\$config\[.DB_USER.\]/', + $content, + 'db_bz24.php must check for credentials before init' + ); + } + + /** + * Test db_bz24.php has PHP doc + */ + public function testDbBz24HasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/db_bz24.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'db_bz24.php must have documentation comment' + ); + } + + // ============================================================ + // salebot_import_from_google.php tests + // ============================================================ + + /** + * Test salebot_import_from_google.php exists + */ + public function testSalebotImportFileExists(): void + { + $filePath = $this->basePath . '/api1_old/cron/salebot_import_from_google.php'; + $this->assertFileExists($filePath, 'api1_old/cron/salebot_import_from_google.php must exist'); + } + + /** + * Test salebot_import uses SALEBOT_IMPORT_TOKEN env + */ + public function testSalebotImportUsesTokenEnv(): void + { + $filePath = $this->basePath . '/api1_old/cron/salebot_import_from_google.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]SALEBOT_IMPORT_TOKEN[\'"]\s*\)/', + $content, + 'salebot_import_from_google.php must use getenv(\'SALEBOT_IMPORT_TOKEN\')' + ); + } + + /** + * Test salebot_import has no hardcoded token + */ + public function testSalebotImportNoHardcodedToken(): void + { + $filePath = $this->basePath . '/api1_old/cron/salebot_import_from_google.php'; + $content = file_get_contents($filePath); + + $this->assertStringNotContainsString( + '1CjgpXfgkh1pXV3KR2H57G3VtHCffrp154up1t36', + $content, + 'salebot_import_from_google.php must not contain hardcoded token' + ); + } + + /** + * Test salebot_import has PHP doc + */ + public function testSalebotImportHasPhpDoc(): void + { + $filePath = $this->basePath . '/api1_old/cron/salebot_import_from_google.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'salebot_import_from_google.php must have documentation comment' + ); + } + + // ============================================================ + // bonus-users-sale-update.php tests + // ============================================================ + + /** + * Test bonus-users-sale-update.php exists + */ + public function testBonusUsersUpdateFileExists(): void + { + $filePath = $this->basePath . '/api1/views/cron/bonus-users-sale-update.php'; + $this->assertFileExists($filePath, 'api1/views/cron/bonus-users-sale-update.php must exist'); + } + + /** + * Test bonus-users-sale-update uses TOKEN_CLOUD env + */ + public function testBonusUsersUpdateUsesTokenCloudEnv(): void + { + $filePath = $this->basePath . '/api1/views/cron/bonus-users-sale-update.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TOKEN_CLOUD[\'"]\s*\)/', + $content, + 'bonus-users-sale-update.php must use getenv(\'TOKEN_CLOUD\')' + ); + } + + /** + * Test bonus-users-sale-update has no hardcoded token + */ + public function testBonusUsersUpdateNoHardcodedToken(): void + { + $filePath = $this->basePath . '/api1/views/cron/bonus-users-sale-update.php'; + $content = file_get_contents($filePath); + + $this->assertStringNotContainsString( + 'iC04295J9HyD2H3GJF3btky', + $content, + 'bonus-users-sale-update.php must not contain hardcoded token' + ); + } + + /** + * Test bonus-users-sale-update has PHP doc + */ + public function testBonusUsersUpdateHasPhpDoc(): void + { + $filePath = $this->basePath . '/api1/views/cron/bonus-users-sale-update.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'bonus-users-sale-update.php must have documentation comment' + ); + } + + // ============================================================ + // api_text.php tests (documentation) + // ============================================================ + + /** + * Test api_text.php has no hardcoded salebot token in docs + */ + public function testApiTextNoHardcodedSalebotToken(): void + { + $filePath = $this->basePath . '/modul/api/api_text.php'; + $content = file_get_contents($filePath); + + $this->assertStringNotContainsString( + '1CjgpXfgkh1pXV3KR2H57G3VtHCffrp154up1t36', + $content, + 'api_text.php must not contain hardcoded salebot token in documentation' + ); + } + + // ============================================================ + // .env.example tests + // ============================================================ + + /** + * Test .env.example has MYSQL_PASSWORD + */ + public function testEnvExampleHasMysqlPassword(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_PASSWORD=', + $envExample, + '.env.example must contain MYSQL_PASSWORD' + ); + } + + /** + * Test .env.example has MYSQL_COUNTER_PASSWORD + */ + public function testEnvExampleHasMysqlCounterPassword(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_COUNTER_PASSWORD=', + $envExample, + '.env.example must contain MYSQL_COUNTER_PASSWORD' + ); + } + + /** + * Test .env.example has MYSQL_BZ24_PASSWORD + */ + public function testEnvExampleHasMysqlBz24Password(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_BZ24_PASSWORD=', + $envExample, + '.env.example must contain MYSQL_BZ24_PASSWORD' + ); + } + + /** + * Test .env.example has SALEBOT_IMPORT_TOKEN + */ + public function testEnvExampleHasSalebotImportToken(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'SALEBOT_IMPORT_TOKEN=', + $envExample, + '.env.example must contain SALEBOT_IMPORT_TOKEN' + ); + } + + /** + * Test no known leaked passwords in .env.example + */ + public function testEnvExampleNoLeakedPasswords(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $leakedPasswords = [ + 'JVJruro_Xdg456o3ir', + '{W~UtN8b}Mm3', + 'Olidoell341', + '1CjgpXfgkh1pXV3KR2H57G3VtHCffrp154up1t36', + 'iC04295J9HyD2H3GJF3btky', + ]; + + foreach ($leakedPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $envExample, + ".env.example must not contain leaked password: {$password}" + ); + } + } +} diff --git a/erp24/tests/unit/config/CookieValidationKeyConfigTest.php b/erp24/tests/unit/config/CookieValidationKeyConfigTest.php new file mode 100644 index 00000000..c76827c3 --- /dev/null +++ b/erp24/tests/unit/config/CookieValidationKeyConfigTest.php @@ -0,0 +1,145 @@ +markTestSkipped('web.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify cookieValidationKey uses getenv + $this->assertStringContainsString( + "getenv('COOKIE_VALIDATION_KEY')", + $content, + 'web.php must use COOKIE_VALIDATION_KEY from env' + ); + + // Verify no hardcoded key + $this->assertStringNotContainsString( + 'Z0uKu8AtuwGTVD_qX4inPOe1xq3FdWcV', + $content, + 'web.php must not contain hardcoded cookie validation key' + ); + } + + /** + * Test that api2.config.php uses COOKIE_VALIDATION_KEY from env + */ + public function testApi2ConfigUsesCookieKeyFromEnv(): void + { + $configPath = dirname(__DIR__, 3) . '/api2/config/api2.config.php'; + + if (!file_exists($configPath)) { + $this->markTestSkipped('api2.config.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify cookieValidationKey uses getenv with fallback + $this->assertStringContainsString( + "getenv('COOKIE_VALIDATION_KEY_API2')", + $content, + 'api2.config.php must check COOKIE_VALIDATION_KEY_API2' + ); + + // Verify fallback to main COOKIE_VALIDATION_KEY + $this->assertStringContainsString( + "getenv('COOKIE_VALIDATION_KEY')", + $content, + 'api2.config.php must fallback to COOKIE_VALIDATION_KEY' + ); + + // Verify no hardcoded key + $this->assertStringNotContainsString( + 'erp24_DLVFJRBvmttertrrt_key', + $content, + 'api2.config.php must not contain hardcoded cookie validation key' + ); + } + + /** + * Test COOKIE_VALIDATION_KEY environment variable + * + * @group integration + */ + public function testCookieValidationKeyEnvVar(): void + { + $key = getenv('COOKIE_VALIDATION_KEY'); + + if ($key === false) { + $this->markTestSkipped('COOKIE_VALIDATION_KEY env var not set'); + } + + $this->assertNotEmpty($key, 'COOKIE_VALIDATION_KEY must not be empty'); + $this->assertGreaterThan( + 10, + strlen($key), + 'COOKIE_VALIDATION_KEY should be at least 10 characters for security' + ); + } + + /** + * Test that Yii app has cookie validation configured + */ + public function testYiiAppHasCookieValidation(): void + { + if (!\Yii::$app || !\Yii::$app->has('request')) { + $this->markTestSkipped('Yii::$app->request not available'); + } + + $request = \Yii::$app->request; + $cookieKey = $request->cookieValidationKey ?? null; + + $this->assertNotNull($cookieKey, 'Request must have cookieValidationKey'); + $this->assertNotEmpty($cookieKey, 'cookieValidationKey must not be empty'); + } + + /** + * Test cookie validation key is not a placeholder in production + */ + public function testCookieKeyNotPlaceholder(): void + { + $key = getenv('COOKIE_VALIDATION_KEY'); + + if ($key === false) { + $this->markTestSkipped('COOKIE_VALIDATION_KEY env var not set'); + } + + $appEnv = getenv('APP_ENV'); + + // Only check in production + if ($appEnv === 'production') { + $this->assertStringNotContainsString( + 'dev_cookie_key', + $key, + 'Production must not use development placeholder cookie key' + ); + } else { + $this->assertTrue(true, 'Placeholder check skipped in non-production environment'); + } + } +} diff --git a/erp24/tests/unit/config/DatabaseConfigTest.php b/erp24/tests/unit/config/DatabaseConfigTest.php new file mode 100644 index 00000000..7070c4b0 --- /dev/null +++ b/erp24/tests/unit/config/DatabaseConfigTest.php @@ -0,0 +1,199 @@ +has('db')) { + $this->markTestSkipped('Yii::$app->db not initialized in test environment'); + } + + $this->assertNotNull(\Yii::$app->db); + $this->assertInstanceOf(Connection::class, \Yii::$app->db); + } + + /** + * Verifies main database connection + * This is an integration test - requires actual database + * + * @group integration + */ + public function testMainDatabaseConnection(): void + { + try { + $db = \Yii::$app->db; + $db->open(); + $result = $db->createCommand('SELECT 1 as value')->queryOne(); + + $this->assertIsArray($result); + $this->assertEquals(1, $result['value']); + + $db->close(); + } catch (\Exception $e) { + $this->markTestSkipped('Database not available: ' . $e->getMessage()); + } + } + + /** + * Verifies database driver in DSN + */ + public function testDatabaseDriverIsConfigured(): void + { + $dsn = \Yii::$app->db->dsn; + + $isPostgreSQL = str_starts_with($dsn, 'pgsql:'); + $isMySQL = str_starts_with($dsn, 'mysql:'); + + $this->assertTrue( + $isPostgreSQL || $isMySQL, + "DSN must start with 'pgsql:' or 'mysql:', got: $dsn" + ); + } + + /** + * Verifies database charset + */ + public function testDatabaseCharsetIsUtf8(): void + { + $charset = \Yii::$app->db->charset; + $this->assertEquals('utf8', $charset, 'Database charset must be utf8'); + } + + /** + * Verifies DSN is formed from environment variables + * NOTE: Test config may use different DSN than production + */ + public function testDatabaseUsesEnvVariables(): void + { + $dsn = \Yii::$app->db->dsn; + + // Just verify DSN has valid format with host + // We can't check exact host match because test env uses different config + $this->assertMatchesRegularExpression( + '/^(pgsql|mysql):host=/', + $dsn, + 'DSN must have valid format with host' + ); + + // Only check env var match if full .env is loaded (COOKIE_VALIDATION_KEY is Dotenv-only) + if (getenv('COOKIE_VALIDATION_KEY') !== false) { + $expectedHost = getenv('POSTGRES_HOSTNAME') ?: getenv('DB_HOST'); + if ($expectedHost) { + $this->assertStringContainsString( + $expectedHost, + $dsn, + 'DSN must contain host from environment variable' + ); + } + } + } + + /** + * Verifies transaction support + * This is an integration test - requires actual database + * + * @group integration + */ + public function testCanExecuteTransaction(): void + { + try { + $db = \Yii::$app->db; + $db->open(); + } catch (\Exception $e) { + $this->markTestSkipped('Database not available: ' . $e->getMessage()); + } + + $transaction = $db->beginTransaction(); + + try { + $db->createCommand('SELECT 1')->execute(); + $transaction->rollBack(); + $this->assertTrue(true); // Transaction created and rolled back successfully + } catch (\Exception $e) { + $transaction->rollBack(); + $this->fail('Transaction error: ' . $e->getMessage()); + } + } + + /** + * Verifies migrations table exists + * This is an integration test - requires actual database + * + * @group integration + */ + public function testMigrationsTableExists(): void + { + try { + $db = \Yii::$app->db; + $db->open(); + $tables = $db->getSchema()->getTableNames(); + $this->assertContains('migration', $tables, 'Table migration must exist'); + } catch (\Exception $e) { + $this->markTestSkipped('Database not available: ' . $e->getMessage()); + } + } + + /** + * Verifies remote database component exists + */ + public function testRemoteDatabaseComponentExists(): void + { + $dbRemote = \Yii::$app->get('dbRemote', false); + + if ($dbRemote === null) { + $this->markTestSkipped('Component dbRemote not configured'); + } + + $this->assertInstanceOf(Connection::class, $dbRemote); + } + + /** + * Verifies remote database uses getenv() + */ + public function testRemoteDatabaseUsesEnvVariables(): void + { + $dbRemote = \Yii::$app->get('dbRemote', false); + + if ($dbRemote === null) { + $this->markTestSkipped('Component dbRemote not configured'); + } + + $expectedHost = getenv('DB_REMOTE_HOST'); + if ($expectedHost) { + $this->assertStringContainsString( + $expectedHost, + $dbRemote->dsn, + 'DSN dbRemote must contain host from DB_REMOTE_HOST' + ); + } + + $expectedUser = getenv('DB_REMOTE_USER'); + if ($expectedUser) { + $this->assertEquals( + $expectedUser, + $dbRemote->username, + 'Username dbRemote must match DB_REMOTE_USER' + ); + } + } +} diff --git a/erp24/tests/unit/config/DbRemoteConfigTest.php b/erp24/tests/unit/config/DbRemoteConfigTest.php new file mode 100644 index 00000000..0c9ed023 --- /dev/null +++ b/erp24/tests/unit/config/DbRemoteConfigTest.php @@ -0,0 +1,341 @@ +markTestSkipped('web.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify conditional unset logic exists + $this->assertStringContainsString( + "if (!getenv('DB_REMOTE_HOST'))", + $content, + 'web.php must have conditional check for DB_REMOTE_HOST' + ); + + $this->assertStringContainsString( + "unset(\$config['components']['dbRemote'])", + $content, + 'web.php must unset dbRemote when DB_REMOTE_HOST is not set' + ); + } + + /** + * Test that console.php has conditional dbRemote loading logic + */ + public function testConsoleConfigHasConditionalDbRemote(): void + { + $configPath = dirname(__DIR__, 3) . '/config/console.php'; + + if (!file_exists($configPath)) { + $this->markTestSkipped('console.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify conditional unset logic exists + $this->assertStringContainsString( + "if (!getenv('DB_REMOTE_HOST'))", + $content, + 'console.php must have conditional check for DB_REMOTE_HOST' + ); + + $this->assertStringContainsString( + "unset(\$config['components']['dbRemote'])", + $content, + 'console.php must unset dbRemote when DB_REMOTE_HOST is not set' + ); + } + + /** + * Test that dev.console.config.php has conditional dbRemote loading logic + */ + public function testDevConsoleConfigHasConditionalDbRemote(): void + { + $configPath = dirname(__DIR__, 3) . '/config/dev.console.config.php'; + + if (!file_exists($configPath)) { + $this->markTestSkipped('dev.console.config.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify conditional unset logic exists + $this->assertStringContainsString( + "if (!getenv('DB_REMOTE_HOST'))", + $content, + 'dev.console.config.php must have conditional check for DB_REMOTE_HOST' + ); + + $this->assertStringContainsString( + "unset(\$config['components']['dbRemote'])", + $content, + 'dev.console.config.php must unset dbRemote when DB_REMOTE_HOST is not set' + ); + } + + /** + * Test that prod.console.config.php has conditional dbRemote loading logic + */ + public function testProdConsoleConfigHasConditionalDbRemote(): void + { + $configPath = dirname(__DIR__, 3) . '/config/prod.console.config.php'; + + if (!file_exists($configPath)) { + $this->markTestSkipped('prod.console.config.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify conditional unset logic exists + $this->assertStringContainsString( + "if (!getenv('DB_REMOTE_HOST'))", + $content, + 'prod.console.config.php must have conditional check for DB_REMOTE_HOST' + ); + + $this->assertStringContainsString( + "unset(\$config['components']['dbRemote'])", + $content, + 'prod.console.config.php must unset dbRemote when DB_REMOTE_HOST is not set' + ); + } + + /** + * Test that dbRemote DSN uses environment variables + */ + public function testDbRemoteDsnUsesEnvVariables(): void + { + $configPath = dirname(__DIR__, 3) . '/config/web.php'; + + if (!file_exists($configPath)) { + $this->markTestSkipped('web.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify DSN uses DB_REMOTE_HOST + $this->assertStringContainsString( + "getenv('DB_REMOTE_HOST')", + $content, + 'dbRemote DSN must use DB_REMOTE_HOST env var' + ); + + // Verify DSN uses DB_REMOTE_PORT + $this->assertStringContainsString( + "getenv('DB_REMOTE_PORT')", + $content, + 'dbRemote DSN must use DB_REMOTE_PORT env var' + ); + + // Verify DSN uses DB_REMOTE_SCHEMA + $this->assertStringContainsString( + "getenv('DB_REMOTE_SCHEMA')", + $content, + 'dbRemote DSN must use DB_REMOTE_SCHEMA env var' + ); + } + + /** + * Test that dbRemote credentials use environment variables + */ + public function testDbRemoteCredentialsUseEnvVariables(): void + { + $configPath = dirname(__DIR__, 3) . '/config/web.php'; + + if (!file_exists($configPath)) { + $this->markTestSkipped('web.php not found'); + } + + $content = file_get_contents($configPath); + + // Verify username uses env var + $this->assertStringContainsString( + "getenv('DB_REMOTE_USER')", + $content, + 'dbRemote username must use DB_REMOTE_USER env var' + ); + + // Verify password uses env var + $this->assertStringContainsString( + "getenv('DB_REMOTE_PASSWORD')", + $content, + 'dbRemote password must use DB_REMOTE_PASSWORD env var' + ); + } + + /** + * Test that hardcoded credentials are removed + */ + public function testNoHardcodedDbRemoteCredentials(): void + { + $configFiles = [ + dirname(__DIR__, 3) . '/config/web.php', + dirname(__DIR__, 3) . '/config/console.php', + dirname(__DIR__, 3) . '/config/dev.console.config.php', + dirname(__DIR__, 3) . '/config/prod.console.config.php', + ]; + + $hardcodedValues = [ + '89.111.174.11', // Old hardcoded IP + 'ERP24_user', // Old hardcoded username + 'HJG6rtrhqaweruit', // Part of old hardcoded password + ]; + + foreach ($configFiles as $configPath) { + if (!file_exists($configPath)) { + continue; + } + + $content = file_get_contents($configPath); + $filename = basename($configPath); + + foreach ($hardcodedValues as $value) { + $this->assertStringNotContainsString( + $value, + $content, + "$filename must not contain hardcoded value: $value" + ); + } + } + } + + /** + * Test dbRemote component behavior in Yii application + */ + public function testDbRemoteComponentBehavior(): void + { + $dbRemoteHost = getenv('DB_REMOTE_HOST'); + + if ($dbRemoteHost === false || empty($dbRemoteHost)) { + // When DB_REMOTE_HOST is not set, dbRemote should not exist + if (\Yii::$app) { + $dbRemote = \Yii::$app->get('dbRemote', false); + $this->assertNull( + $dbRemote, + 'dbRemote component should not exist when DB_REMOTE_HOST is not set' + ); + } else { + $this->markTestSkipped('Yii::$app not available'); + } + } else { + // When DB_REMOTE_HOST is set, dbRemote should exist + if (\Yii::$app) { + $dbRemote = \Yii::$app->get('dbRemote', false); + $this->assertNotNull( + $dbRemote, + 'dbRemote component should exist when DB_REMOTE_HOST is set' + ); + } else { + $this->markTestSkipped('Yii::$app not available'); + } + } + } + + /** + * Test DSN building logic for dbRemote + */ + public function testDbRemoteDsnBuildingLogic(): void + { + $testCases = [ + [ + 'host' => 'remote-db.example.com', + 'port' => '3306', + 'schema' => 'cms', + 'expected_contains' => ['remote-db.example.com', '3306', 'cms'], + ], + [ + 'host' => '192.168.1.100', + 'port' => '3307', + 'schema' => 'remote_db', + 'expected_contains' => ['192.168.1.100', '3307', 'remote_db'], + ], + ]; + + foreach ($testCases as $index => $case) { + $dsn = $this->buildDbRemoteDsn($case['host'], $case['port'], $case['schema']); + + $this->assertStringStartsWith('mysql:host=', $dsn, "Case $index: DSN must start with mysql:host="); + + foreach ($case['expected_contains'] as $expected) { + $this->assertStringContainsString( + $expected, + $dsn, + "Case $index: DSN must contain $expected" + ); + } + } + } + + /** + * Test default port when not specified + */ + public function testDbRemoteDefaultPort(): void + { + $dsn = $this->buildDbRemoteDsn('localhost', '', 'cms'); + + $this->assertStringContainsString( + 'port=3306', + $dsn, + 'Default port should be 3306 when not specified' + ); + } + + /** + * Test default schema when not specified + */ + public function testDbRemoteDefaultSchema(): void + { + $dsn = $this->buildDbRemoteDsn('localhost', '3306', ''); + + $this->assertStringContainsString( + 'dbname=cms', + $dsn, + 'Default schema should be cms when not specified' + ); + } + + /** + * Helper method to build dbRemote DSN string + * Replicates the logic from config files + */ + private function buildDbRemoteDsn(string $host, string $port, string $schema): string + { + return 'mysql:host=' . $host . + ';port=' . ($port ?: '3306') . + ';dbname=' . ($schema ?: 'cms'); + } +} diff --git a/erp24/tests/unit/config/DbTestSecretsTest.php b/erp24/tests/unit/config/DbTestSecretsTest.php new file mode 100644 index 00000000..b506607a --- /dev/null +++ b/erp24/tests/unit/config/DbTestSecretsTest.php @@ -0,0 +1,233 @@ +basePath = dirname(__DIR__, 3); + } + + /** + * Test db-test.php exists + */ + public function testDbTestFileExists(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $this->assertFileExists($filePath, 'inc/db-test.php must exist'); + } + + /** + * Test db-test.php uses MYSQL_TEST_HOST env + */ + public function testDbTestUsesMysqlTestHostEnv(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_TEST_HOST[\'"]\s*\)/', + $content, + 'db-test.php must use getenv(\'MYSQL_TEST_HOST\')' + ); + } + + /** + * Test db-test.php uses MYSQL_TEST_USER env + */ + public function testDbTestUsesMysqlTestUserEnv(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_TEST_USER[\'"]\s*\)/', + $content, + 'db-test.php must use getenv(\'MYSQL_TEST_USER\')' + ); + } + + /** + * Test db-test.php uses MYSQL_TEST_PASSWORD env + */ + public function testDbTestUsesMysqlTestPasswordEnv(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_TEST_PASSWORD[\'"]\s*\)/', + $content, + 'db-test.php must use getenv(\'MYSQL_TEST_PASSWORD\')' + ); + } + + /** + * Test db-test.php uses MYSQL_TEST_DB env + */ + public function testDbTestUsesMysqlTestDbEnv(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]MYSQL_TEST_DB[\'"]\s*\)/', + $content, + 'db-test.php must use getenv(\'MYSQL_TEST_DB\')' + ); + } + + /** + * Test db-test.php has no hardcoded password + */ + public function testDbTestNoHardcodedPassword(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + // Known leaked password + $this->assertStringNotContainsString( + 'yX2hF4mO2omY7x', + $content, + 'db-test.php must not contain hardcoded test password' + ); + } + + /** + * Test db-test.php has no hardcoded user + */ + public function testDbTestNoHardcodedUser(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + // Should not have hardcoded DB_USER assignment with test username + $this->assertDoesNotMatchRegularExpression( + '/[\'"]DB_USER[\'"]\s*=>\s*[\'"]erp24_api_test[\'"]/', + $content, + 'db-test.php must not contain hardcoded test username' + ); + } + + /** + * Test db-test.php has conditional initialization + */ + public function testDbTestHasConditionalInit(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/if\s*\(\s*!empty\s*\(\s*\$config2\[.DB_USER.\]/', + $content, + 'db-test.php must check for credentials before init' + ); + } + + /** + * Test db-test.php has PHP doc + */ + public function testDbTestHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/db-test.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'db-test.php must have documentation comment' + ); + } + + // ============================================================ + // .env.example tests + // ============================================================ + + /** + * Test .env.example has MYSQL_TEST_HOST + */ + public function testEnvExampleHasMysqlTestHost(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_TEST_HOST=', + $envExample, + '.env.example must contain MYSQL_TEST_HOST' + ); + } + + /** + * Test .env.example has MYSQL_TEST_USER + */ + public function testEnvExampleHasMysqlTestUser(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_TEST_USER=', + $envExample, + '.env.example must contain MYSQL_TEST_USER' + ); + } + + /** + * Test .env.example has MYSQL_TEST_PASSWORD + */ + public function testEnvExampleHasMysqlTestPassword(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_TEST_PASSWORD=', + $envExample, + '.env.example must contain MYSQL_TEST_PASSWORD' + ); + } + + /** + * Test .env.example has MYSQL_TEST_DB + */ + public function testEnvExampleHasMysqlTestDb(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'MYSQL_TEST_DB=', + $envExample, + '.env.example must contain MYSQL_TEST_DB' + ); + } + + /** + * Test no leaked test password in .env.example + */ + public function testEnvExampleNoLeakedTestPassword(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringNotContainsString( + 'yX2hF4mO2omY7x', + $envExample, + '.env.example must not contain leaked test password' + ); + } +} diff --git a/erp24/tests/unit/config/DockerSecretsTest.php b/erp24/tests/unit/config/DockerSecretsTest.php new file mode 100644 index 00000000..9fe9645a --- /dev/null +++ b/erp24/tests/unit/config/DockerSecretsTest.php @@ -0,0 +1,303 @@ +runningInDocker = file_exists('/.dockerenv') || + is_dir('/www/vendor') || + (getenv('DOCKER_CONTAINER') !== false); + + // When running in Docker, erp24 is mounted at /www + // but docker-compose.yml is in the parent directory (not mounted) + if ($this->runningInDocker) { + // Try to find project root by going up from /www + $this->projectRoot = '/www/..'; // Parent of /www + } else { + $this->projectRoot = dirname(__DIR__, 4); + } + } + + /** + * Helper to get file content, skipping test if file not accessible + */ + private function getFileContentOrSkip(string $filePath, string $skipReason): string + { + if (!file_exists($filePath)) { + $this->markTestSkipped( + "Skipped: {$skipReason}. File not accessible in Docker container." + ); + } + return file_get_contents($filePath); + } + + // ============================================================ + // docker-compose.yml tests + // ============================================================ + + /** + * Test docker-compose.yml exists + */ + public function testDockerComposeExists(): void + { + $filePath = $this->projectRoot . '/docker-compose.yml'; + + if ($this->runningInDocker && !file_exists($filePath)) { + $this->markTestSkipped('docker-compose.yml is outside Docker mount. Run tests on host.'); + } + + $this->assertFileExists($filePath, 'docker-compose.yml must exist'); + } + + /** + * Test RabbitMQ user uses env substitution + */ + public function testRabbitMqUserUsesEnv(): void + { + $filePath = $this->projectRoot . '/docker-compose.yml'; + $content = $this->getFileContentOrSkip($filePath, 'docker-compose.yml'); + + $this->assertMatchesRegularExpression( + '/RABBITMQ_DEFAULT_USER\s*:\s*\$\{RABBIT_USER/', + $content, + 'RABBITMQ_DEFAULT_USER must use ${RABBIT_USER} substitution' + ); + } + + /** + * Test RabbitMQ password uses env substitution + */ + public function testRabbitMqPasswordUsesEnv(): void + { + $filePath = $this->projectRoot . '/docker-compose.yml'; + $content = $this->getFileContentOrSkip($filePath, 'docker-compose.yml'); + + $this->assertMatchesRegularExpression( + '/RABBITMQ_DEFAULT_PASS\s*:\s*\$\{RABBIT_PASSWORD/', + $content, + 'RABBITMQ_DEFAULT_PASS must use ${RABBIT_PASSWORD} substitution' + ); + } + + /** + * Test no hardcoded RabbitMQ password + */ + public function testNoHardcodedRabbitMqPassword(): void + { + $filePath = $this->projectRoot . '/docker-compose.yml'; + $content = $this->getFileContentOrSkip($filePath, 'docker-compose.yml'); + + // Should not contain a hardcoded password like: RABBITMQ_DEFAULT_PASS: somepassword + // But allow: RABBITMQ_DEFAULT_PASS: ${RABBIT_PASSWORD:-default} + $this->assertDoesNotMatchRegularExpression( + '/RABBITMQ_DEFAULT_PASS\s*:\s*[^$\s\n][^\n]*(?projectRoot . '/docker-compose.yml'; + $content = $this->getFileContentOrSkip($filePath, 'docker-compose.yml'); + + // Check for default value syntax: ${VAR:-default} + $this->assertMatchesRegularExpression( + '/\$\{RABBIT_USER:-[^}]+\}/', + $content, + 'RABBIT_USER must have default fallback value' + ); + + $this->assertMatchesRegularExpression( + '/\$\{RABBIT_PASSWORD:-[^}]+\}/', + $content, + 'RABBIT_PASSWORD must have default fallback value' + ); + } + + // ============================================================ + // docker/php/dev.php.env tests + // ============================================================ + + /** + * Test dev.php.env exists + */ + public function testDevPhpEnvExists(): void + { + $filePath = $this->projectRoot . '/docker/php/dev.php.env'; + + if ($this->runningInDocker && !file_exists($filePath)) { + $this->markTestSkipped('docker/php/dev.php.env is outside Docker mount. Run tests on host.'); + } + + $this->assertFileExists($filePath, 'docker/php/dev.php.env must exist'); + } + + /** + * Test dev.php.env has no hardcoded DB password + */ + public function testDevPhpEnvNoHardcodedDbPassword(): void + { + $filePath = $this->projectRoot . '/docker/php/dev.php.env'; + $content = $this->getFileContentOrSkip($filePath, 'docker/php/dev.php.env'); + + // Should not contain actual password, only placeholder or env reference + $this->assertDoesNotMatchRegularExpression( + '/DB_PASSWORD\s*=\s*[A-Za-z0-9_]{8,}(?projectRoot . '/docker/php/dev.php.env'; + $content = $this->getFileContentOrSkip($filePath, 'docker/php/dev.php.env'); + + // Should use ${DB_PASSWORD:-...} or placeholder + $this->assertMatchesRegularExpression( + '/DB_PASSWORD\s*=\s*\$\{DB_PASSWORD|DB_PASSWORD\s*=\s*dev_password/', + $content, + 'dev.php.env must use env substitution or placeholder for DB_PASSWORD' + ); + } + + /** + * Test dev.php.env has warning comment + */ + public function testDevPhpEnvHasWarningComment(): void + { + $filePath = $this->projectRoot . '/docker/php/dev.php.env'; + $content = $this->getFileContentOrSkip($filePath, 'docker/php/dev.php.env'); + + $this->assertStringContainsString( + 'ВАЖНО', + $content, + 'dev.php.env must have warning comment' + ); + } + + // ============================================================ + // .gitignore tests for docker secrets + // ============================================================ + + /** + * Test .gitignore excludes docker env files + */ + public function testGitignoreExcludesDockerEnv(): void + { + $filePath = $this->projectRoot . '/.gitignore'; + $content = $this->getFileContentOrSkip($filePath, '.gitignore'); + + $this->assertMatchesRegularExpression( + '/docker\/php\/\*\.env/', + $content, + '.gitignore must exclude docker/php/*.env files' + ); + } + + /** + * Test .gitignore keeps .env.example files + */ + public function testGitignoreKeepsEnvExample(): void + { + $filePath = $this->projectRoot . '/.gitignore'; + $content = $this->getFileContentOrSkip($filePath, '.gitignore'); + + $this->assertMatchesRegularExpression( + '/!\*\.env\.example|!docker\/php\/\*\.env\.example/', + $content, + '.gitignore must keep *.env.example files' + ); + } + + // ============================================================ + // Security pattern tests + // ============================================================ + + /** + * Test no known leaked passwords in docker files + */ + public function testNoKnownLeakedPasswords(): void + { + $files = [ + $this->projectRoot . '/docker-compose.yml', + $this->projectRoot . '/docker/php/dev.php.env', + ]; + + // Known leaked passwords that should be rotated + $leakedPasswords = [ + '3qqHK2MRgGgxUdVT61', // RabbitMQ + 'JVJruro_Xdg456o3ir', // DB password + ]; + + $testedAny = false; + foreach ($files as $file) { + if (!file_exists($file)) { + continue; + } + $testedAny = true; + $content = file_get_contents($file); + + foreach ($leakedPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + basename($file) . " must not contain leaked password: {$password}" + ); + } + } + + if (!$testedAny && $this->runningInDocker) { + $this->markTestSkipped('Docker files not accessible. Run tests on host.'); + } + } + + /** + * Test docker-compose.yml env_file references + */ + public function testDockerComposeEnvFileReferences(): void + { + $filePath = $this->projectRoot . '/docker-compose.yml'; + $content = $this->getFileContentOrSkip($filePath, 'docker-compose.yml'); + + // PHP service should reference env file + $this->assertMatchesRegularExpression( + '/env_file\s*:.*docker\/php\/.*\.env/s', + $content, + 'docker-compose.yml should reference docker/php/*.env in env_file' + ); + } +} diff --git a/erp24/tests/unit/config/DocumentationSecretsTest.php b/erp24/tests/unit/config/DocumentationSecretsTest.php new file mode 100644 index 00000000..c30ff6b5 --- /dev/null +++ b/erp24/tests/unit/config/DocumentationSecretsTest.php @@ -0,0 +1,191 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // DEPENDENCIES.md tests + // ============================================================ + + /** + * Test DEPENDENCIES.md exists + */ + public function testDependenciesMdExists(): void + { + $filePath = $this->basePath . '/docs/api/api2/DEPENDENCIES.md'; + $this->assertFileExists($filePath, 'docs/api/api2/DEPENDENCIES.md must exist'); + } + + /** + * Test DEPENDENCIES.md has no hardcoded RabbitMQ password + */ + public function testDependenciesMdNoHardcodedRabbitPassword(): void + { + $filePath = $this->basePath . '/docs/api/api2/DEPENDENCIES.md'; + $content = file_get_contents($filePath); + + // Known leaked RabbitMQ password + $this->assertStringNotContainsString( + '3qqHK2MRgGgxUdVT61', + $content, + 'DEPENDENCIES.md must not contain hardcoded RabbitMQ password' + ); + } + + /** + * Test DEPENDENCIES.md uses placeholder for RabbitMQ credentials + */ + public function testDependenciesMdUsesRabbitPlaceholder(): void + { + $filePath = $this->basePath . '/docs/api/api2/DEPENDENCIES.md'; + $content = file_get_contents($filePath); + + // Should use placeholder variables + $this->assertMatchesRegularExpression( + '/\$\{RABBIT_USER\}|\$\{RABBIT_PASSWORD\}/', + $content, + 'DEPENDENCIES.md should use ${RABBIT_*} placeholders' + ); + } + + // ============================================================ + // ARCHITECTURE.md tests + // ============================================================ + + /** + * Test ARCHITECTURE.md exists + */ + public function testArchitectureMdExists(): void + { + $filePath = $this->basePath . '/docs/api/api2/ARCHITECTURE.md'; + $this->assertFileExists($filePath, 'docs/api/api2/ARCHITECTURE.md must exist'); + } + + /** + * Test ARCHITECTURE.md has no hardcoded RabbitMQ password + */ + public function testArchitectureMdNoHardcodedRabbitPassword(): void + { + $filePath = $this->basePath . '/docs/api/api2/ARCHITECTURE.md'; + $content = file_get_contents($filePath); + + // Known leaked RabbitMQ password + $this->assertStringNotContainsString( + '3qqHK2MRgGgxUdVT61', + $content, + 'ARCHITECTURE.md must not contain hardcoded RabbitMQ password' + ); + } + + /** + * Test ARCHITECTURE.md uses placeholder for RabbitMQ credentials + */ + public function testArchitectureMdUsesRabbitPlaceholder(): void + { + $filePath = $this->basePath . '/docs/api/api2/ARCHITECTURE.md'; + $content = file_get_contents($filePath); + + // Should use placeholder variables + $this->assertMatchesRegularExpression( + '/\$\{RABBIT_USER\}|\$\{RABBIT_PASSWORD\}/', + $content, + 'ARCHITECTURE.md should use ${RABBIT_*} placeholders' + ); + } + + /** + * Test ARCHITECTURE.md mentions .env for secrets + */ + public function testArchitectureMdMentionsEnvForSecrets(): void + { + $filePath = $this->basePath . '/docs/api/api2/ARCHITECTURE.md'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + '.env', + $content, + 'ARCHITECTURE.md should mention .env for credentials' + ); + } + + // ============================================================ + // General documentation security tests + // ============================================================ + + /** + * Test no known leaked passwords in documentation + */ + public function testNoLeakedPasswordsInDocs(): void + { + $docFiles = [ + $this->basePath . '/docs/api/api2/DEPENDENCIES.md', + $this->basePath . '/docs/api/api2/ARCHITECTURE.md', + ]; + + $leakedPasswords = [ + '3qqHK2MRgGgxUdVT61', // RabbitMQ + 'JVJruro_Xdg456o3ir', // MySQL + 'iC04295J9HyD2H3GJF3btky', // TOKEN_CLOUD + 'getJH6GFi4tpU84YVPW9M', // API_TOKEN + 'aG7hY9kR', // demo2 basic auth + 'mL9mN0pO7l', // kassear basic auth + 'yX2hF4mO2omY7x', // test DB password + ]; + + foreach ($docFiles as $filePath) { + if (!file_exists($filePath)) { + continue; + } + + $content = file_get_contents($filePath); + + foreach ($leakedPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + basename($filePath) . " must not contain leaked password: {$password}" + ); + } + } + } + + /** + * Test documentation files recommend using environment variables + */ + public function testDocsRecommendEnvVariables(): void + { + $filePath = $this->basePath . '/docs/api/api2/ARCHITECTURE.md'; + $content = file_get_contents($filePath); + + // Should have a note about environment variables + $this->assertMatchesRegularExpression( + '/ВАЖНО|env|\.env|переменных окружения/i', + $content, + 'ARCHITECTURE.md should recommend using environment variables' + ); + } +} diff --git a/erp24/tests/unit/config/DomruCamerasSecretsTest.php b/erp24/tests/unit/config/DomruCamerasSecretsTest.php new file mode 100644 index 00000000..d111346c --- /dev/null +++ b/erp24/tests/unit/config/DomruCamerasSecretsTest.php @@ -0,0 +1,395 @@ +api1CronPath = $basePath . '/api1/views/cron/domru-cams.php'; + $this->modulApiPath = $basePath . '/modul/api/domru_cams.php'; + $this->api1OldPath = $basePath . '/api1_old/cron/domru_cams.php'; + } + + // === api1/views/cron/domru-cams.php tests === + + /** + * Test api1 cron file exists + */ + public function testApi1CronFileExists(): void + { + $this->assertFileExists( + $this->api1CronPath, + 'api1/views/cron/domru-cams.php must exist' + ); + } + + /** + * Test api1 cron uses all 6 camera credentials from env + */ + public function testApi1CronUsesAllCameraCredentials(): void + { + $content = file_get_contents($this->api1CronPath); + + for ($i = 1; $i <= 6; $i++) { + $this->assertStringContainsString( + "getenv('CAMERA_{$i}_LOGIN')", + $content, + "domru-cams.php must use CAMERA_{$i}_LOGIN from env" + ); + + $this->assertStringContainsString( + "getenv('CAMERA_{$i}_PASSWORD')", + $content, + "domru-cams.php must use CAMERA_{$i}_PASSWORD from env" + ); + } + } + + /** + * Test api1 cron has no hardcoded logins + */ + public function testApi1CronNoHardcodedLogins(): void + { + $content = file_get_contents($this->api1CronPath); + + foreach ($this->forbiddenLogins as $login) { + $this->assertStringNotContainsString( + $login, + $content, + "domru-cams.php must not contain hardcoded login: $login" + ); + } + } + + /** + * Test api1 cron has no hardcoded passwords + */ + public function testApi1CronNoHardcodedPasswords(): void + { + $content = file_get_contents($this->api1CronPath); + + foreach ($this->forbiddenPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + "domru-cams.php must not contain hardcoded password: $password" + ); + } + } + + /** + * Test api1 cron PHPDoc exists + */ + public function testApi1CronPhpDocExists(): void + { + $content = file_get_contents($this->api1CronPath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + + $this->assertStringContainsString( + 'CAMERA_1_LOGIN', + $content, + 'PHPDoc must mention CAMERA_1_LOGIN' + ); + } + + /** + * Test api1 cron getenv has empty fallback + */ + public function testApi1CronGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->api1CronPath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'CAMERA_1_LOGIN'\s*\)\s*\?:\s*''/", + $content, + 'CAMERA_1_LOGIN must have empty string fallback' + ); + } + + // === modul/api/domru_cams.php tests === + + /** + * Test modul api file exists + */ + public function testModulApiFileExists(): void + { + $this->assertFileExists( + $this->modulApiPath, + 'modul/api/domru_cams.php must exist' + ); + } + + /** + * Test modul api uses camera credentials from env + */ + public function testModulApiUsesCameraCredentials(): void + { + $content = file_get_contents($this->modulApiPath); + + // This file uses cameras 1 and 2 + for ($i = 1; $i <= 2; $i++) { + $this->assertStringContainsString( + "getenv('CAMERA_{$i}_LOGIN')", + $content, + "domru_cams.php must use CAMERA_{$i}_LOGIN from env" + ); + + $this->assertStringContainsString( + "getenv('CAMERA_{$i}_PASSWORD')", + $content, + "domru_cams.php must use CAMERA_{$i}_PASSWORD from env" + ); + } + } + + /** + * Test modul api has no hardcoded logins + */ + public function testModulApiNoHardcodedLogins(): void + { + $content = file_get_contents($this->modulApiPath); + + foreach ($this->forbiddenLogins as $login) { + $this->assertStringNotContainsString( + $login, + $content, + "domru_cams.php must not contain hardcoded login: $login" + ); + } + } + + /** + * Test modul api has no hardcoded passwords + */ + public function testModulApiNoHardcodedPasswords(): void + { + $content = file_get_contents($this->modulApiPath); + + foreach ($this->forbiddenPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + "domru_cams.php must not contain hardcoded password: $password" + ); + } + } + + /** + * Test modul api PHPDoc exists + */ + public function testModulApiPhpDocExists(): void + { + $content = file_get_contents($this->modulApiPath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + } + + // === api1_old/cron/domru_cams.php tests === + + /** + * Test api1_old file exists + */ + public function testApi1OldFileExists(): void + { + $this->assertFileExists( + $this->api1OldPath, + 'api1_old/cron/domru_cams.php must exist' + ); + } + + /** + * Test api1_old uses camera credentials from env + */ + public function testApi1OldUsesCameraCredentials(): void + { + $content = file_get_contents($this->api1OldPath); + + // This file uses cameras 1-5 + for ($i = 1; $i <= 5; $i++) { + $this->assertStringContainsString( + "getenv('CAMERA_{$i}_LOGIN')", + $content, + "api1_old/domru_cams.php must use CAMERA_{$i}_LOGIN from env" + ); + + $this->assertStringContainsString( + "getenv('CAMERA_{$i}_PASSWORD')", + $content, + "api1_old/domru_cams.php must use CAMERA_{$i}_PASSWORD from env" + ); + } + } + + /** + * Test api1_old has no hardcoded logins + */ + public function testApi1OldNoHardcodedLogins(): void + { + $content = file_get_contents($this->api1OldPath); + + foreach ($this->forbiddenLogins as $login) { + $this->assertStringNotContainsString( + $login, + $content, + "api1_old/domru_cams.php must not contain hardcoded login: $login" + ); + } + } + + /** + * Test api1_old has no hardcoded passwords + */ + public function testApi1OldNoHardcodedPasswords(): void + { + $content = file_get_contents($this->api1OldPath); + + foreach ($this->forbiddenPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + "api1_old/domru_cams.php must not contain hardcoded password: $password" + ); + } + } + + /** + * Test api1_old PHPDoc exists + */ + public function testApi1OldPhpDocExists(): void + { + $content = file_get_contents($this->api1OldPath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + } + + /** + * Test api1_old no hardcoded API token in URL comment + */ + public function testApi1OldNoHardcodedApiTokenInUrl(): void + { + $content = file_get_contents($this->api1OldPath); + + // The URL in comment should not contain the actual token + $this->assertStringNotContainsString( + '1CjgpXfgkh1pXV3KR2H57G3VtHCffrp154up1t36', + $content, + 'api1_old/domru_cams.php must not contain hardcoded API token' + ); + } + + // === Cross-file consistency tests === + + /** + * Test all files use consistent getenv pattern + */ + public function testAllFilesUseConsistentPattern(): void + { + $files = [ + $this->api1CronPath, + $this->modulApiPath, + $this->api1OldPath, + ]; + + foreach ($files as $file) { + $content = file_get_contents($file); + + // All should use the pattern: getenv('CAMERA_X_LOGIN') ?: '' + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'CAMERA_\d+_LOGIN'\s*\)\s*\?:\s*''/", + $content, + basename($file) . ' must use getenv with empty fallback pattern' + ); + } + } + + /** + * Test .env.example has all camera variables + */ + public function testEnvExampleHasAllCameraVariables(): void + { + $envExample = dirname(__DIR__, 3) . '/.env.example'; + $this->assertFileExists($envExample); + + $content = file_get_contents($envExample); + + for ($i = 1; $i <= 6; $i++) { + $this->assertStringContainsString( + "CAMERA_{$i}_LOGIN=", + $content, + ".env.example must have CAMERA_{$i}_LOGIN" + ); + + $this->assertStringContainsString( + "CAMERA_{$i}_PASSWORD=", + $content, + ".env.example must have CAMERA_{$i}_PASSWORD" + ); + } + } +} diff --git a/erp24/tests/unit/config/EnvConfigurationTest.php b/erp24/tests/unit/config/EnvConfigurationTest.php new file mode 100644 index 00000000..f9a02853 --- /dev/null +++ b/erp24/tests/unit/config/EnvConfigurationTest.php @@ -0,0 +1,237 @@ +markTestSkipped('.env file not fully loaded (Dotenv not initialized)'); + } + + foreach ($this->requiredEnvVars as $var) { + $value = getenv($var); + $this->assertNotFalse($value, "Required variable $var is not set"); + $this->assertNotEmpty($value, "Required variable $var is empty"); + } + } + + /** + * Verifies PostgreSQL environment variables + */ + public function testPostgresEnvVariables(): void + { + $host = getenv('POSTGRES_HOSTNAME'); + $port = getenv('POSTGRES_PORT'); + $schema = getenv('POSTGRES_SCHEMA'); + $user = getenv('POSTGRES_USER'); + $password = getenv('POSTGRES_PASSWORD'); + + // Only check if at least one var is set (indicates .env loaded) + if ($host === false && $port === false && $schema === false) { + $this->markTestSkipped('PostgreSQL env vars not loaded'); + } + + if ($host !== false) { + $this->assertNotEmpty($host, 'POSTGRES_HOSTNAME should not be empty'); + } + if ($port !== false) { + $this->assertTrue(is_numeric($port), 'POSTGRES_PORT must be numeric'); + } + } + + /** + * Verifies RabbitMQ environment variables + * + * @group integration + */ + public function testRabbitMQEnvVariables(): void + { + $host = getenv('RABBIT_HOST'); + $user = getenv('RABBIT_USER'); + $password = getenv('RABBIT_PASSWORD'); + + // Skip if not configured + if ($host === false && $user === false && $password === false) { + $this->markTestSkipped('RabbitMQ env vars not loaded'); + } + + if ($host !== false) { + $this->assertNotEmpty($host, 'RABBIT_HOST should not be empty'); + } + if ($user !== false) { + $this->assertNotEmpty($user, 'RABBIT_USER should not be empty (required for AMQP DSN)'); + } + if ($password !== false) { + $this->assertNotEmpty($password, 'RABBIT_PASSWORD should not be empty'); + } + } + + /** + * Verifies Telegram bot token format + * + * @group integration + */ + public function testTelegramBotToken(): void + { + $token = getenv('TELEGRAM_BOT_TOKEN'); + + if ($token === false) { + $this->markTestSkipped('TELEGRAM_BOT_TOKEN not set in test environment'); + } + + $this->assertNotEmpty($token, 'TELEGRAM_BOT_TOKEN is empty'); + // Format: numeric_id:alphanumeric_part + $this->assertMatchesRegularExpression( + '/^\d+:[A-Za-z0-9_-]+$/', + $token, + 'TELEGRAM_BOT_TOKEN has invalid format' + ); + } + + /** + * Verifies camera environment variables (optional) + * Cameras are optional - only check cameras that are configured + * + * @group integration + */ + public function testCameraEnvVariables(): void + { + $configuredCameras = 0; + + // Check only cameras that have LOGIN configured + for ($i = 1; $i <= 5; $i++) { + $login = getenv("CAMERA_{$i}_LOGIN"); + + // Skip cameras that are not configured + if ($login === false || $login === '') { + continue; + } + + $configuredCameras++; + $password = getenv("CAMERA_{$i}_PASSWORD"); + + // If login is set, password should also be set + $this->assertNotFalse($password, "CAMERA_{$i}_PASSWORD not set but LOGIN is configured"); + } + + // Skip if no cameras configured at all + if ($configuredCameras === 0) { + $this->markTestSkipped('No camera env vars configured'); + } + + $this->assertGreaterThan(0, $configuredCameras, 'At least one camera should be configured'); + } + + /** + * Verifies security environment variables + * + * @group integration + */ + public function testSecurityEnvVariables(): void + { + $cookieKey = getenv('COOKIE_VALIDATION_KEY'); + $switchPassword = getenv('SWITCH_USER_COOKIE_PASSWORD'); + + if ($cookieKey === false) { + $this->markTestSkipped('Security env vars not loaded'); + } + + $this->assertNotEmpty($cookieKey, 'COOKIE_VALIDATION_KEY is empty'); + $this->assertGreaterThan(10, strlen($cookieKey), 'COOKIE_VALIDATION_KEY is too short'); + + if ($switchPassword !== false) { + $this->assertNotEmpty($switchPassword, 'SWITCH_USER_COOKIE_PASSWORD is empty'); + } + } + + /** + * Verifies API keys (optional) + * Keys may not be set in test environments + * + * @group integration + */ + public function testApiKeysEnvVariables(): void + { + $whatsappKey = getenv('WHATSAPP_API_KEY'); + $yandexKey = getenv('YANDEX_MARKET_API_KEY'); + + // Both are optional in test environment + if ($whatsappKey === false && $yandexKey === false) { + $this->markTestSkipped('API keys not configured in test environment'); + } + + // If WhatsApp key is set, validate UUID format + if ($whatsappKey !== false && !empty($whatsappKey)) { + $this->assertMatchesRegularExpression( + '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', + $whatsappKey, + 'WHATSAPP_API_KEY must be in UUID format' + ); + } + + // Yandex key just needs to exist if set + if ($yandexKey !== false) { + $this->assertNotEmpty($yandexKey, 'YANDEX_MARKET_API_KEY is empty'); + } + } + + /** + * Verifies remote database environment variables (optional) + * + * @group integration + */ + public function testRemoteDatabaseEnvVariables(): void + { + $host = getenv('DB_REMOTE_HOST'); + + // Remote DB is optional + if ($host === false) { + $this->markTestSkipped('Remote database env vars not configured'); + } + + $port = getenv('DB_REMOTE_PORT'); + $schema = getenv('DB_REMOTE_SCHEMA'); + $user = getenv('DB_REMOTE_USER'); + $password = getenv('DB_REMOTE_PASSWORD'); + + $this->assertNotEmpty($host, 'DB_REMOTE_HOST is empty'); + if ($port !== false) { + $this->assertTrue(is_numeric($port), 'DB_REMOTE_PORT must be numeric'); + } + } +} diff --git a/erp24/tests/unit/config/EnvExampleCompletenessTest.php b/erp24/tests/unit/config/EnvExampleCompletenessTest.php new file mode 100644 index 00000000..b01ec86e --- /dev/null +++ b/erp24/tests/unit/config/EnvExampleCompletenessTest.php @@ -0,0 +1,393 @@ +envExamplePath = dirname(__DIR__, 3) . '/.env.example'; + $this->envExampleContent = file_get_contents($this->envExamplePath); + } + + /** + * Test .env.example file exists + */ + public function testEnvExampleFileExists(): void + { + $this->assertFileExists( + $this->envExamplePath, + '.env.example must exist' + ); + } + + /** + * Test all required variables are present + */ + public function testAllRequiredVariablesPresent(): void + { + $missing = []; + + foreach ($this->requiredVariables as $var) { + if (strpos($this->envExampleContent, $var . '=') === false) { + $missing[] = $var; + } + } + + $this->assertEmpty( + $missing, + 'Missing required ENV variables in .env.example: ' . implode(', ', $missing) + ); + } + + /** + * Test Telegram variables section + */ + public function testTelegramSectionComplete(): void + { + $telegramVars = [ + 'TELEGRAM_BOT_TOKEN', + 'TELEGRAM_BOT_TOKEN_PROD', + 'TELEGRAM_BOT_TOKEN_SALEBOT', + 'TELEGRAM_CHAT_CHANNEL_ID', + 'TELEGRAM_CHAT_CHANNEL_ERP_ID', + 'CHATBOT_SALT', + ]; + + foreach ($telegramVars as $var) { + $this->assertStringContainsString( + $var . '=', + $this->envExampleContent, + "Telegram variable $var must be in .env.example" + ); + } + } + + /** + * Test CloudPayments variables section + */ + public function testCloudPaymentsSectionComplete(): void + { + $vars = [ + 'CLOUDPAYMENTS_PUBLIC_ID', + 'CLOUDPAYMENTS_SECRET', + 'CLOUDPAYMENTS_REGION_PUBLIC_ID', + 'CLOUDPAYMENTS_REGION_SECRET', + ]; + + foreach ($vars as $var) { + $this->assertStringContainsString( + $var . '=', + $this->envExampleContent, + "CloudPayments variable $var must be in .env.example" + ); + } + } + + /** + * Test LPTracker variables section + */ + public function testLpTrackerSectionComplete(): void + { + $this->assertStringContainsString( + 'LPTRACKER_LOGIN=', + $this->envExampleContent, + 'LPTRACKER_LOGIN must be in .env.example' + ); + + $this->assertStringContainsString( + 'LPTRACKER_PASSWORD=', + $this->envExampleContent, + 'LPTRACKER_PASSWORD must be in .env.example' + ); + } + + /** + * Test Email variables section + */ + public function testEmailSectionComplete(): void + { + $this->assertStringContainsString( + 'EMAIL_ZAKAZ_PASSWORD=', + $this->envExampleContent, + 'EMAIL_ZAKAZ_PASSWORD must be in .env.example' + ); + + $this->assertStringContainsString( + 'EMAIL_FLOW_PASSWORD=', + $this->envExampleContent, + 'EMAIL_FLOW_PASSWORD must be in .env.example' + ); + } + + /** + * Test IMAP variables section + */ + public function testImapSectionComplete(): void + { + $this->assertStringContainsString( + 'IMAP_EMAIL=', + $this->envExampleContent, + 'IMAP_EMAIL must be in .env.example' + ); + + $this->assertStringContainsString( + 'IMAP_PASSWORD=', + $this->envExampleContent, + 'IMAP_PASSWORD must be in .env.example' + ); + } + + /** + * Test Cameras section complete (6 cameras) + */ + public function testCamerasSectionComplete(): void + { + for ($i = 1; $i <= 6; $i++) { + $this->assertStringContainsString( + "CAMERA_{$i}_LOGIN=", + $this->envExampleContent, + "CAMERA_{$i}_LOGIN must be in .env.example" + ); + + $this->assertStringContainsString( + "CAMERA_{$i}_PASSWORD=", + $this->envExampleContent, + "CAMERA_{$i}_PASSWORD must be in .env.example" + ); + } + } + + /** + * Test GreenSMS variables section + */ + public function testGreenSmsSectionComplete(): void + { + $this->assertStringContainsString( + 'GREENSMS_API_KEY=', + $this->envExampleContent, + 'GREENSMS_API_KEY must be in .env.example' + ); + } + + /** + * Test BonusPlus variables section + */ + public function testBonusPlusSectionComplete(): void + { + $this->assertStringContainsString( + 'BONUSPLUS_API_KEY=', + $this->envExampleContent, + 'BONUSPLUS_API_KEY must be in .env.example' + ); + } + + /** + * Test .env.example has proper section headers + */ + public function testHasSectionHeaders(): void + { + $sections = [ + 'APPLICATION', + 'DATABASE', + 'RABBITMQ', + 'TELEGRAM', + 'SECURITY', + 'LPTRACKER', + 'PAYMENTS', + 'EMAIL', + ]; + + foreach ($sections as $section) { + $this->assertMatchesRegularExpression( + '/=+\s*' . $section . '\s*=+|#\s*=+\s*' . $section . '/i', + $this->envExampleContent, + "Section header for $section should exist" + ); + } + } + + /** + * Test .env.example has comments for sensitive fields + */ + public function testHasCommentsForSensitiveFields(): void + { + // Check that there are helpful comments + $this->assertStringContainsString( + 'DEV STUB', + $this->envExampleContent, + '.env.example should have DEV STUB comments for sensitive fields' + ); + } + + /** + * Test no real secrets in .env.example + */ + public function testNoRealSecretsInEnvExample(): void + { + // Known patterns that should NOT be in .env.example + $forbiddenPatterns = [ + '/\d{10}:AA[A-Za-z0-9_-]{33}/', // Real Telegram token + '/pk_[a-f0-9]{30,}/', // Real CloudPayments key + '/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/', // Real UUID (except as example) + ]; + + foreach ($forbiddenPatterns as $pattern) { + // Allow the placeholder UUID format but not real-looking ones + if ($pattern !== '/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/') { + $this->assertDoesNotMatchRegularExpression( + $pattern, + $this->envExampleContent, + ".env.example must not contain real secrets matching pattern: $pattern" + ); + } + } + } + + /** + * Test placeholder values are used for sensitive fields + */ + public function testPlaceholderValuesForSensitiveFields(): void + { + // Check that sensitive fields have placeholder or empty values + $this->assertMatchesRegularExpression( + '/POSTGRES_PASSWORD=dev_password_change_me|POSTGRES_PASSWORD=$/', + $this->envExampleContent, + 'POSTGRES_PASSWORD should have placeholder or empty value' + ); + } +} diff --git a/erp24/tests/unit/config/GreenSmsSecretsTest.php b/erp24/tests/unit/config/GreenSmsSecretsTest.php new file mode 100644 index 00000000..dd6cd13f --- /dev/null +++ b/erp24/tests/unit/config/GreenSmsSecretsTest.php @@ -0,0 +1,142 @@ +filePath = dirname(__DIR__, 3) . '/modul/api/greensms.php'; + } + + /** + * Test greensms.php file exists + */ + public function testFileExists(): void + { + $this->assertFileExists( + $this->filePath, + 'greensms.php must exist' + ); + } + + /** + * Test GREENSMS_API_KEY uses getenv + */ + public function testApiKeyUsesEnv(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + "getenv('GREENSMS_API_KEY')", + $content, + 'greensms.php must use GREENSMS_API_KEY from env' + ); + } + + /** + * Test no hardcoded API key + */ + public function testNoHardcodedApiKey(): void + { + $content = file_get_contents($this->filePath); + + // Check that $apikey is not assigned a literal string + $this->assertDoesNotMatchRegularExpression( + '/\$apikey\s*=\s*[\'"][a-zA-Z0-9]{20,}[\'"];/', + $content, + 'greensms.php must not have hardcoded API key' + ); + } + + /** + * Test getenv has empty string fallback + */ + public function testGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->filePath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'GREENSMS_API_KEY'\s*\)\s*\?:\s*''/", + $content, + 'GREENSMS_API_KEY must have empty string fallback' + ); + } + + /** + * Test PHPDoc documentation exists + */ + public function testPhpDocExists(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + + $this->assertStringContainsString( + 'GREENSMS_API_KEY', + $content, + 'PHPDoc must mention GREENSMS_API_KEY' + ); + } + + /** + * Test API key is used in Authorization header + */ + public function testApiKeyUsedInAuthorization(): void + { + $content = file_get_contents($this->filePath); + + // Check that $apikey is used in Authorization header + $this->assertStringContainsString( + '"Authorization: $apikey"', + $content, + 'API key must be used in Authorization header' + ); + } + + /** + * Test API key is used in JSON payload + */ + public function testApiKeyUsedInJsonPayload(): void + { + $content = file_get_contents($this->filePath); + + // Check that $apikey variable is used in JSON security block + // Pattern in code: "apiKey": "'.$apikey.'" + $this->assertStringContainsString( + '$apikey', + $content, + 'API key variable must be used in JSON security payload' + ); + + $this->assertStringContainsString( + 'apiKey', + $content, + 'apiKey field must be present in JSON payload' + ); + } +} diff --git a/erp24/tests/unit/config/ImapSecretsTest.php b/erp24/tests/unit/config/ImapSecretsTest.php new file mode 100644 index 00000000..2e19cb68 --- /dev/null +++ b/erp24/tests/unit/config/ImapSecretsTest.php @@ -0,0 +1,202 @@ +filePath = dirname(__DIR__, 3) . '/inc/mail.php'; + } + + /** + * Test mail.php file exists + */ + public function testFileExists(): void + { + $this->assertFileExists( + $this->filePath, + 'mail.php must exist' + ); + } + + /** + * Test IMAP_EMAIL uses getenv in rmail function + */ + public function testImapEmailUsesEnv(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + "getenv('IMAP_EMAIL')", + $content, + 'mail.php must use IMAP_EMAIL from env' + ); + } + + /** + * Test IMAP_PASSWORD uses getenv + */ + public function testImapPasswordUsesEnv(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + "getenv('IMAP_PASSWORD')", + $content, + 'mail.php must use IMAP_PASSWORD from env' + ); + } + + /** + * Test no hardcoded email address + */ + public function testNoHardcodedEmail(): void + { + $content = file_get_contents($this->filePath); + + // Check that doors-click@yandex.ru is not used as a value assignment + $this->assertDoesNotMatchRegularExpression( + '/\$mail_login\s*=\s*[\'"]doors-click@yandex\.ru[\'"]\s*;/', + $content, + 'mail.php must not have hardcoded email address' + ); + + $this->assertDoesNotMatchRegularExpression( + '/\$email\s*=\s*[\'"]doors-click@yandex\.ru[\'"]\s*;/', + $content, + 'mail.php must not have hardcoded email in $email variable' + ); + } + + /** + * Test no hardcoded password + */ + public function testNoHardcodedPassword(): void + { + $content = file_get_contents($this->filePath); + + // Check that VlrVtnwjCg462vs is not present + $this->assertStringNotContainsString( + 'VlrVtnwjCg462vs', + $content, + 'mail.php must not have hardcoded IMAP password' + ); + } + + /** + * Test getenv has empty string fallback for email + */ + public function testEmailGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->filePath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'IMAP_EMAIL'\s*\)\s*\?:\s*''/", + $content, + 'IMAP_EMAIL must have empty string fallback' + ); + } + + /** + * Test getenv has empty string fallback for password + */ + public function testPasswordGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->filePath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'IMAP_PASSWORD'\s*\)\s*\?:\s*''/", + $content, + 'IMAP_PASSWORD must have empty string fallback' + ); + } + + /** + * Test PHPDoc documentation exists + */ + public function testPhpDocExists(): void + { + $content = file_get_contents($this->filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + + $this->assertStringContainsString( + 'IMAP_EMAIL', + $content, + 'PHPDoc must mention IMAP_EMAIL' + ); + + $this->assertStringContainsString( + 'IMAP_PASSWORD', + $content, + 'PHPDoc must mention IMAP_PASSWORD' + ); + } + + /** + * Test imap_open uses getenv credentials + */ + public function testImapOpenUsesEnvCredentials(): void + { + $content = file_get_contents($this->filePath); + + // Check that imap_open is called with variables from getenv + // Pattern: getenv('IMAP_EMAIL') should be used before imap_open + $emailEnvPos = strpos($content, "getenv('IMAP_EMAIL')"); + $imapOpenPos = strpos($content, 'imap_open'); + + $this->assertNotFalse($emailEnvPos, 'getenv(IMAP_EMAIL) must be present'); + $this->assertNotFalse($imapOpenPos, 'imap_open must be present'); + } + + /** + * Test multiple IMAP functions use getenv + */ + public function testMultipleImapFunctionsUseEnv(): void + { + $content = file_get_contents($this->filePath); + + // Count occurrences of getenv('IMAP_EMAIL') + $emailCount = substr_count($content, "getenv('IMAP_EMAIL')"); + $passwordCount = substr_count($content, "getenv('IMAP_PASSWORD')"); + + // Should be used at least 3 times (rmail, reader_mail, commented section) + $this->assertGreaterThanOrEqual( + 3, + $emailCount, + 'IMAP_EMAIL getenv should be used in multiple places' + ); + + $this->assertGreaterThanOrEqual( + 3, + $passwordCount, + 'IMAP_PASSWORD getenv should be used in multiple places' + ); + } +} diff --git a/erp24/tests/unit/config/LegacyDbSecretsTest.php b/erp24/tests/unit/config/LegacyDbSecretsTest.php new file mode 100644 index 00000000..385fb247 --- /dev/null +++ b/erp24/tests/unit/config/LegacyDbSecretsTest.php @@ -0,0 +1,457 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // inc/db2.php tests + // ============================================================ + + /** + * Test db2.php exists + */ + public function testDb2FileExists(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $this->assertFileExists($filePath, 'inc/db2.php must exist'); + } + + /** + * Test db2.php is valid PHP + */ + public function testDb2IsValidPhp(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $output = []; + $returnCode = 0; + exec("php -l {$filePath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'inc/db2.php must be valid PHP: ' . implode("\n", $output) + ); + } + + /** + * Test db2.php uses DB_REMOTE_HOST env + */ + public function testDb2UsesRemoteHostEnv(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_HOST[\'"]\s*\)/', + $content, + 'db2.php must use getenv(\'DB_REMOTE_HOST\')' + ); + } + + /** + * Test db2.php uses DB_REMOTE_USER env + */ + public function testDb2UsesRemoteUserEnv(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_USER[\'"]\s*\)/', + $content, + 'db2.php must use getenv(\'DB_REMOTE_USER\')' + ); + } + + /** + * Test db2.php uses DB_REMOTE_PASSWORD env + */ + public function testDb2UsesRemotePasswordEnv(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_PASSWORD[\'"]\s*\)/', + $content, + 'db2.php must use getenv(\'DB_REMOTE_PASSWORD\')' + ); + } + + /** + * Test db2.php uses DB_REMOTE_SCHEMA env + */ + public function testDb2UsesRemoteSchemaEnv(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_SCHEMA[\'"]\s*\)/', + $content, + 'db2.php must use getenv(\'DB_REMOTE_SCHEMA\')' + ); + } + + /** + * Test db2.php has no hardcoded password + */ + public function testDb2NoHardcodedPassword(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + // Should not contain hardcoded password in config array + $this->assertDoesNotMatchRegularExpression( + '/[\'"]DB_PASSWORD[\'"]\s*=>\s*[\'"][A-Za-z0-9_]{8,}[\'"]/', + $content, + 'db2.php must not contain hardcoded DB_PASSWORD' + ); + } + + /** + * Test db2.php has no hardcoded user + */ + public function testDb2NoHardcodedUser(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + // Should not have hardcoded username (except in comments) + $this->assertDoesNotMatchRegularExpression( + '/[\'"]DB_USER[\'"]\s*=>\s*[\'"]bazacvetov24[\'"]/', + $content, + 'db2.php must not contain hardcoded DB_USER' + ); + } + + /** + * Test db2.php getenv has fallbacks + */ + public function testDb2HasFallbacks(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + // Host should have default fallback + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_HOST[\'"]\s*\)\s*\?:/', + $content, + 'DB_REMOTE_HOST getenv must have fallback' + ); + + // User/Password should have empty fallback + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_USER[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $content, + 'DB_REMOTE_USER getenv must have empty fallback' + ); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_REMOTE_PASSWORD[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $content, + 'DB_REMOTE_PASSWORD getenv must have empty fallback' + ); + } + + /** + * Test db2.php has conditional initialization + */ + public function testDb2HasConditionalInit(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + // DB2 should only be initialized if credentials are provided + $this->assertMatchesRegularExpression( + '/if\s*\(\s*!empty\s*\(\s*\$config2\[.DB_USER.\]\s*\)/', + $content, + 'db2.php must check for DB_USER before initializing' + ); + } + + /** + * Test db2.php has PHP doc + */ + public function testDb2HasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/db2.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'db2.php must have documentation comment' + ); + } + + // ============================================================ + // inc/crmconf.php tests + // ============================================================ + + /** + * Test crmconf.php exists + */ + public function testCrmconfFileExists(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $this->assertFileExists($filePath, 'inc/crmconf.php must exist'); + } + + /** + * Test crmconf.php is valid PHP + */ + public function testCrmconfIsValidPhp(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $output = []; + $returnCode = 0; + exec("php -l {$filePath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'inc/crmconf.php must be valid PHP: ' . implode("\n", $output) + ); + } + + /** + * Test crmconf.php uses DB_HOST env + */ + public function testCrmconfUsesDbHostEnv(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_HOST[\'"]\s*\)/', + $content, + 'crmconf.php must use getenv(\'DB_HOST\')' + ); + } + + /** + * Test crmconf.php uses DB_SCHEMA env + */ + public function testCrmconfUsesDbSchemaEnv(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_SCHEMA[\'"]\s*\)/', + $content, + 'crmconf.php must use getenv(\'DB_SCHEMA\')' + ); + } + + /** + * Test crmconf.php uses DB_USER env + */ + public function testCrmconfUsesDbUserEnv(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_USER[\'"]\s*\)/', + $content, + 'crmconf.php must use getenv(\'DB_USER\')' + ); + } + + /** + * Test crmconf.php uses DB_PASSWORD env + */ + public function testCrmconfUsesDbPasswordEnv(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_PASSWORD[\'"]\s*\)/', + $content, + 'crmconf.php must use getenv(\'DB_PASSWORD\')' + ); + } + + /** + * Test crmconf.php has no hardcoded mysql_pass + */ + public function testCrmconfNoHardcodedMysqlPass(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + // Should not contain hardcoded password + $this->assertDoesNotMatchRegularExpression( + '/\[.mysql_pass.\]\s*=\s*["\'][A-Za-z0-9_]{8,}["\']/', + $content, + 'crmconf.php must not contain hardcoded mysql_pass' + ); + } + + /** + * Test crmconf.php has no hardcoded mysql_user + */ + public function testCrmconfNoHardcodedMysqlUser(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + // Should not have hardcoded username + $this->assertDoesNotMatchRegularExpression( + '/\[.mysql_user.\]\s*=\s*["\']bazacvetov24["\']/', + $content, + 'crmconf.php must not contain hardcoded mysql_user' + ); + } + + /** + * Test crmconf.php getenv has fallbacks + */ + public function testCrmconfHasFallbacks(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]DB_HOST[\'"]\s*\)\s*\?:/', + $content, + 'DB_HOST getenv must have fallback' + ); + } + + /** + * Test crmconf.php has PHP doc + */ + public function testCrmconfHasPhpDoc(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'crmconf.php must have documentation comment' + ); + } + + /** + * Test crmconf.php references .env.example + */ + public function testCrmconfReferencesEnvExample(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + '@see erp24/.env.example', + $content, + 'crmconf.php must reference .env.example' + ); + } + + // ============================================================ + // Cross-file consistency tests + // ============================================================ + + /** + * Test no known leaked passwords in legacy DB files + */ + public function testNoKnownLeakedPasswords(): void + { + $files = [ + $this->basePath . '/inc/db2.php', + $this->basePath . '/inc/crmconf.php', + ]; + + $leakedPasswords = [ + 'JVJruro_Xdg456o3ir', + 'hQ6xG5aQ7paV5q', // Password from comments + ]; + + foreach ($files as $file) { + $content = file_get_contents($file); + + foreach ($leakedPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + basename($file) . " must not contain leaked password" + ); + } + } + } + + /** + * Test .env.example has all required DB_REMOTE variables + */ + public function testEnvExampleHasRemoteDbVariables(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $requiredVars = [ + 'DB_REMOTE_HOST', + 'DB_REMOTE_PORT', + 'DB_REMOTE_SCHEMA', + 'DB_REMOTE_USER', + 'DB_REMOTE_PASSWORD', + ]; + + foreach ($requiredVars as $var) { + $this->assertStringContainsString( + $var . '=', + $envExample, + ".env.example must contain {$var}" + ); + } + } + + /** + * Test both $_CONFIG_SITE and $CONFIG_SETKA use getenv + */ + public function testBothConfigArraysUseGetenv(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + // Check $_CONFIG_SITE uses getenv for mysql_pass + $this->assertMatchesRegularExpression( + '/\$_CONFIG_SITE\[.mysql_pass.\]\s*=\s*getenv/', + $content, + '$_CONFIG_SITE["mysql_pass"] must use getenv' + ); + + // Check $CONFIG_SETKA uses getenv for mysql_pass + $this->assertMatchesRegularExpression( + '/\$CONFIG_SETKA\[.mysql_pass.\]\s*=\s*getenv/', + $content, + '$CONFIG_SETKA["mysql_pass"] must use getenv' + ); + } +} diff --git a/erp24/tests/unit/config/LegacyTelegramBotsSecretsTest.php b/erp24/tests/unit/config/LegacyTelegramBotsSecretsTest.php new file mode 100644 index 00000000..d7c728b6 --- /dev/null +++ b/erp24/tests/unit/config/LegacyTelegramBotsSecretsTest.php @@ -0,0 +1,322 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // bc24_alerts_bot.php tests + // ============================================================ + + /** + * Test bc24_alerts_bot.php exists + */ + public function testAlertsBot24FileExists(): void + { + $filePath = $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php'; + $this->assertFileExists($filePath, 'bc24_alerts_bot.php must exist'); + } + + /** + * Test bc24_alerts_bot.php uses TELEGRAM_BOT_ALERTS env + */ + public function testAlertsBot24UsesEnv(): void + { + $filePath = $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_ALERTS[\'"]\s*\)/', + $content, + 'bc24_alerts_bot.php must use getenv(\'TELEGRAM_BOT_ALERTS\')' + ); + } + + /** + * Test bc24_alerts_bot.php has no hardcoded token + */ + public function testAlertsBot24NoHardcodedToken(): void + { + $filePath = $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php'; + $content = file_get_contents($filePath); + + // Real Telegram token format + $this->assertDoesNotMatchRegularExpression( + '/[\'"][0-9]{9,11}:AA[A-Za-z0-9_-]{30,}[\'"]/', + $content, + 'bc24_alerts_bot.php must not contain hardcoded bot token' + ); + } + + /** + * Test bc24_alerts_bot.php getenv has fallback + */ + public function testAlertsBot24HasFallback(): void + { + $filePath = $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_ALERTS[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $content, + 'bc24_alerts_bot.php getenv must have empty fallback' + ); + } + + /** + * Test bc24_alerts_bot.php has PHP doc + */ + public function testAlertsBot24HasPhpDoc(): void + { + $filePath = $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'bc24_alerts_bot.php must have documentation comment' + ); + } + + // ============================================================ + // OrderFlowersBaza24Bot.php tests + // ============================================================ + + /** + * Test OrderFlowersBaza24Bot.php exists + */ + public function testOrdersBotFileExists(): void + { + $filePath = $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php'; + $this->assertFileExists($filePath, 'OrderFlowersBaza24Bot.php must exist'); + } + + /** + * Test OrderFlowersBaza24Bot.php uses TELEGRAM_BOT_ORDERS env + */ + public function testOrdersBotUsesEnv(): void + { + $filePath = $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_ORDERS[\'"]\s*\)/', + $content, + 'OrderFlowersBaza24Bot.php must use getenv(\'TELEGRAM_BOT_ORDERS\')' + ); + } + + /** + * Test OrderFlowersBaza24Bot.php has no hardcoded token + */ + public function testOrdersBotNoHardcodedToken(): void + { + $filePath = $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/[\'"][0-9]{9,11}:AA[A-Za-z0-9_-]{30,}[\'"]/', + $content, + 'OrderFlowersBaza24Bot.php must not contain hardcoded bot token' + ); + } + + /** + * Test OrderFlowersBaza24Bot.php getenv has fallback + */ + public function testOrdersBotHasFallback(): void + { + $filePath = $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_ORDERS[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $content, + 'OrderFlowersBaza24Bot.php getenv must have empty fallback' + ); + } + + /** + * Test OrderFlowersBaza24Bot.php has PHP doc + */ + public function testOrdersBotHasPhpDoc(): void + { + $filePath = $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'OrderFlowersBaza24Bot.php must have documentation comment' + ); + } + + // ============================================================ + // telegram_alerts.php tests + // ============================================================ + + /** + * Test telegram_alerts.php exists + */ + public function testTelegramAlertsFileExists(): void + { + $filePath = $this->basePath . '/modul/config/telegram_alerts.php'; + $this->assertFileExists($filePath, 'telegram_alerts.php must exist'); + } + + /** + * Test telegram_alerts.php uses TELEGRAM_BOT_ALERTS env + */ + public function testTelegramAlertsUsesEnv(): void + { + $filePath = $this->basePath . '/modul/config/telegram_alerts.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_ALERTS[\'"]\s*\)/', + $content, + 'telegram_alerts.php must use getenv(\'TELEGRAM_BOT_ALERTS\')' + ); + } + + /** + * Test telegram_alerts.php has no hardcoded token + */ + public function testTelegramAlertsNoHardcodedToken(): void + { + $filePath = $this->basePath . '/modul/config/telegram_alerts.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/[\'"][0-9]{9,11}:AA[A-Za-z0-9_-]{30,}[\'"]/', + $content, + 'telegram_alerts.php must not contain hardcoded bot token' + ); + } + + /** + * Test telegram_alerts.php getenv has fallback + */ + public function testTelegramAlertsHasFallback(): void + { + $filePath = $this->basePath . '/modul/config/telegram_alerts.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_ALERTS[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $content, + 'telegram_alerts.php getenv must have empty fallback' + ); + } + + /** + * Test telegram_alerts.php has PHP doc + */ + public function testTelegramAlertsHasPhpDoc(): void + { + $filePath = $this->basePath . '/modul/config/telegram_alerts.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'telegram_alerts.php must have documentation comment' + ); + } + + // ============================================================ + // Cross-file consistency tests + // ============================================================ + + /** + * Test all legacy Telegram files use define() for TOKEN + */ + public function testAllFilesUseDefineForToken(): void + { + $files = [ + $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php', + $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php', + $this->basePath . '/modul/config/telegram_alerts.php', + ]; + + foreach ($files as $file) { + $content = file_get_contents($file); + $this->assertMatchesRegularExpression( + '/define\s*\(\s*[\'"]TOKEN[\'"]\s*,/', + $content, + basename($file) . ' must define TOKEN constant' + ); + } + } + + /** + * Test .env.example has all required Telegram bot variables + */ + public function testEnvExampleHasAllBotVariables(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'TELEGRAM_BOT_ALERTS=', + $envExample, + '.env.example must contain TELEGRAM_BOT_ALERTS' + ); + + $this->assertStringContainsString( + 'TELEGRAM_BOT_ORDERS=', + $envExample, + '.env.example must contain TELEGRAM_BOT_ORDERS' + ); + } + + /** + * Test no Telegram tokens in comments + */ + public function testNoTokensInComments(): void + { + $files = [ + $this->basePath . '/api1_old/telegram/bc24_alerts_bot.php', + $this->basePath . '/api1_old/telegram/OrderFlowersBaza24Bot.php', + $this->basePath . '/modul/config/telegram_alerts.php', + $this->basePath . '/modul/shipment/MessangerAdd.php', + ]; + + foreach ($files as $file) { + if (!file_exists($file)) { + continue; + } + $content = file_get_contents($file); + + // Check for tokens in comments (// or /* */) + $this->assertDoesNotMatchRegularExpression( + '/(?:\/\/|\/\*|\*)[^\n]*[0-9]{9,11}:AA[A-Za-z0-9_-]{30,}/', + $content, + basename($file) . ' must not contain bot tokens in comments' + ); + } + } +} diff --git a/erp24/tests/unit/config/MediaConfigTest.php b/erp24/tests/unit/config/MediaConfigTest.php new file mode 100644 index 00000000..8bdf1560 --- /dev/null +++ b/erp24/tests/unit/config/MediaConfigTest.php @@ -0,0 +1,237 @@ +configPath = dirname(__DIR__, 3) . '/media/config/media.config.php'; + } + + /** + * Test media.config.php file exists + */ + public function testConfigFileExists(): void + { + $this->assertFileExists( + $this->configPath, + 'media.config.php must exist' + ); + } + + /** + * Test RabbitMQ DSN uses getenv with rawurlencode for RABBIT_USER + * + * rawurlencode is required because RabbitMQ credentials may contain + * special characters that need to be URL-encoded in AMQP DSN + */ + public function testRabbitMqDsnUsesEnvUser(): void + { + $content = file_get_contents($this->configPath); + + $this->assertStringContainsString( + "rawurlencode(getenv('RABBIT_USER')", + $content, + 'RabbitMQ DSN must use rawurlencode for RABBIT_USER' + ); + } + + /** + * Test RabbitMQ DSN uses getenv with rawurlencode for RABBIT_PASSWORD + */ + public function testRabbitMqDsnUsesEnvPassword(): void + { + $content = file_get_contents($this->configPath); + + $this->assertStringContainsString( + "rawurlencode(getenv('RABBIT_PASSWORD')", + $content, + 'RabbitMQ DSN must use rawurlencode for RABBIT_PASSWORD' + ); + } + + /** + * Test RabbitMQ DSN uses getenv for RABBIT_HOST + */ + public function testRabbitMqDsnUsesEnvHost(): void + { + $content = file_get_contents($this->configPath); + + $this->assertStringContainsString( + "getenv('RABBIT_HOST')", + $content, + 'RabbitMQ DSN must use RABBIT_HOST from env' + ); + } + + /** + * Test no hardcoded RabbitMQ password + * + * Old implementation had hardcoded password in DSN string + */ + public function testNoHardcodedRabbitMqPassword(): void + { + $content = file_get_contents($this->configPath); + + // Known old password pattern (masked) + $this->assertStringNotContainsString( + '3qqHK2MRgGgxUdVT61', + $content, + 'Must not contain hardcoded RabbitMQ password' + ); + } + + /** + * Test no hardcoded RabbitMQ user + */ + public function testNoHardcodedRabbitMqUser(): void + { + $content = file_get_contents($this->configPath); + + // Check that user is not hardcoded in DSN + $this->assertDoesNotMatchRegularExpression( + "/amqp:\/\/[a-zA-Z0-9_]+:[^@]+@/", + $content, + 'RabbitMQ DSN must not have hardcoded user:password pattern' + ); + } + + /** + * Test cookieValidationKey uses getenv + */ + public function testCookieKeyUsesEnv(): void + { + $content = file_get_contents($this->configPath); + + $this->assertStringContainsString( + "getenv('COOKIE_VALIDATION_KEY')", + $content, + 'cookieValidationKey must use COOKIE_VALIDATION_KEY from env' + ); + } + + /** + * Test no hardcoded cookie validation key + * + * Old implementation had hardcoded cookie key + */ + public function testNoHardcodedCookieKey(): void + { + $content = file_get_contents($this->configPath); + + // Known old cookie key pattern + $this->assertStringNotContainsString( + 'erp24_DLVFJRBvmttertrrt_key', + $content, + 'Must not contain hardcoded cookie validation key' + ); + } + + /** + * Test RabbitMQ DSN has proper AMQP format + */ + public function testRabbitMqDsnHasProperFormat(): void + { + $content = file_get_contents($this->configPath); + + // Check DSN starts with amqp:// + $this->assertStringContainsString( + "'dsn' => 'amqp://'", + $content, + 'RabbitMQ DSN must start with amqp://' + ); + } + + /** + * Test RabbitMQ has fallback for empty credentials + */ + public function testRabbitMqHasEmptyFallback(): void + { + $content = file_get_contents($this->configPath); + + // Check that getenv has ?: '' fallback + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'RABBIT_USER'\s*\)\s*\?:\s*''/", + $content, + 'RABBIT_USER must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'RABBIT_PASSWORD'\s*\)\s*\?:\s*''/", + $content, + 'RABBIT_PASSWORD must have empty string fallback' + ); + } + + /** + * Test cookie validation key has fallback for local development + */ + public function testCookieKeyHasFallback(): void + { + $content = file_get_contents($this->configPath); + + // Check that cookieValidationKey has a fallback for local dev + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'COOKIE_VALIDATION_KEY'\s*\)\s*\?:/", + $content, + 'COOKIE_VALIDATION_KEY must have fallback for local development' + ); + } + + /** + * Test config is valid PHP syntax + */ + public function testConfigIsValidPhp(): void + { + $output = []; + $returnCode = 0; + exec("php -l {$this->configPath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'media.config.php must have valid PHP syntax: ' . implode("\n", $output) + ); + } + + /** + * Test config returns array + */ + public function testConfigReturnsArray(): void + { + // Set required env vars to prevent errors + putenv('RABBIT_USER=test'); + putenv('RABBIT_PASSWORD=test'); + putenv('RABBIT_HOST=localhost'); + putenv('COOKIE_VALIDATION_KEY=test-key'); + + $config = require $this->configPath; + + $this->assertIsArray($config, 'media.config.php must return array'); + $this->assertArrayHasKey('components', $config, 'Config must have components key'); + + // Clean up env + putenv('RABBIT_USER'); + putenv('RABBIT_PASSWORD'); + putenv('RABBIT_HOST'); + putenv('COOKIE_VALIDATION_KEY'); + } +} diff --git a/erp24/tests/unit/config/NoHardcodedSecretsTest.php b/erp24/tests/unit/config/NoHardcodedSecretsTest.php new file mode 100644 index 00000000..3d060fc4 --- /dev/null +++ b/erp24/tests/unit/config/NoHardcodedSecretsTest.php @@ -0,0 +1,356 @@ +forbiddenRegexPatterns as $pattern) { + // Find all matches + preg_match_all($pattern, $content, $matches); + + // Filter out allowed placeholders + $realSecrets = array_filter($matches[0] ?? [], function ($match) { + foreach ($this->allowedPlaceholders as $placeholder) { + if (strpos($match, $placeholder) !== false) { + return false; + } + } + return true; + }); + + $this->assertEmpty( + $realSecrets, + "Found potential hardcoded secrets in $fileName matching pattern $pattern: " . implode(', ', $realSecrets) + ); + } + } + + /** + * Check params.php for hardcoded secrets + */ + public function testParamsPhpNoHardcodedSecrets(): void + { + $file = \Yii::getAlias('@app/config/params.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $this->assertNoForbiddenPatterns($content, 'params.php'); + + // Check that getenv() is used + $this->assertStringContainsString('getenv(', $content, 'params.php should use getenv()'); + } + + /** + * Check web.php for hardcoded secrets + */ + public function testWebPhpNoHardcodedSecrets(): void + { + $file = \Yii::getAlias('@app/config/web.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $this->assertNoForbiddenPatterns($content, 'web.php'); + + // Check that cookieValidationKey uses getenv() + $this->assertStringContainsString( + "getenv('COOKIE_VALIDATION_KEY')", + $content, + 'cookieValidationKey should use getenv()' + ); + } + + /** + * Check console.php for hardcoded secrets + */ + public function testConsolePhpNoHardcodedSecrets(): void + { + $file = \Yii::getAlias('@app/config/console.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $this->assertNoForbiddenPatterns($content, 'console.php'); + } + + /** + * Check db.php for hardcoded passwords in fallback + */ + public function testDbPhpNoHardcodedPasswords(): void + { + $file = \Yii::getAlias('@app/config/db.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + // Check that fallback values are empty + $this->assertStringContainsString( + "getenv('POSTGRES_PASSWORD') ?: ''", + $content, + 'POSTGRES_PASSWORD should have empty fallback' + ); + + $this->assertStringContainsString( + "getenv('DB_PASSWORD') ?: ''", + $content, + 'DB_PASSWORD should have empty fallback' + ); + } + + /** + * Check db2.php for hardcoded passwords in fallback + */ + public function testDb2PhpNoHardcodedPasswords(): void + { + $file = \Yii::getAlias('@app/config/db2.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $this->assertStringContainsString( + "getenv('DB_PASSWORD') ?: ''", + $content, + 'DB_PASSWORD should have empty fallback in db2.php' + ); + } + + /** + * Check dev.console.config.php for hardcoded secrets + */ + public function testDevConsoleConfigNoHardcodedSecrets(): void + { + $file = \Yii::getAlias('@app/config/dev.console.config.php'); + if (!file_exists($file)) { + $this->markTestSkipped('File dev.console.config.php does not exist'); + } + + $content = file_get_contents($file); + $this->assertNoForbiddenPatterns($content, 'dev.console.config.php'); + } + + /** + * Check prod.console.config.php for hardcoded secrets + */ + public function testProdConsoleConfigNoHardcodedSecrets(): void + { + $file = \Yii::getAlias('@app/config/prod.console.config.php'); + if (!file_exists($file)) { + $this->markTestSkipped('File prod.console.config.php does not exist'); + } + + $content = file_get_contents($file); + $this->assertNoForbiddenPatterns($content, 'prod.console.config.php'); + } + + /** + * Check api2.config.php for hardcoded secrets + */ + public function testApi2ConfigNoHardcodedSecrets(): void + { + $file = \Yii::getAlias('@app/api2/config/api2.config.php'); + if (!file_exists($file)) { + $this->markTestSkipped('File api2.config.php does not exist'); + } + + $content = file_get_contents($file); + $this->assertNoForbiddenPatterns($content, 'api2.config.php'); + } + + /** + * Comprehensive check of all config files for secrets + */ + public function testAllConfigFilesNoHardcodedSecrets(): void + { + $basePath = \Yii::getAlias('@app'); + + foreach ($this->configFiles as $relPath) { + $file = $basePath . '/' . $relPath; + if (!file_exists($file)) { + continue; + } + + $content = file_get_contents($file); + $this->assertNoForbiddenPatterns($content, $relPath); + } + } + + /** + * Check TelegramService uses getenv for all secrets + */ + public function testTelegramServiceUsesGetenv(): void + { + $file = \Yii::getAlias('@app/services/TelegramService.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $requiredEnvVars = [ + 'TELEGRAM_BOT_TOKEN', + 'TELEGRAM_BOT_TOKEN_PROD', + 'TELEGRAM_CHAT_CHANNEL_ID', + 'TELEGRAM_CHAT_CHANNEL_ERP_ID', + 'CHATBOT_SALT', + ]; + + foreach ($requiredEnvVars as $var) { + $this->assertStringContainsString( + "getenv('$var')", + $content, + "TelegramService.php should use getenv('$var')" + ); + } + } + + /** + * Check LPTrackerApi uses getenv for credentials + */ + public function testLpTrackerApiUsesGetenv(): void + { + $file = \Yii::getAlias('@app/records/LPTrackerApi.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $this->assertStringContainsString( + "getenv('LPTRACKER_LOGIN')", + $content, + "LPTrackerApi.php should use getenv('LPTRACKER_LOGIN')" + ); + + $this->assertStringContainsString( + "getenv('LPTRACKER_PASSWORD')", + $content, + "LPTrackerApi.php should use getenv('LPTRACKER_PASSWORD')" + ); + } + + /** + * Check CloudPayments files use getenv for credentials + */ + public function testCloudPaymentsFilesUseGetenv(): void + { + $basePath = \Yii::getAlias('@app'); + $files = [ + 'inc/cloudpayments.php', + 'modul/api/cloudpayments.php', + 'modul/collation/cloudpayments.php', + ]; + + foreach ($files as $relPath) { + $file = $basePath . '/' . $relPath; + if (!file_exists($file)) { + continue; + } + + $content = file_get_contents($file); + $this->assertStringContainsString( + "getenv('CLOUDPAYMENTS_PUBLIC_ID')", + $content, + "$relPath should use getenv('CLOUDPAYMENTS_PUBLIC_ID')" + ); + } + } + + /** + * Check MarketplaceService uses getenv for email credentials + */ + public function testMarketplaceServiceUsesGetenv(): void + { + $file = \Yii::getAlias('@app/services/MarketplaceService.php'); + $this->assertFileExists($file); + $content = file_get_contents($file); + + $this->assertStringContainsString( + "getenv('EMAIL_ZAKAZ_PASSWORD')", + $content, + "MarketplaceService.php should use getenv('EMAIL_ZAKAZ_PASSWORD')" + ); + + $this->assertStringContainsString( + "getenv('EMAIL_FLOW_PASSWORD')", + $content, + "MarketplaceService.php should use getenv('EMAIL_FLOW_PASSWORD')" + ); + } + + /** + * Check all service files use getenv pattern + */ + public function testAllServiceFilesUseGetenv(): void + { + $basePath = \Yii::getAlias('@app'); + $errors = []; + + foreach ($this->serviceFiles as $relPath) { + $file = $basePath . '/' . $relPath; + if (!file_exists($file)) { + continue; + } + + $content = file_get_contents($file); + if (strpos($content, 'getenv(') === false) { + $errors[] = "$relPath does not use getenv()"; + } + } + + $this->assertEmpty($errors, "Files not using getenv():\n" . implode("\n", $errors)); + } +} diff --git a/erp24/tests/unit/config/ParamsConfigTest.php b/erp24/tests/unit/config/ParamsConfigTest.php new file mode 100644 index 00000000..23644df3 --- /dev/null +++ b/erp24/tests/unit/config/ParamsConfigTest.php @@ -0,0 +1,167 @@ +params; + + $this->assertIsArray($params); + $this->assertNotEmpty($params); + } + + /** + * Verifies TELEGRAM_API_URL configuration + */ + public function testTelegramApiUrlConfigured(): void + { + $telegramUrl = \Yii::$app->params['TELEGRAM_API_URL'] ?? null; + + $this->assertNotNull($telegramUrl, 'TELEGRAM_API_URL must be configured'); + $this->assertStringStartsWith( + 'https://api.telegram.org/bot', + $telegramUrl, + 'TELEGRAM_API_URL must start with https://api.telegram.org/bot' + ); + + // Verify token is loaded from .env + $token = getenv('TELEGRAM_BOT_TOKEN'); + if ($token) { + $this->assertStringContainsString( + $token, + $telegramUrl, + 'TELEGRAM_API_URL must contain token from TELEGRAM_BOT_TOKEN' + ); + } + } + + /** + * Verifies WHATSAPP_API_KEY configuration (optional) + * Key may be empty in test/dev environments + */ + public function testWhatsAppApiKeyConfigured(): void + { + $apiKey = \Yii::$app->params['WHATSAPP_API_KEY'] ?? null; + + // Key must exist in params (but can be empty) + $this->assertArrayHasKey('WHATSAPP_API_KEY', \Yii::$app->params, 'WHATSAPP_API_KEY must be defined in params'); + + // If env var is set, verify it matches params + $envKey = getenv('WHATSAPP_API_KEY'); + if ($envKey) { + $this->assertEquals($envKey, $apiKey, 'WHATSAPP_API_KEY must match env variable'); + } + } + + /** + * Verifies YANDEX_MARKET_API_KEY configuration + */ + public function testYandexMarketApiKeyConfigured(): void + { + $apiKey = \Yii::$app->params['YANDEX_MARKET_API_KEY'] ?? null; + + $this->assertNotNull($apiKey, 'YANDEX_MARKET_API_KEY must be configured'); + + // Verify it matches env var if set + $envKey = getenv('YANDEX_MARKET_API_KEY'); + if ($envKey) { + $this->assertEquals($envKey, $apiKey, 'YANDEX_MARKET_API_KEY must match env variable'); + } + } + + /** + * Verifies SWITCH_USER_COOKIE_PASSWORD configuration + */ + public function testSwitchUserPasswordConfigured(): void + { + $password = \Yii::$app->params['SWITCH_USER_COOKIE_PASSWORD'] ?? null; + + $this->assertNotNull($password, 'SWITCH_USER_COOKIE_PASSWORD must be configured'); + + // Verify it matches env var if set + $envPassword = getenv('SWITCH_USER_COOKIE_PASSWORD'); + if ($envPassword) { + $this->assertEquals($envPassword, $password, 'SWITCH_USER_COOKIE_PASSWORD must match env variable'); + } + } + + /** + * Verifies CAMERAS configuration structure (optional values) + * Cameras may have empty credentials in test/dev environments + */ + public function testCamerasConfigured(): void + { + $cameras = \Yii::$app->params['CAMERAS'] ?? null; + + $this->assertNotNull($cameras, 'CAMERAS must be configured'); + $this->assertIsArray($cameras, 'CAMERAS must be an array'); + $this->assertCount(5, $cameras, 'Must have 5 cameras'); + + foreach ($cameras as $index => $camera) { + // Structure must exist + $this->assertArrayHasKey('Login', $camera, "Camera $index must have Login key"); + $this->assertArrayHasKey('Password', $camera, "Camera $index must have Password key"); + // Values can be empty in test environment - only check structure + } + + // If env vars are set, verify they match + $camera1Login = getenv('CAMERA_1_LOGIN'); + if ($camera1Login) { + $this->assertEquals($camera1Login, $cameras[0]['Login'], 'Camera 1 Login must match CAMERA_1_LOGIN env var'); + } + } + + /** + * Verifies RABBIT_HOST configuration + */ + public function testRabbitHostConfigured(): void + { + $rabbitHost = \Yii::$app->params['RABBIT_HOST'] ?? null; + + $this->assertNotNull($rabbitHost, 'RABBIT_HOST must be configured in params'); + + // Verify it matches env var if set + $envHost = getenv('RABBIT_HOST'); + if ($envHost) { + $this->assertEquals($envHost, $rabbitHost, 'RABBIT_HOST must match env variable'); + } + } + + /** + * Verifies API2_URL configuration + */ + public function testApi2UrlConfigured(): void + { + $api2Url = \Yii::$app->params['API2_URL'] ?? null; + + $this->assertNotNull($api2Url, 'API2_URL must be configured'); + $this->assertStringStartsWith('http', $api2Url, 'API2_URL must start with http'); + } + + /** + * Verifies TELEGRAM_WEBHOOK_URL configuration + */ + public function testTelegramWebhookUrlConfigured(): void + { + $webhookUrl = \Yii::$app->params['TELEGRAM_WEBHOOK_URL'] ?? null; + + $this->assertNotNull($webhookUrl, 'TELEGRAM_WEBHOOK_URL must be configured'); + $this->assertStringStartsWith('https://', $webhookUrl, 'TELEGRAM_WEBHOOK_URL must start with https://'); + $this->assertStringContainsString('/telegram/webhook', $webhookUrl, 'TELEGRAM_WEBHOOK_URL must contain /telegram/webhook'); + } +} diff --git a/erp24/tests/unit/config/RabbitMqConfigTest.php b/erp24/tests/unit/config/RabbitMqConfigTest.php new file mode 100644 index 00000000..fbd54f83 --- /dev/null +++ b/erp24/tests/unit/config/RabbitMqConfigTest.php @@ -0,0 +1,169 @@ + 'admin', 'password' => 'password123', 'host' => 'localhost'], + // Credentials with @ symbol + ['user' => 'admin', 'password' => 'pass@word', 'host' => 'rabbitmq'], + // Credentials with : symbol + ['user' => 'admin', 'password' => 'pass:word', 'host' => 'rabbitmq'], + // Credentials with / symbol + ['user' => 'admin', 'password' => 'pass/word', 'host' => 'rabbitmq'], + // Credentials with multiple special characters + ['user' => 'admin', 'password' => 'p@ss:w/rd!#$', 'host' => 'localhost'], + // Empty credentials (edge case) + ['user' => '', 'password' => '', 'host' => 'localhost'], + ]; + + foreach ($testCases as $index => $case) { + $dsn = $this->buildRabbitMqDsn($case['user'], $case['password'], $case['host']); + + // Verify DSN format is valid AMQP URL + $this->assertStringStartsWith('amqp://', $dsn, "Case $index: DSN must start with amqp://"); + + // Verify URL-encoded credentials don't break the URL structure + $this->assertStringContainsString('@', $dsn, "Case $index: DSN must contain @ separator"); + $this->assertStringContainsString(':5672', $dsn, "Case $index: DSN must contain port"); + + // Verify special characters are URL-encoded + if (strpos($case['password'], '@') !== false) { + $this->assertStringNotContainsString('@@', $dsn, "Case $index: @ in password must be encoded"); + } + if (strpos($case['password'], ':') !== false) { + // The password part should have encoded colons + $encodedPassword = rawurlencode($case['password']); + $this->assertStringContainsString($encodedPassword, $dsn, "Case $index: Password must be URL-encoded"); + } + } + } + + /** + * Test that DSN can be parsed correctly after URL encoding + */ + public function testRabbitMqDsnCanBeParsed(): void + { + $user = 'admin'; + $password = 'p@ss:w/rd!'; + $host = 'rabbitmq-host'; + + $dsn = $this->buildRabbitMqDsn($user, $password, $host); + + // Parse the DSN using parse_url + $parsed = parse_url($dsn); + + $this->assertEquals('amqp', $parsed['scheme'], 'Scheme must be amqp'); + $this->assertEquals($user, urldecode($parsed['user']), 'Username must be decodable'); + $this->assertEquals($password, urldecode($parsed['pass']), 'Password must be decodable'); + $this->assertEquals($host, $parsed['host'], 'Host must match'); + $this->assertEquals(5672, $parsed['port'], 'Port must be 5672'); + } + + /** + * Test that empty credentials result in valid DSN + */ + public function testRabbitMqDsnWithEmptyCredentials(): void + { + $dsn = $this->buildRabbitMqDsn('', '', 'localhost'); + + // Even with empty credentials, DSN should be syntactically valid + $this->assertStringStartsWith('amqp://', $dsn); + $this->assertStringContainsString(':@localhost:5672', $dsn); + } + + /** + * Test RabbitMQ host from environment variable + */ + public function testRabbitHostFromEnv(): void + { + $rabbitHost = getenv('RABBIT_HOST'); + + if ($rabbitHost === false) { + $this->markTestSkipped('RABBIT_HOST env var not set'); + } + + $this->assertNotEmpty($rabbitHost, 'RABBIT_HOST must not be empty'); + + // Verify it's a valid hostname or container name + $this->assertMatchesRegularExpression( + '/^[a-zA-Z0-9]([a-zA-Z0-9\-_.]*[a-zA-Z0-9])?$/', + $rabbitHost, + 'RABBIT_HOST must be a valid hostname' + ); + } + + /** + * Test that queue component is configured in Yii app (if available) + */ + public function testQueueComponentExists(): void + { + if (!\Yii::$app || !\Yii::$app->has('queue')) { + $this->markTestSkipped('Yii::$app->queue not initialized in test environment'); + } + + $queue = \Yii::$app->queue; + $this->assertNotNull($queue, 'Queue component must exist'); + } + + /** + * Test queue DSN uses environment variables + * + * @group integration + */ + public function testQueueDsnUsesEnvVariables(): void + { + if (!\Yii::$app || !\Yii::$app->has('queue')) { + $this->markTestSkipped('Yii::$app->queue not initialized'); + } + + $queue = \Yii::$app->queue; + $dsn = $queue->dsn ?? null; + + if ($dsn === null) { + $this->markTestSkipped('Queue DSN not accessible'); + } + + // Verify DSN format + $this->assertStringStartsWith('amqp://', $dsn, 'Queue DSN must use AMQP protocol'); + + // Check that credentials from env are used + $rabbitUser = getenv('RABBIT_USER'); + if ($rabbitUser !== false && !empty($rabbitUser)) { + $this->assertStringContainsString( + rawurlencode($rabbitUser), + $dsn, + 'Queue DSN must contain URL-encoded RABBIT_USER' + ); + } + } + + /** + * Helper method to build RabbitMQ DSN string + * Replicates the logic from config files + */ + private function buildRabbitMqDsn(string $user, string $password, string $host): string + { + return 'amqp://' . rawurlencode($user) . ':' . rawurlencode($password) . '@' . $host . ':5672'; + } +} diff --git a/erp24/tests/unit/config/StartupConfigSecretsTest.php b/erp24/tests/unit/config/StartupConfigSecretsTest.php new file mode 100644 index 00000000..6a170655 --- /dev/null +++ b/erp24/tests/unit/config/StartupConfigSecretsTest.php @@ -0,0 +1,433 @@ +basePath = dirname(__DIR__, 3); + } + + // ============================================================ + // startup.php tests + // ============================================================ + + /** + * Test startup.php exists + */ + public function testStartupFileExists(): void + { + $filePath = $this->basePath . '/startup.php'; + $this->assertFileExists($filePath, 'startup.php must exist'); + } + + /** + * Test startup.php uses API_TOKEN env + */ + public function testStartupUsesApiTokenEnv(): void + { + $filePath = $this->basePath . '/startup.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]API_TOKEN[\'"]\s*\)/', + $content, + 'startup.php must use getenv(\'API_TOKEN\')' + ); + } + + /** + * Test startup.php uses TOKEN_CLOUD env + */ + public function testStartupUsesTokenCloudEnv(): void + { + $filePath = $this->basePath . '/startup.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TOKEN_CLOUD[\'"]\s*\)/', + $content, + 'startup.php must use getenv(\'TOKEN_CLOUD\')' + ); + } + + /** + * Test startup.php uses AMO_SECRET_PHRASE env + */ + public function testStartupUsesAmoSecretPhraseEnv(): void + { + $filePath = $this->basePath . '/startup.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_SECRET_PHRASE[\'"]\s*\)/', + $content, + 'startup.php must use getenv(\'AMO_SECRET_PHRASE\')' + ); + } + + /** + * Test startup.php has no hardcoded API token + */ + public function testStartupNoHardcodedApiToken(): void + { + $filePath = $this->basePath . '/startup.php'; + $content = file_get_contents($filePath); + + // Known leaked token pattern + $this->assertDoesNotMatchRegularExpression( + '/[\'"]getJH6GFi4tpU84YVPW9M/', + $content, + 'startup.php must not contain hardcoded API token' + ); + } + + /** + * Test startup.php has no hardcoded TOKEN_CLOUD + */ + public function testStartupNoHardcodedTokenCloud(): void + { + $filePath = $this->basePath . '/startup.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/[\'"]iC04295J9HyD2H3GJF3btky[\'"]/', + $content, + 'startup.php must not contain hardcoded TOKEN_CLOUD' + ); + } + + /** + * Test startup.php has PHP doc + */ + public function testStartupHasPhpDoc(): void + { + $filePath = $this->basePath . '/startup.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'startup.php must have documentation comment' + ); + } + + // ============================================================ + // delivery.php tests + // ============================================================ + + /** + * Test delivery.php exists + */ + public function testDeliveryFileExists(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $this->assertFileExists($filePath, 'modul/orders/delivery.php must exist'); + } + + /** + * Test delivery.php uses API_TOKEN env + */ + public function testDeliveryUsesApiTokenEnv(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]API_TOKEN[\'"]\s*\)/', + $content, + 'delivery.php must use getenv(\'API_TOKEN\')' + ); + } + + /** + * Test delivery.php uses TOKEN_CLOUD env + */ + public function testDeliveryUsesTokenCloudEnv(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TOKEN_CLOUD[\'"]\s*\)/', + $content, + 'delivery.php must use getenv(\'TOKEN_CLOUD\')' + ); + } + + /** + * Test delivery.php uses AMO_SECRET_PHRASE env + */ + public function testDeliveryUsesAmoSecretPhraseEnv(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]AMO_SECRET_PHRASE[\'"]\s*\)/', + $content, + 'delivery.php must use getenv(\'AMO_SECRET_PHRASE\')' + ); + } + + /** + * Test delivery.php has no hardcoded tokens in URL + */ + public function testDeliveryNoHardcodedTokensInUrl(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $content = file_get_contents($filePath); + + // Check that URL doesn't contain hardcoded tokens + $this->assertDoesNotMatchRegularExpression( + '/key=getJH6GFi4tpU84YVPW9M/', + $content, + 'delivery.php must not contain hardcoded API key in URL' + ); + + $this->assertDoesNotMatchRegularExpression( + '/token_cloud=iC04295J9HyD2H3GJF3btky/', + $content, + 'delivery.php must not contain hardcoded TOKEN_CLOUD in URL' + ); + + $this->assertDoesNotMatchRegularExpression( + '/secret_phrase=VJJVkt467ltuXU/', + $content, + 'delivery.php must not contain hardcoded secret_phrase in URL' + ); + } + + /** + * Test delivery.php uses urlencode for tokens + */ + public function testDeliveryUsesUrlEncodeForTokens(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'urlencode($apiToken)', + $content, + 'delivery.php must use urlencode for API token' + ); + } + + /** + * Test delivery.php has PHP doc + */ + public function testDeliveryHasPhpDoc(): void + { + $filePath = $this->basePath . '/modul/orders/delivery.php'; + $content = file_get_contents($filePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'delivery.php must have documentation comment' + ); + } + + // ============================================================ + // inc/db.php tests + // ============================================================ + + /** + * Test inc/db.php has no hardcoded passwords + */ + public function testIncDbNoHardcodedPasswords(): void + { + $filePath = $this->basePath . '/inc/db.php'; + $content = file_get_contents($filePath); + + // Known leaked passwords + $leakedPasswords = [ + 'H7s0Y1v0', + 'I0v6Y3p8', + 'JVJruro_Xdg456o3ir', + ]; + + foreach ($leakedPasswords as $password) { + $this->assertStringNotContainsString( + $password, + $content, + "inc/db.php must not contain leaked password: {$password}" + ); + } + } + + /** + * Test inc/db.php has no commented out credentials + */ + public function testIncDbNoCommentedCredentials(): void + { + $filePath = $this->basePath . '/inc/db.php'; + $content = file_get_contents($filePath); + + // Should not have define('DB_PASS', ...) even in comments + $this->assertDoesNotMatchRegularExpression( + '/define\s*\(\s*[\'"]DB_PASS[\'"]\s*,/', + $content, + 'inc/db.php must not contain DB_PASS define (even in comments)' + ); + } + + /** + * Test inc/db.php uses POSTGRES_PASSWORD env + */ + public function testIncDbUsesPostgresPasswordEnv(): void + { + $filePath = $this->basePath . '/inc/db.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]POSTGRES_PASSWORD[\'"]\s*\)/', + $content, + 'inc/db.php must use getenv(\'POSTGRES_PASSWORD\')' + ); + } + + /** + * Test inc/db.php has empty fallback for password + */ + public function testIncDbHasEmptyPasswordFallback(): void + { + $filePath = $this->basePath . '/inc/db.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]POSTGRES_PASSWORD[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $content, + 'POSTGRES_PASSWORD getenv must have empty fallback' + ); + } + + /** + * Test inc/db.php has conditional initialization + */ + public function testIncDbHasConditionalInit(): void + { + $filePath = $this->basePath . '/inc/db.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/if\s*\(\s*!empty\s*\(\s*\$config\[.DB_USER.\]\s*\)/', + $content, + 'inc/db.php must check for DB_USER before initializing' + ); + } + + // ============================================================ + // crmconf.php tests + // ============================================================ + + /** + * Test crmconf.php uses SHOP_DELETE_PASSWORD env + */ + public function testCrmconfUsesShopDeletePasswordEnv(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]SHOP_DELETE_PASSWORD[\'"]\s*\)/', + $content, + 'crmconf.php must use getenv(\'SHOP_DELETE_PASSWORD\')' + ); + } + + /** + * Test crmconf.php has no hardcoded pass_dell_shop + */ + public function testCrmconfNoHardcodedPassDellShop(): void + { + $filePath = $this->basePath . '/inc/crmconf.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/\[.pass_dell_shop.\]\s*=\s*["\']Olidoell341["\']/', + $content, + 'crmconf.php must not contain hardcoded pass_dell_shop' + ); + } + + // ============================================================ + // .env.example tests + // ============================================================ + + /** + * Test .env.example has SHOP_DELETE_PASSWORD + */ + public function testEnvExampleHasShopDeletePassword(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'SHOP_DELETE_PASSWORD=', + $envExample, + '.env.example must contain SHOP_DELETE_PASSWORD' + ); + } + + /** + * Test .env.example has API_TOKEN + */ + public function testEnvExampleHasApiToken(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'API_TOKEN=', + $envExample, + '.env.example must contain API_TOKEN' + ); + } + + /** + * Test .env.example has TOKEN_CLOUD + */ + public function testEnvExampleHasTokenCloud(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'TOKEN_CLOUD=', + $envExample, + '.env.example must contain TOKEN_CLOUD' + ); + } + + /** + * Test no known leaked tokens in api_text.php documentation + */ + public function testApiTextNoLeakedTokens(): void + { + $filePath = $this->basePath . '/modul/api/api_text.php'; + $content = file_get_contents($filePath); + + $this->assertDoesNotMatchRegularExpression( + '/getJH6GFi4tpU84YVPW9M/', + $content, + 'api_text.php must not contain leaked API token in documentation' + ); + } +} diff --git a/erp24/tests/unit/controllers/TelegramControllerConfigTest.php b/erp24/tests/unit/controllers/TelegramControllerConfigTest.php new file mode 100644 index 00000000..df951b32 --- /dev/null +++ b/erp24/tests/unit/controllers/TelegramControllerConfigTest.php @@ -0,0 +1,172 @@ +controllerPath = dirname(__DIR__, 3) . '/api2/controllers/TelegramController.php'; + } + + /** + * Test TelegramController file exists + */ + public function testControllerFileExists(): void + { + $this->assertFileExists( + $this->controllerPath, + 'TelegramController.php must exist' + ); + } + + /** + * Test TelegramController uses TELEGRAM_BOT_TOKEN from env + */ + public function testControllerUsesEnvToken(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + "getenv('TELEGRAM_BOT_TOKEN')", + $content, + 'TelegramController must use TELEGRAM_BOT_TOKEN from env' + ); + } + + /** + * Test no hardcoded Telegram tokens in controller + * + * Telegram bot tokens have format: 123456789:ABCdefGHI_jklMNOpqrsTUVwxyz12345 + * We check that no such patterns exist in the code + */ + public function testNoHardcodedTokens(): void + { + $content = file_get_contents($this->controllerPath); + + // Check for old hardcoded token patterns (9+ digits : 35 alphanumeric chars) + $this->assertDoesNotMatchRegularExpression( + '/\d{9,}:[A-Za-z0-9_-]{35}/', + $content, + 'TelegramController must not contain hardcoded bot tokens' + ); + } + + /** + * Test static $API_URL property is removed + * + * Old implementation had: private static $API_URL = "https://api.telegram.org/bot..."; + * New implementation uses getApiUrl() method + */ + public function testNoStaticApiUrlProperty(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringNotContainsString( + 'private static $API_URL', + $content, + 'Static $API_URL property should be removed (use getApiUrl() method instead)' + ); + } + + /** + * Test getApiUrl() method exists + */ + public function testGetApiUrlMethodExists(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + 'private static function getApiUrl()', + $content, + 'getApiUrl() method must exist' + ); + } + + /** + * Test getApiUrl() returns correct Telegram API base URL + */ + public function testGetApiUrlReturnsCorrectBaseUrl(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + "return 'https://api.telegram.org/bot'", + $content, + 'getApiUrl() must return Telegram API URL base' + ); + } + + /** + * Test init() method exists and initializes botToken from env + */ + public function testInitMethodSetsBotToken(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + 'public function init()', + $content, + 'init() method must exist' + ); + + $this->assertStringContainsString( + "\$this->botToken = getenv('TELEGRAM_BOT_TOKEN')", + $content, + 'init() must set botToken from env' + ); + } + + /** + * Test botToken property is declared + */ + public function testBotTokenPropertyExists(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + 'public string $botToken', + $content, + 'botToken property must be declared as public string' + ); + } + + /** + * Test controller doesn't contain old hardcoded token values + */ + public function testNoKnownHardcodedTokenPatterns(): void + { + $content = file_get_contents($this->controllerPath); + + // Check for specific known old token prefixes (masked) + $oldTokenPatterns = [ + '8063257458:', // old dev token prefix + '5456741805:', // old prod token prefix + ]; + + foreach ($oldTokenPatterns as $pattern) { + $this->assertStringNotContainsString( + $pattern, + $content, + "Controller must not contain old hardcoded token pattern: $pattern" + ); + } + } +} diff --git a/erp24/tests/unit/controllers/TelegramSalebotControllerConfigTest.php b/erp24/tests/unit/controllers/TelegramSalebotControllerConfigTest.php new file mode 100644 index 00000000..747eed0f --- /dev/null +++ b/erp24/tests/unit/controllers/TelegramSalebotControllerConfigTest.php @@ -0,0 +1,199 @@ +controllerPath = dirname(__DIR__, 3) . '/api2/controllers/TelegramSalebotController.php'; + } + + /** + * Test TelegramSalebotController file exists + */ + public function testControllerFileExists(): void + { + $this->assertFileExists( + $this->controllerPath, + 'TelegramSalebotController.php must exist' + ); + } + + /** + * Test controller uses TELEGRAM_BOT_TOKEN_SALEBOT from env + */ + public function testControllerUsesEnvToken(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + "getenv('TELEGRAM_BOT_TOKEN_SALEBOT')", + $content, + 'TelegramSalebotController must use TELEGRAM_BOT_TOKEN_SALEBOT from env' + ); + } + + /** + * Test no hardcoded Telegram tokens in controller + * + * Telegram bot tokens have format: 123456789:ABCdefGHI_jklMNOpqrsTUVwxyz12345 + * We check that no such patterns exist in the code + */ + public function testNoHardcodedTokens(): void + { + $content = file_get_contents($this->controllerPath); + + // Check for hardcoded token patterns (9+ digits : 35 alphanumeric chars) + $this->assertDoesNotMatchRegularExpression( + '/\d{9,}:[A-Za-z0-9_-]{35}/', + $content, + 'TelegramSalebotController must not contain hardcoded bot tokens' + ); + } + + /** + * Test static $TOKEN property is removed + * + * Old implementation had: private static $TOKEN = "..."; + * New implementation uses getToken() method + */ + public function testNoStaticTokenProperty(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringNotContainsString( + 'private static $TOKEN', + $content, + 'Static $TOKEN property should be removed (use getToken() method instead)' + ); + } + + /** + * Test static $API_URL property is removed + * + * Old implementation had: private static $API_URL = "https://api.telegram.org/bot..."; + * New implementation uses getApiUrl() method + */ + public function testNoStaticApiUrlProperty(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringNotContainsString( + 'private static $API_URL', + $content, + 'Static $API_URL property should be removed (use getApiUrl() method instead)' + ); + } + + /** + * Test getToken() method exists + */ + public function testGetTokenMethodExists(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + 'private static function getToken()', + $content, + 'getToken() method must exist' + ); + } + + /** + * Test getApiUrl() method exists + */ + public function testGetApiUrlMethodExists(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + 'private static function getApiUrl()', + $content, + 'getApiUrl() method must exist' + ); + } + + /** + * Test getApiUrl() constructs URL using getToken() + */ + public function testGetApiUrlUsesGetToken(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + 'self::getToken()', + $content, + 'getApiUrl() must use self::getToken() to construct URL' + ); + } + + /** + * Test getApiUrl() returns correct Telegram API base URL + */ + public function testGetApiUrlReturnsCorrectBaseUrl(): void + { + $content = file_get_contents($this->controllerPath); + + $this->assertStringContainsString( + "'https://api.telegram.org/bot'", + $content, + 'getApiUrl() must use Telegram API base URL' + ); + } + + /** + * Test controller doesn't contain old hardcoded token values + */ + public function testNoKnownHardcodedTokenPatterns(): void + { + $content = file_get_contents($this->controllerPath); + + // Check for specific known old token prefixes (masked for security) + $oldTokenPatterns = [ + '8063257458:', // old dev token prefix + '5456741805:', // old prod token prefix + '7654321098:', // other known patterns + ]; + + foreach ($oldTokenPatterns as $pattern) { + $this->assertStringNotContainsString( + $pattern, + $content, + "Controller must not contain old hardcoded token pattern: $pattern" + ); + } + } + + /** + * Test getToken() returns empty string as fallback (not null or throws) + */ + public function testGetTokenHasEmptyStringFallback(): void + { + $content = file_get_contents($this->controllerPath); + + // Check that getToken has ?: '' fallback for missing env var + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'TELEGRAM_BOT_TOKEN_SALEBOT'\s*\)\s*\?:\s*''/", + $content, + 'getToken() must have empty string fallback for missing env var' + ); + } +} diff --git a/erp24/tests/unit/models/UsersFilterTelegramUsersForSendingTest.php b/erp24/tests/unit/models/UsersFilterTelegramUsersForSendingTest.php new file mode 100644 index 00000000..f3baae73 --- /dev/null +++ b/erp24/tests/unit/models/UsersFilterTelegramUsersForSendingTest.php @@ -0,0 +1,44 @@ + KogortStopListFixture::class, + ]; + } + + public function testFilterTelegramUsersForSending() + { + $telegramUsers = [ + ['phone' => '79990000001', 'chat_id' => 1], // в стоп-листе + ['phone' => '79990000002', 'chat_id' => 2], // в стоп-листе + ['phone' => '79990000003', 'chat_id' => 3], // не в стоп-листе + ['phone' => '79990000004', 'chat_id' => 4], // не в стоп-листе, но отправлен ранее + ]; + + $sentStatusKogort = ['79990000004']; + + $filtered = Users::filterTelegramUsersForSending($telegramUsers, $sentStatusKogort); + + $phones = array_values(array_map(function($u){ return $u['phone']; }, $filtered)); + + $this->assertEquals(['79990000003'], $phones); + } +} + + + + + + + + + + diff --git a/erp24/tests/unit/records/LPTrackerApiSecretsTest.php b/erp24/tests/unit/records/LPTrackerApiSecretsTest.php new file mode 100644 index 00000000..63dae5ab --- /dev/null +++ b/erp24/tests/unit/records/LPTrackerApiSecretsTest.php @@ -0,0 +1,191 @@ +recordPath = dirname(__DIR__, 3) . '/records/LPTrackerApi.php'; + } + + /** + * Test LPTrackerApi.php file exists + */ + public function testRecordFileExists(): void + { + $this->assertFileExists( + $this->recordPath, + 'LPTrackerApi.php must exist' + ); + } + + /** + * Test config is valid PHP syntax + */ + public function testRecordIsValidPhp(): void + { + $output = []; + $returnCode = 0; + exec("php -l {$this->recordPath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'LPTrackerApi.php must have valid PHP syntax: ' . implode("\n", $output) + ); + } + + /** + * Test LPTRACKER_LOGIN uses getenv + */ + public function testLoginUsesEnv(): void + { + $content = file_get_contents($this->recordPath); + + $this->assertStringContainsString( + "getenv('LPTRACKER_LOGIN')", + $content, + 'LPTrackerApi must use LPTRACKER_LOGIN from env' + ); + } + + /** + * Test LPTRACKER_PASSWORD uses getenv + */ + public function testPasswordUsesEnv(): void + { + $content = file_get_contents($this->recordPath); + + $this->assertStringContainsString( + "getenv('LPTRACKER_PASSWORD')", + $content, + 'LPTrackerApi must use LPTRACKER_PASSWORD from env' + ); + } + + /** + * Test no hardcoded LOGIN constant + */ + public function testNoHardcodedLoginConstant(): void + { + $content = file_get_contents($this->recordPath); + + // Check that LOGIN is not a const with literal value + $this->assertDoesNotMatchRegularExpression( + '/const\s+LOGIN\s*=\s*[\'"][^\'"]+[\'"];/', + $content, + 'Must not have hardcoded LOGIN constant' + ); + } + + /** + * Test no hardcoded PASSWORD constant + */ + public function testNoHardcodedPasswordConstant(): void + { + $content = file_get_contents($this->recordPath); + + // Check that PASSWORD is not a const with literal value + $this->assertDoesNotMatchRegularExpression( + '/const\s+PASSWORD\s*=\s*[\'"][^\'"]+[\'"];/', + $content, + 'Must not have hardcoded PASSWORD constant' + ); + } + + /** + * Test private getter methods exist + */ + public function testPrivateGetterMethodsExist(): void + { + $content = file_get_contents($this->recordPath); + + $this->assertStringContainsString( + 'private static function getLogin()', + $content, + 'getLogin() method must exist' + ); + + $this->assertStringContainsString( + 'private static function getPassword()', + $content, + 'getPassword() method must exist' + ); + } + + /** + * Test getenv calls have empty string fallback + */ + public function testGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->recordPath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'LPTRACKER_LOGIN'\s*\)\s*\?:\s*''/", + $content, + 'LPTRACKER_LOGIN must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'LPTRACKER_PASSWORD'\s*\)\s*\?:\s*''/", + $content, + 'LPTRACKER_PASSWORD must have empty string fallback' + ); + } + + /** + * Test auth method uses getLogin and getPassword + */ + public function testAuthUsesGetterMethods(): void + { + $content = file_get_contents($this->recordPath); + + $this->assertStringContainsString( + 'self::getLogin()', + $content, + 'auth() must use self::getLogin()' + ); + + $this->assertStringContainsString( + 'self::getPassword()', + $content, + 'auth() must use self::getPassword()' + ); + } + + /** + * Test PHPDoc documentation exists + */ + public function testPhpDocExists(): void + { + $content = file_get_contents($this->recordPath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + } +} diff --git a/erp24/tests/unit/services/MarketplaceServiceSecretsTest.php b/erp24/tests/unit/services/MarketplaceServiceSecretsTest.php new file mode 100644 index 00000000..a5679d6a --- /dev/null +++ b/erp24/tests/unit/services/MarketplaceServiceSecretsTest.php @@ -0,0 +1,180 @@ +servicePath = dirname(__DIR__, 3) . '/services/MarketplaceService.php'; + } + + /** + * Test MarketplaceService.php file exists + */ + public function testServiceFileExists(): void + { + $this->assertFileExists( + $this->servicePath, + 'MarketplaceService.php must exist' + ); + } + + /** + * Test config is valid PHP syntax + */ + public function testServiceIsValidPhp(): void + { + $output = []; + $returnCode = 0; + exec("php -l {$this->servicePath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'MarketplaceService.php must have valid PHP syntax: ' . implode("\n", $output) + ); + } + + /** + * Test EMAIL_ZAKAZ_PASSWORD uses getenv + */ + public function testEmailZakazPasswordUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('EMAIL_ZAKAZ_PASSWORD')", + $content, + 'MarketplaceService must use EMAIL_ZAKAZ_PASSWORD from env' + ); + } + + /** + * Test EMAIL_FLOW_PASSWORD uses getenv + */ + public function testEmailFlowPasswordUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('EMAIL_FLOW_PASSWORD')", + $content, + 'MarketplaceService must use EMAIL_FLOW_PASSWORD from env' + ); + } + + /** + * Test no hardcoded email passwords + */ + public function testNoHardcodedEmailPasswords(): void + { + $content = file_get_contents($this->servicePath); + + // Check that $password is not assigned a literal string directly + // Should be from getenv + $this->assertDoesNotMatchRegularExpression( + '/\$password\s*=\s*[\'"][a-zA-Z0-9!@#$%^&*()_+-=]{8,}[\'"];/', + $content, + 'MarketplaceService must not have hardcoded email passwords' + ); + } + + /** + * Test getenv has empty string fallback for EMAIL_ZAKAZ_PASSWORD + */ + public function testEmailZakazPasswordHasEmptyFallback(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'EMAIL_ZAKAZ_PASSWORD'\s*\)\s*\?:\s*''/", + $content, + 'EMAIL_ZAKAZ_PASSWORD must have empty string fallback' + ); + } + + /** + * Test getenv has empty string fallback for EMAIL_FLOW_PASSWORD + */ + public function testEmailFlowPasswordHasEmptyFallback(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'EMAIL_FLOW_PASSWORD'\s*\)\s*\?:\s*''/", + $content, + 'EMAIL_FLOW_PASSWORD must have empty string fallback' + ); + } + + /** + * Test alternate mailbox handling exists + */ + public function testAlternateMailboxHandling(): void + { + $content = file_get_contents($this->servicePath); + + // Check that useAlternateMailbox condition exists + $this->assertStringContainsString( + '$useAlternateMailbox', + $content, + 'MarketplaceService must handle alternate mailbox' + ); + } + + /** + * Test email usernames are present + */ + public function testEmailUsernamesPresent(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + 'Zakaz-bazacvetov24@yandex.ru', + $content, + 'Zakaz email username must be present' + ); + + $this->assertStringContainsString( + 'flow@bazacvetov24.ru', + $content, + 'Flow email username must be present' + ); + } + + /** + * Test PHPDoc documentation exists + */ + public function testPhpDocExists(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + } +} diff --git a/erp24/tests/unit/services/SiteServiceSecretsTest.php b/erp24/tests/unit/services/SiteServiceSecretsTest.php new file mode 100644 index 00000000..a334d535 --- /dev/null +++ b/erp24/tests/unit/services/SiteServiceSecretsTest.php @@ -0,0 +1,114 @@ +basePath = dirname(__DIR__, 3); + $this->serviceContent = file_get_contents($this->basePath . '/services/SiteService.php'); + } + + /** + * Test SiteService file exists + */ + public function testFileExists(): void + { + $filePath = $this->basePath . '/services/SiteService.php'; + $this->assertFileExists($filePath, 'services/SiteService.php must exist'); + } + + /** + * Test SiteService is valid PHP + */ + public function testIsValidPhp(): void + { + $filePath = $this->basePath . '/services/SiteService.php'; + $output = []; + $returnCode = 0; + exec("php -l " . escapeshellarg($filePath) . " 2>&1", $output, $returnCode); + $this->assertEquals(0, $returnCode, 'SiteService.php must be valid PHP: ' . implode("\n", $output)); + } + + /** + * Test SiteService uses SITE_API_URL env + */ + public function testUsesSiteApiUrlEnv(): void + { + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]SITE_API_URL[\'"]\s*\)/', + $this->serviceContent, + 'SiteService must use getenv(\'SITE_API_URL\')' + ); + } + + /** + * Test SiteService has no hardcoded API URL + */ + public function testNoHardcodedApiUrl(): void + { + // Should not contain hardcoded full URL + $this->assertDoesNotMatchRegularExpression( + '/https?:\/\/[a-z0-9.-]+\/v1\/order-logs/', + $this->serviceContent, + 'SiteService must not contain hardcoded API URL' + ); + } + + /** + * Test SiteService URL is constructed from env + */ + public function testUrlConstructedFromEnv(): void + { + // URL should be constructed: getenv('SITE_API_URL') . '/v1/order-logs' + $this->assertStringContainsString( + "getenv('SITE_API_URL')", + $this->serviceContent, + 'SiteService must construct URL from SITE_API_URL env' + ); + } + + /** + * Test SiteService has proper namespace + */ + public function testHasProperNamespace(): void + { + $this->assertStringContainsString( + 'namespace yii_app\services', + $this->serviceContent, + 'SiteService must have proper namespace' + ); + } + + /** + * Test .env.example has SITE_API_URL + */ + public function testEnvExampleHasSiteApiUrl(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'SITE_API_URL=', + $envExample, + '.env.example must contain SITE_API_URL' + ); + } +} diff --git a/erp24/tests/unit/services/TelegramServiceSecretsTest.php b/erp24/tests/unit/services/TelegramServiceSecretsTest.php new file mode 100644 index 00000000..6729db92 --- /dev/null +++ b/erp24/tests/unit/services/TelegramServiceSecretsTest.php @@ -0,0 +1,253 @@ +servicePath = dirname(__DIR__, 3) . '/services/TelegramService.php'; + } + + /** + * Test TelegramService.php file exists + */ + public function testServiceFileExists(): void + { + $this->assertFileExists( + $this->servicePath, + 'TelegramService.php must exist' + ); + } + + /** + * Test config is valid PHP syntax + */ + public function testServiceIsValidPhp(): void + { + $output = []; + $returnCode = 0; + exec("php -l {$this->servicePath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'TelegramService.php must have valid PHP syntax: ' . implode("\n", $output) + ); + } + + /** + * Test TELEGRAM_BOT_TOKEN uses getenv + */ + public function testBotTokenUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('TELEGRAM_BOT_TOKEN')", + $content, + 'TelegramService must use TELEGRAM_BOT_TOKEN from env' + ); + } + + /** + * Test TELEGRAM_BOT_TOKEN_PROD uses getenv + */ + public function testBotTokenProdUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('TELEGRAM_BOT_TOKEN_PROD')", + $content, + 'TelegramService must use TELEGRAM_BOT_TOKEN_PROD from env' + ); + } + + /** + * Test TELEGRAM_CHAT_CHANNEL_ID uses getenv + */ + public function testChatChannelIdUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('TELEGRAM_CHAT_CHANNEL_ID')", + $content, + 'TelegramService must use TELEGRAM_CHAT_CHANNEL_ID from env' + ); + } + + /** + * Test TELEGRAM_CHAT_CHANNEL_ERP_ID uses getenv + */ + public function testChatChannelErpIdUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('TELEGRAM_CHAT_CHANNEL_ERP_ID')", + $content, + 'TelegramService must use TELEGRAM_CHAT_CHANNEL_ERP_ID from env' + ); + } + + /** + * Test CHATBOT_SALT uses getenv + */ + public function testChatbotSaltUsesEnv(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + "getenv('CHATBOT_SALT')", + $content, + 'TelegramService must use CHATBOT_SALT from env' + ); + } + + /** + * Test no hardcoded bot tokens + */ + public function testNoHardcodedBotToken(): void + { + $content = file_get_contents($this->servicePath); + + // Known hardcoded token patterns (masked format) + $this->assertDoesNotMatchRegularExpression( + '/\d{10}:AA[A-Za-z0-9_-]{33}/', + $content, + 'Must not contain hardcoded Telegram bot token' + ); + } + + /** + * Test no hardcoded chat IDs as constants + */ + public function testNoHardcodedChatIdConstants(): void + { + $content = file_get_contents($this->servicePath); + + // Check no const with numeric chat ID + $this->assertDoesNotMatchRegularExpression( + '/const\s+.*CHAT.*=\s*[\'\"]-?\d{9,12}[\'\"]\s*;/', + $content, + 'Must not have const with hardcoded chat ID' + ); + } + + /** + * Test private getter methods exist + */ + public function testPrivateGetterMethodsExist(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + 'private static function getBotTokenDev()', + $content, + 'getBotTokenDev() method must exist' + ); + + $this->assertStringContainsString( + 'private static function getBotTokenProd()', + $content, + 'getBotTokenProd() method must exist' + ); + + $this->assertStringContainsString( + 'private static function getChatChannelId()', + $content, + 'getChatChannelId() method must exist' + ); + + $this->assertStringContainsString( + 'private static function getChatChannelErpId()', + $content, + 'getChatChannelErpId() method must exist' + ); + + $this->assertStringContainsString( + 'private static function getChatbotSalt()', + $content, + 'getChatbotSalt() method must exist' + ); + } + + /** + * Test all getenv calls have empty string fallback + */ + public function testGetenvHasEmptyFallback(): void + { + $content = file_get_contents($this->servicePath); + + // Pattern: getenv('VAR') ?: '' + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'TELEGRAM_BOT_TOKEN'\s*\)\s*\?:\s*''/", + $content, + 'TELEGRAM_BOT_TOKEN must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'TELEGRAM_BOT_TOKEN_PROD'\s*\)\s*\?:\s*''/", + $content, + 'TELEGRAM_BOT_TOKEN_PROD must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'TELEGRAM_CHAT_CHANNEL_ID'\s*\)\s*\?:\s*''/", + $content, + 'TELEGRAM_CHAT_CHANNEL_ID must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'TELEGRAM_CHAT_CHANNEL_ERP_ID'\s*\)\s*\?:\s*''/", + $content, + 'TELEGRAM_CHAT_CHANNEL_ERP_ID must have empty string fallback' + ); + + $this->assertMatchesRegularExpression( + "/getenv\s*\(\s*'CHATBOT_SALT'\s*\)\s*\?:\s*''/", + $content, + 'CHATBOT_SALT must have empty string fallback' + ); + } + + /** + * Test PHPDoc documentation exists + */ + public function testPhpDocExists(): void + { + $content = file_get_contents($this->servicePath); + + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $content, + 'PHPDoc with ENV documentation must exist' + ); + } +} diff --git a/erp24/tests/unit/services/TelegramTargetSecretsTest.php b/erp24/tests/unit/services/TelegramTargetSecretsTest.php new file mode 100644 index 00000000..5ea4fab9 --- /dev/null +++ b/erp24/tests/unit/services/TelegramTargetSecretsTest.php @@ -0,0 +1,219 @@ +filePath = dirname(__DIR__, 3) . '/services/TelegramTarget.php'; + $this->fileContent = file_get_contents($this->filePath); + } + + /** + * Test file exists + */ + public function testFileExists(): void + { + $this->assertFileExists( + $this->filePath, + 'TelegramTarget.php must exist' + ); + } + + /** + * Test file is valid PHP + */ + public function testIsValidPhp(): void + { + $output = []; + $returnCode = 0; + exec("php -l {$this->filePath} 2>&1", $output, $returnCode); + + $this->assertEquals( + 0, + $returnCode, + 'TelegramTarget.php must be valid PHP: ' . implode("\n", $output) + ); + } + + /** + * Test bot token uses getenv() + */ + public function testBotTokenUsesEnv(): void + { + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_TOKEN[\'"]\s*\)/', + $this->fileContent, + 'botToken must use getenv(\'TELEGRAM_BOT_TOKEN\')' + ); + } + + /** + * Test chat ID uses getenv() + */ + public function testChatIdUsesEnv(): void + { + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_CHAT_CHANNEL_ERP_ID[\'"]\s*\)/', + $this->fileContent, + 'chatId must use getenv(\'TELEGRAM_CHAT_CHANNEL_ERP_ID\')' + ); + } + + /** + * Test no hardcoded Telegram bot token (format: NNNNNNNNNN:AA...) + */ + public function testNoHardcodedBotToken(): void + { + // Real Telegram token format: 10 digits : AA + 33 chars + $this->assertDoesNotMatchRegularExpression( + '/["\'][0-9]{9,11}:AA[A-Za-z0-9_-]{30,}["\']/', + $this->fileContent, + 'TelegramTarget.php must not contain hardcoded bot token' + ); + } + + /** + * Test no hardcoded chat ID (format: -100XXXXXXXXX) + */ + public function testNoHardcodedChatId(): void + { + // Telegram channel IDs start with -100 + $this->assertDoesNotMatchRegularExpression( + '/["\']?-100\d{10,}["\']?\s*;/', + $this->fileContent, + 'TelegramTarget.php must not contain hardcoded chat ID' + ); + } + + /** + * Test getenv has empty fallback for bot token + */ + public function testBotTokenHasEmptyFallback(): void + { + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_BOT_TOKEN[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $this->fileContent, + 'botToken getenv must have empty string fallback' + ); + } + + /** + * Test getenv has empty fallback for chat ID + */ + public function testChatIdHasEmptyFallback(): void + { + $this->assertMatchesRegularExpression( + '/getenv\s*\(\s*[\'"]TELEGRAM_CHAT_CHANNEL_ERP_ID[\'"]\s*\)\s*\?:\s*[\'"][\'"]/', + $this->fileContent, + 'chatId getenv must have empty string fallback' + ); + } + + /** + * Test PHP doc exists + */ + public function testPhpDocExists(): void + { + $this->assertStringContainsString( + 'ВАЖНО для ERP24', + $this->fileContent, + 'TelegramTarget.php must have documentation comment' + ); + } + + /** + * Test init() method exists and sets credentials + */ + public function testInitMethodSetsCredentials(): void + { + $this->assertStringContainsString( + 'public function init()', + $this->fileContent, + 'init() method must exist' + ); + + $this->assertStringContainsString( + '$this->botToken', + $this->fileContent, + 'init() must set botToken' + ); + + $this->assertStringContainsString( + '$this->chatId', + $this->fileContent, + 'init() must set chatId' + ); + } + + /** + * Test properties are declared without default values + */ + public function testPropertiesDeclaredWithoutDefaults(): void + { + // botToken should be declared without hardcoded value + $this->assertMatchesRegularExpression( + '/public\s+\$botToken\s*;/', + $this->fileContent, + 'botToken property must be declared without default value' + ); + + // chatId should be declared without hardcoded value + $this->assertMatchesRegularExpression( + '/public\s+\$chatId\s*;/', + $this->fileContent, + 'chatId property must be declared without default value' + ); + } + + /** + * Test allows override via config + */ + public function testAllowsOverrideViaConfig(): void + { + // Pattern: $this->property = $this->property ?: getenv(...) + // This allows setting values via Yii config + $this->assertMatchesRegularExpression( + '/\$this->botToken\s*=\s*\$this->botToken\s*\?:/', + $this->fileContent, + 'botToken must allow override via config (ternary pattern)' + ); + + $this->assertMatchesRegularExpression( + '/\$this->chatId\s*=\s*\$this->chatId\s*\?:/', + $this->fileContent, + 'chatId must allow override via config (ternary pattern)' + ); + } + + /** + * Test references .env.example + */ + public function testReferencesEnvExample(): void + { + $this->assertStringContainsString( + '@see erp24/.env.example', + $this->fileContent, + 'TelegramTarget.php must reference .env.example' + ); + } +} diff --git a/erp24/tests/unit/services/WhatsAppServiceSecretsTest.php b/erp24/tests/unit/services/WhatsAppServiceSecretsTest.php new file mode 100644 index 00000000..f619073b --- /dev/null +++ b/erp24/tests/unit/services/WhatsAppServiceSecretsTest.php @@ -0,0 +1,153 @@ +basePath = dirname(__DIR__, 3); + $this->serviceContent = file_get_contents($this->basePath . '/services/WhatsAppService.php'); + } + + /** + * Test WhatsAppService file exists + */ + public function testFileExists(): void + { + $filePath = $this->basePath . '/services/WhatsAppService.php'; + $this->assertFileExists($filePath, 'services/WhatsAppService.php must exist'); + } + + /** + * Test WhatsAppService is valid PHP + */ + public function testIsValidPhp(): void + { + $filePath = $this->basePath . '/services/WhatsAppService.php'; + $output = []; + $returnCode = 0; + exec("php -l " . escapeshellarg($filePath) . " 2>&1", $output, $returnCode); + $this->assertEquals(0, $returnCode, 'WhatsAppService.php must be valid PHP: ' . implode("\n", $output)); + } + + /** + * Test WhatsAppService has proper namespace + */ + public function testHasProperNamespace(): void + { + $this->assertStringContainsString( + 'namespace yii_app\services', + $this->serviceContent, + 'WhatsAppService must have proper namespace' + ); + } + + /** + * Test WhatsAppService accepts apiKey as constructor parameter + */ + public function testAcceptsApiKeyAsParameter(): void + { + // API key should be passed via constructor, not hardcoded + $this->assertMatchesRegularExpression( + '/function\s+__construct\s*\(\s*\$apiKey/', + $this->serviceContent, + 'WhatsAppService must accept apiKey as constructor parameter' + ); + } + + /** + * Test WhatsAppService stores apiKey as private property + */ + public function testStoresApiKeyAsPrivate(): void + { + $this->assertMatchesRegularExpression( + '/private\s+\$apiKey/', + $this->serviceContent, + 'WhatsAppService must store apiKey as private property' + ); + } + + /** + * Test WhatsAppService has no hardcoded API key + */ + public function testNoHardcodedApiKey(): void + { + // Should not contain hardcoded API key (long alphanumeric string) + $this->assertDoesNotMatchRegularExpression( + '/\$apiKey\s*=\s*[\'"][a-zA-Z0-9]{30,}[\'"]/', + $this->serviceContent, + 'WhatsAppService must not contain hardcoded API key' + ); + } + + /** + * Test WhatsAppService has no hardcoded cascade ID + */ + public function testNoHardcodedCascadeId(): void + { + // Should not contain hardcoded cascade ID + $this->assertDoesNotMatchRegularExpression( + '/\$cascadeId\s*=\s*[\'"][a-zA-Z0-9-]{20,}[\'"]/', + $this->serviceContent, + 'WhatsAppService must not contain hardcoded cascadeId' + ); + } + + /** + * Test WhatsAppService uses static API base URL (acceptable for endpoint) + */ + public function testHasApiBaseUrl(): void + { + // It's acceptable to have a static API base URL for the service endpoint + $this->assertStringContainsString( + '$apiBaseUrl', + $this->serviceContent, + 'WhatsAppService should have apiBaseUrl property' + ); + } + + /** + * Test WhatsAppService constructor assigns apiKey + */ + public function testConstructorAssignsApiKey(): void + { + $this->assertMatchesRegularExpression( + '/\$this->apiKey\s*=\s*\$apiKey/', + $this->serviceContent, + 'WhatsAppService constructor must assign apiKey' + ); + } + + /** + * Test .env.example has WHATSAPP_API_KEY + */ + public function testEnvExampleHasWhatsAppApiKey(): void + { + $envExample = file_get_contents($this->basePath . '/.env.example'); + + $this->assertStringContainsString( + 'WHATSAPP_API_KEY=', + $envExample, + '.env.example must contain WHATSAPP_API_KEY' + ); + } +}