diff --git a/features/package-auth.feature b/features/package-auth.feature new file mode 100644 index 00000000..fa568f24 --- /dev/null +++ b/features/package-auth.feature @@ -0,0 +1,43 @@ +Feature: Composer authentication for various git providers + + Scenario: GitHub OAuth token is set in COMPOSER_AUTH + Given an empty directory + When I run `GITHUB_TOKEN=ghp_test123456789 wp package path` + Then STDOUT should not be empty + And the return code should be 0 + + Scenario: GitLab OAuth token is set in COMPOSER_AUTH + Given an empty directory + When I run `GITLAB_OAUTH_TOKEN=glpat_test123456789 wp package path` + Then STDOUT should not be empty + And the return code should be 0 + + Scenario: GitLab personal access token is set in COMPOSER_AUTH + Given an empty directory + When I run `GITLAB_TOKEN=glpat_test123456789 wp package path` + Then STDOUT should not be empty + And the return code should be 0 + + Scenario: Bitbucket OAuth consumer is set in COMPOSER_AUTH + Given an empty directory + When I run `BITBUCKET_CONSUMER_KEY=test_key BITBUCKET_CONSUMER_SECRET=test_secret wp package path` + Then STDOUT should not be empty + And the return code should be 0 + + Scenario: HTTP Basic Auth is set in COMPOSER_AUTH + Given an empty directory + When I run `HTTP_BASIC_AUTH='{"repo.example.com":{"username":"user","password":"pass"}}' wp package path` + Then STDOUT should not be empty + And the return code should be 0 + + Scenario: Multiple auth providers can be used together + Given an empty directory + When I run `GITHUB_TOKEN=ghp_test123 GITLAB_TOKEN=glpat_test456 wp package path` + Then STDOUT should not be empty + And the return code should be 0 + + Scenario: Invalid HTTP_BASIC_AUTH JSON is ignored + Given an empty directory + When I run `HTTP_BASIC_AUTH='not-valid-json' wp package path` + Then STDOUT should not be empty + And the return code should be 0 diff --git a/src/Package_Command.php b/src/Package_Command.php index b5a4af8f..74564687 100644 --- a/src/Package_Command.php +++ b/src/Package_Command.php @@ -1409,11 +1409,54 @@ private function set_composer_auth_env_var() { if ( empty( $composer_auth ) || ! is_array( $composer_auth ) ) { $composer_auth = []; } + + // GitHub OAuth token. $github_token = getenv( 'GITHUB_TOKEN' ); if ( ! isset( $composer_auth['github-oauth'] ) && is_string( $github_token ) ) { $composer_auth['github-oauth'] = [ 'github.com' => $github_token ]; $changed = true; } + + // GitLab OAuth token. + $gitlab_oauth_token = getenv( 'GITLAB_OAUTH_TOKEN' ); + if ( ! isset( $composer_auth['gitlab-oauth'] ) && is_string( $gitlab_oauth_token ) && '' !== $gitlab_oauth_token ) { + $composer_auth['gitlab-oauth'] = [ 'gitlab.com' => $gitlab_oauth_token ]; + $changed = true; + } + + // GitLab personal access token. + $gitlab_token = getenv( 'GITLAB_TOKEN' ); + if ( ! isset( $composer_auth['gitlab-token'] ) && is_string( $gitlab_token ) ) { + $composer_auth['gitlab-token'] = [ 'gitlab.com' => $gitlab_token ]; + $changed = true; + } + + // Bitbucket OAuth consumer. + $bitbucket_key = getenv( 'BITBUCKET_CONSUMER_KEY' ); + $bitbucket_secret = getenv( 'BITBUCKET_CONSUMER_SECRET' ); + if ( ! isset( $composer_auth['bitbucket-oauth'] ) + && is_string( $bitbucket_key ) && '' !== $bitbucket_key + && is_string( $bitbucket_secret ) && '' !== $bitbucket_secret + ) { + $composer_auth['bitbucket-oauth'] = [ + 'bitbucket.org' => [ + 'consumer-key' => $bitbucket_key, + 'consumer-secret' => $bitbucket_secret, + ], + ]; + $changed = true; + } + + // HTTP Basic Authentication. + $http_basic_auth = getenv( 'HTTP_BASIC_AUTH' ); + if ( ! isset( $composer_auth['http-basic'] ) && is_string( $http_basic_auth ) && '' !== $http_basic_auth ) { + $http_basic_auth_decoded = json_decode( $http_basic_auth, true /*assoc*/ ); + if ( is_array( $http_basic_auth_decoded ) ) { + $composer_auth['http-basic'] = $http_basic_auth_decoded; + $changed = true; + } + } + if ( $changed ) { putenv( 'COMPOSER_AUTH=' . json_encode( $composer_auth ) ); } diff --git a/tests/PackageAuthTest.php b/tests/PackageAuthTest.php new file mode 100644 index 00000000..a930bcfc --- /dev/null +++ b/tests/PackageAuthTest.php @@ -0,0 +1,260 @@ +env_vars_to_restore = [ + 'COMPOSER_AUTH' => getenv( 'COMPOSER_AUTH' ), + 'GITHUB_TOKEN' => getenv( 'GITHUB_TOKEN' ), + 'GITLAB_OAUTH_TOKEN' => getenv( 'GITLAB_OAUTH_TOKEN' ), + 'GITLAB_TOKEN' => getenv( 'GITLAB_TOKEN' ), + 'BITBUCKET_CONSUMER_KEY' => getenv( 'BITBUCKET_CONSUMER_KEY' ), + 'BITBUCKET_CONSUMER_SECRET' => getenv( 'BITBUCKET_CONSUMER_SECRET' ), + 'HTTP_BASIC_AUTH' => getenv( 'HTTP_BASIC_AUTH' ), + ]; + + // Clear all auth-related environment variables + foreach ( array_keys( $this->env_vars_to_restore ) as $var ) { + putenv( $var ); + } + } + + public function tear_down() { + // Restore environment variables + foreach ( $this->env_vars_to_restore as $var => $value ) { + if ( false !== $value ) { + putenv( "$var=$value" ); + } else { + putenv( $var ); + } + } + + parent::tear_down(); + } + + /** + * Helper method to invoke the set_composer_auth_env_var method. + */ + private function invoke_set_composer_auth() { + $package = new Package_Command(); + $method = new \ReflectionMethod( 'Package_Command', 'set_composer_auth_env_var' ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + $method->invoke( $package ); + } + + /** + * Test that GITHUB_TOKEN is added to COMPOSER_AUTH. + */ + public function test_github_token_added_to_composer_auth() { + putenv( 'GITHUB_TOKEN=ghp_test123456789' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + $this->assertArrayHasKey( 'github-oauth', $auth_array ); + $this->assertArrayHasKey( 'github.com', $auth_array['github-oauth'] ); + $this->assertSame( 'ghp_test123456789', $auth_array['github-oauth']['github.com'] ); + } + + /** + * Test that GITLAB_OAUTH_TOKEN is added to COMPOSER_AUTH. + */ + public function test_gitlab_oauth_token_added_to_composer_auth() { + putenv( 'GITLAB_OAUTH_TOKEN=glpat_test123456789' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + $this->assertArrayHasKey( 'gitlab-oauth', $auth_array ); + $this->assertArrayHasKey( 'gitlab.com', $auth_array['gitlab-oauth'] ); + $this->assertSame( 'glpat_test123456789', $auth_array['gitlab-oauth']['gitlab.com'] ); + } + + /** + * Test that GITLAB_TOKEN is added to COMPOSER_AUTH. + */ + public function test_gitlab_token_added_to_composer_auth() { + putenv( 'GITLAB_TOKEN=glpat_test123456789' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + $this->assertArrayHasKey( 'gitlab-token', $auth_array ); + $this->assertArrayHasKey( 'gitlab.com', $auth_array['gitlab-token'] ); + $this->assertSame( 'glpat_test123456789', $auth_array['gitlab-token']['gitlab.com'] ); + } + + /** + * Test that BITBUCKET_CONSUMER_KEY and BITBUCKET_CONSUMER_SECRET are added to COMPOSER_AUTH. + */ + public function test_bitbucket_oauth_added_to_composer_auth() { + putenv( 'BITBUCKET_CONSUMER_KEY=test_key' ); + putenv( 'BITBUCKET_CONSUMER_SECRET=test_secret' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + $this->assertArrayHasKey( 'bitbucket-oauth', $auth_array ); + $this->assertArrayHasKey( 'bitbucket.org', $auth_array['bitbucket-oauth'] ); + $this->assertArrayHasKey( 'consumer-key', $auth_array['bitbucket-oauth']['bitbucket.org'] ); + $this->assertArrayHasKey( 'consumer-secret', $auth_array['bitbucket-oauth']['bitbucket.org'] ); + $this->assertSame( 'test_key', $auth_array['bitbucket-oauth']['bitbucket.org']['consumer-key'] ); + $this->assertSame( 'test_secret', $auth_array['bitbucket-oauth']['bitbucket.org']['consumer-secret'] ); + } + + /** + * Test that Bitbucket OAuth is not added if only one credential is provided. + */ + public function test_bitbucket_oauth_requires_both_credentials() { + putenv( 'BITBUCKET_CONSUMER_KEY=test_key' ); + // BITBUCKET_CONSUMER_SECRET is not set + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + // No auth should be set because only one credential was provided + $this->assertFalse( $composer_auth ); + } + + /** + * Test that HTTP_BASIC_AUTH is added to COMPOSER_AUTH. + */ + public function test_http_basic_auth_added_to_composer_auth() { + $http_basic = json_encode( + [ + 'repo.example.com' => [ + 'username' => 'user', + 'password' => 'pass', + ], + ] + ); + putenv( "HTTP_BASIC_AUTH=$http_basic" ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + $this->assertArrayHasKey( 'http-basic', $auth_array ); + $this->assertArrayHasKey( 'repo.example.com', $auth_array['http-basic'] ); + $this->assertArrayHasKey( 'username', $auth_array['http-basic']['repo.example.com'] ); + $this->assertArrayHasKey( 'password', $auth_array['http-basic']['repo.example.com'] ); + $this->assertSame( 'user', $auth_array['http-basic']['repo.example.com']['username'] ); + $this->assertSame( 'pass', $auth_array['http-basic']['repo.example.com']['password'] ); + } + + /** + * Test that invalid HTTP_BASIC_AUTH JSON is ignored. + */ + public function test_invalid_http_basic_auth_json_ignored() { + putenv( 'HTTP_BASIC_AUTH=not-valid-json' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + // No auth should be set because the JSON was invalid + $this->assertFalse( $composer_auth ); + } + + /** + * Test that multiple auth providers can be used together. + */ + public function test_multiple_auth_providers() { + putenv( 'GITHUB_TOKEN=ghp_test123' ); + putenv( 'GITLAB_TOKEN=glpat_test456' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + $this->assertArrayHasKey( 'github-oauth', $auth_array ); + $this->assertArrayHasKey( 'gitlab-token', $auth_array ); + $this->assertSame( 'ghp_test123', $auth_array['github-oauth']['github.com'] ); + $this->assertSame( 'glpat_test456', $auth_array['gitlab-token']['gitlab.com'] ); + } + + /** + * Test that existing COMPOSER_AUTH is preserved. + */ + public function test_existing_composer_auth_preserved() { + $existing_auth = json_encode( + [ + 'github-oauth' => [ + 'github.com' => 'existing_token', + ], + ] + ); + putenv( "COMPOSER_AUTH=$existing_auth" ); + putenv( 'GITLAB_TOKEN=glpat_new_token' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + // Existing github-oauth should be preserved + $this->assertArrayHasKey( 'github-oauth', $auth_array ); + $this->assertSame( 'existing_token', $auth_array['github-oauth']['github.com'] ); + // New gitlab-token should be added + $this->assertArrayHasKey( 'gitlab-token', $auth_array ); + $this->assertSame( 'glpat_new_token', $auth_array['gitlab-token']['gitlab.com'] ); + } + + /** + * Test that environment variable tokens don't override existing COMPOSER_AUTH values. + */ + public function test_env_tokens_dont_override_composer_auth() { + $existing_auth = json_encode( + [ + 'github-oauth' => [ + 'github.com' => 'existing_token', + ], + ] + ); + putenv( "COMPOSER_AUTH=$existing_auth" ); + putenv( 'GITHUB_TOKEN=new_token' ); + + $this->invoke_set_composer_auth(); + + $composer_auth = getenv( 'COMPOSER_AUTH' ); + $this->assertNotFalse( $composer_auth ); + + $auth_array = json_decode( $composer_auth, true ); + $this->assertIsArray( $auth_array ); + // Existing github-oauth should be preserved, not overridden by GITHUB_TOKEN + $this->assertArrayHasKey( 'github-oauth', $auth_array ); + $this->assertSame( 'existing_token', $auth_array['github-oauth']['github.com'] ); + } +}