Feature: OAuth authentication Background: Given I am in "dotfiles" git repo Scenario: Ask for username & password, create authorization Given the GitHub API server: """ require 'socket' require 'etc' machine_id = "#{Etc.getlogin}@#{Socket.gethostname}" post('/authorizations') { assert_basic_auth 'mislav', 'kitty' assert :scopes => ['repo', 'gist'], :note => "hub for #{machine_id}", :note_url => 'https://hub.github.com/' status 201 json :token => 'OTOKEN' } get('/user') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' json :login => 'MiSlAv' } post('/user/repos') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' status 201 json :full_name => 'mislav/dotfiles' } """ When I run `hub create` interactively When I type "mislav" And I type "kitty" Then the output should contain "github.com username:" And the output should contain "github.com password for mislav (never stored):" And the exit status should be 0 And the file "~/.config/hub" should contain "user: MiSlAv" And the file "~/.config/hub" should contain "oauth_token: OTOKEN" And the file "~/.config/hub" should have mode "0600" Scenario: Prompt for username & password, receive personal access token Given the GitHub API server: """ get('/user') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token 0123456789012345678901234567890123456789' json :login => 'llIMLLib' } post('/user/repos') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token 0123456789012345678901234567890123456789' status 201 json :full_name => 'llimllib/dotfiles' } """ When I run `hub create` interactively When I type "llimllib" And I type "0123456789012345678901234567890123456789" And the exit status should be 0 And the file "../home/.config/hub" should contain "user: llIMLLib" And the file "../home/.config/hub" should contain: """ oauth_token: "0123456789012345678901234567890123456789" """ Scenario: Ask for username & password, receive password that looks like a token Given the GitHub API server: """ post('/authorizations') { assert_basic_auth 'llimllib', '0123456789012345678901234567890123456789' status 201 json :token => 'OTOKEN' } get('/user') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' json :login => 'llIMLLib' } post('/user/repos') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' status 201 json :full_name => 'llimllib/dotfiles' } """ When I run `hub create` interactively When I type "llimllib" And I type "0123456789012345678901234567890123456789" And the exit status should be 0 And the file "../home/.config/hub" should contain "user: llIMLLib" And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: Rename & retry creating authorization if there's a token name collision Given the GitHub API server: """ require 'socket' require 'etc' machine_id = "#{Etc.getlogin}@#{Socket.gethostname}" post('/authorizations') { assert_basic_auth 'mislav', 'kitty' if params[:note] == "hub for #{machine_id} 3" status 201 json :token => 'OTOKEN' else status 422 json :message => 'Validation Failed', :errors => [{ :resource => 'OauthAccess', :code => 'already_exists', :field => 'description' }] end } get('/user') { json :login => 'MiSlAv' } post('/user/repos') { status 201 json :full_name => 'mislav/dotfiles' } """ When I run `hub create` interactively When I type "mislav" And I type "kitty" Then the output should contain "github.com username:" And the exit status should be 0 And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: Avoid getting caught up in infinite recursion while retrying token names Given the GitHub API server: """ tries = 0 post('/authorizations') { tries += 1 halt 400, json(:message => "too many tries") if tries >= 10 status 422 json :message => 'Validation Failed', :errors => [{ :resource => 'OauthAccess', :code => 'already_exists', :field => 'description' }] } """ When I run `hub create` interactively When I type "mislav" And I type "kitty" Then the output should contain: """ Error creating repository: Unprocessable Entity (HTTP 422) Duplicate value for "description" """ And the exit status should be 1 And the file "../home/.config/hub" should not exist Scenario: Credentials from GITHUB_USER & GITHUB_PASSWORD Given the GitHub API server: """ post('/authorizations') { assert_basic_auth 'mislav', 'kitty' status 201 json :token => 'OTOKEN' } get('/user') { json :login => 'mislav' } post('/user/repos') { status 201 json :full_name => 'mislav/dotfiles' } """ Given $GITHUB_USER is "mislav" And $GITHUB_PASSWORD is "kitty" When I successfully run `hub create` Then the output should not contain "github.com password for mislav" And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: XDG: legacy config found, credentials from GITHUB_USER & GITHUB_PASSWORD Given I am "mislav" on github.com with OAuth token "LTOKEN" And the GitHub API server: """ post('/authorizations') { assert_basic_auth 'mislav', 'kitty' status 201 json :token => 'OTOKEN' } get('/user') { json :login => 'mislav' } post('/user/repos') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' status 201 json :full_name => 'mislav/dotfiles' } """ And $GITHUB_USER is "mislav" And $GITHUB_PASSWORD is "kitty" And $XDG_CONFIG_HOME is "$HOME/.xdg" When I successfully run `hub create` Then the file "../home/.xdg/hub" should contain "oauth_token: OTOKEN" And the stderr with expanded variables should contain exactly: """ Notice: config file found but not respected at: <$HOME>/.config/hub You might want to move it to `<$HOME>/.xdg/hub' to avoid re-authenticating.\n """ Scenario: XDG: config from secondary directories Given I am "mislav" on github.com with OAuth token "OTOKEN" And the GitHub API server: """ get('/user') { json :login => 'mislav' } post('/user/repos') { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' status 201 json :full_name => 'mislav/dotfiles' } """ And $GITHUB_USER is "mislav" And $GITHUB_PASSWORD is "kitty" And $XDG_CONFIG_HOME is "$HOME/.xdg" And $XDG_CONFIG_DIRS is "/etc/xdg-nonsense:$HOME/.xdg-dir" When I move the file named "../home/.config/hub" to "../home/.xdg-dir/hub" And I successfully run `hub create` Then the file "../home/.xdg/hub" should not exist And the stderr should contain exactly "" Scenario: Credentials from GITHUB_TOKEN Given the GitHub API server: """ get('/user') { halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token OTOKEN" json :login => 'mislav' } post('/user/repos') { halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token OTOKEN" status 201 json :full_name => 'mislav/dotfiles' } """ Given $GITHUB_TOKEN is "OTOKEN" When I successfully run `hub create` Then the output should not contain "github.com password" And the output should not contain "github.com username" And the file "../home/.config/hub" should not exist Scenario: Credentials from GITHUB_TOKEN when obtaining username fails Given I am in "git://github.com/monalisa/playground.git" git repo Given the GitHub API server: """ get('/user') { status 403 json :message => "Resource not accessible by integration", :documentation_url => "https://developer.github.com/v3/users/#get-the-authenticated-user" } """ Given $GITHUB_TOKEN is "OTOKEN" Given $GITHUB_USER is "" When I run `hub release show v1.2.0` Then the output should not contain "github.com password" And the output should not contain "github.com username" And the file "../home/.config/hub" should not exist And the exit status should be 1 And the stderr should contain exactly: """ Error getting current user: Forbidden (HTTP 403) Resource not accessible by integration You must specify GITHUB_USER via environment variable.\n """ Scenario: Credentials from GITHUB_TOKEN and GITHUB_USER Given I am in "git://github.com/monalisa/playground.git" git repo Given the GitHub API server: """ get('/user') { status 403 json :message => "Resource not accessible by integration", :documentation_url => "https://developer.github.com/v3/users/#get-the-authenticated-user" } get('/repos/monalisa/playground/releases') { halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token OTOKEN" json [ { tag_name: 'v1.2.0', } ] } """ Given $GITHUB_TOKEN is "OTOKEN" Given $GITHUB_USER is "hubot" When I successfully run `hub release show v1.2.0` Then the output should not contain "github.com password" And the output should not contain "github.com username" And the file "../home/.config/hub" should not exist Scenario: Credentials from GITHUB_TOKEN and GITHUB_REPOSITORY Given I am in "git://github.com/monalisa/playground.git" git repo Given the GitHub API server: """ get('/user') { status 403 json :message => "Resource not accessible by integration", :documentation_url => "https://developer.github.com/v3/users/#get-the-authenticated-user" } get('/repos/monalisa/playground/releases') { halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token OTOKEN" json [ { tag_name: 'v1.2.0', } ] } """ Given $GITHUB_TOKEN is "OTOKEN" Given $GITHUB_REPOSITORY is "mona-lisa/play-ground" Given $GITHUB_USER is "" When I successfully run `hub release show v1.2.0` Then the output should not contain "github.com password" And the output should not contain "github.com username" And the file "../home/.config/hub" should not exist Scenario: Credentials from GITHUB_TOKEN override those from config file Given I am "mislav" on github.com with OAuth token "OTOKEN" Given the GitHub API server: """ get('/user') { halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token PTOKEN" json :login => 'parkr' } get('/repos/parkr/dotfiles') { halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token PTOKEN" json :private => false, :name => 'dotfiles', :owner => { :login => 'parkr' }, :permissions => { :push => true } } """ Given $GITHUB_TOKEN is "PTOKEN" When I successfully run `hub clone dotfiles` Then it should clone "git@github.com:parkr/dotfiles.git" And the file "../home/.config/hub" should contain "user: mislav" And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: Wrong password Given the GitHub API server: """ post('/authorizations') { assert_basic_auth 'mislav', 'kitty' } """ When I run `hub create` interactively When I type "mislav" And I type "WRONG" Then the stderr should contain exactly: """ Error creating repository: Unauthorized (HTTP 401) Bad credentials """ And the exit status should be 1 And the file "../home/.config/hub" should not exist Scenario: Two-factor authentication, create authorization Given the GitHub API server: """ post('/authorizations') { assert_basic_auth 'mislav', 'kitty' if request.env['HTTP_X_GITHUB_OTP'] == '112233' status 201 json :token => 'OTOKEN' else response.headers['X-GitHub-OTP'] = 'required; app' status 401 json :message => "Must specify two-factor authentication OTP code." end } get('/user') { json :login => 'mislav' } post('/user/repos') { status 201 json :full_name => 'mislav/dotfiles' } """ When I run `hub create` interactively When I type "mislav" And I type "kitty" And I type "112233" Then the output should contain "github.com password for mislav (never stored):" Then the output should contain "two-factor authentication code:" And the output should not contain "warning: invalid two-factor code" And the exit status should be 0 And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: Retry entering two-factor authentication code Given the GitHub API server: """ previous_otp_code = nil post('/authorizations') { assert_basic_auth 'mislav', 'kitty' if request.env['HTTP_X_GITHUB_OTP'] == '112233' halt 400 unless '666' == previous_otp_code status 201 json :token => 'OTOKEN' else previous_otp_code = request.env['HTTP_X_GITHUB_OTP'] response.headers['X-GitHub-OTP'] = 'required; app' status 401 json :message => "Must specify two-factor authentication OTP code." end } get('/user') { json :login => 'mislav' } post('/user/repos') { status 201 json :full_name => 'mislav/dotfiles' } """ When I run `hub create` interactively When I type "mislav" And I type "kitty" And I type "666" And I type "112233" Then the output should contain "warning: invalid two-factor code" And the exit status should be 0 And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: Special characters in username & password Given the GitHub API server: """ post('/authorizations') { assert_basic_auth 'mislav@example.com', 'my pass@phrase ok?' status 201 json :token => 'OTOKEN' } get('/user') { json :login => 'mislav' } get('/repos/mislav/dotfiles') { json :full_name => 'mislav/dotfiles' } """ When I run `hub create` interactively When I type "mislav@example.com" And I type "my pass@phrase ok?" Then the output should contain "github.com password for mislav@example.com (never stored):" And the exit status should be 0 And the file "../home/.config/hub" should contain "user: mislav" And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" Scenario: Enterprise fork authentication with username & password, re-using existing authorization Given the GitHub API server: """ require 'rack/auth/basic' post('/api/v3/authorizations', :host_name => 'git.my.org') { auth = Rack::Auth::Basic::Request.new(env) halt 401 unless auth.credentials == %w[mislav kitty] status 201 json :token => 'OTOKEN', :note_url => 'https://hub.github.com/' } get('/api/v3/user', :host_name => 'git.my.org') { json :login => 'mislav' } post('/api/v3/repos/evilchelu/dotfiles/forks', :host_name => 'git.my.org') { status 202 json :name => 'dotfiles', :owner => { :login => 'mislav' } } """ And "git.my.org" is a whitelisted Enterprise host And the "origin" remote has url "git@git.my.org:evilchelu/dotfiles.git" When I run `hub fork` interactively And I type "mislav" And I type "kitty" Then the output should contain "git.my.org password for mislav (never stored):" And the exit status should be 0 And the file "../home/.config/hub" should contain "git.my.org" And the file "../home/.config/hub" should contain "user: mislav" And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" And the url for "mislav" should be "git@git.my.org:mislav/dotfiles.git" Scenario: Broken config is missing user. Given a file named "../home/.config/hub" with: """ github.com: - oauth_token: OTOKEN protocol: https """ And the "origin" remote has url "git://github.com/mislav/coral.git" When I run `hub browse -u` interactively And I type "pcorpet" Then the output should contain "github.com username:" And the file "../home/.config/hub" should contain "- user: pcorpet" And the file "../home/.config/hub" should contain " oauth_token: OTOKEN" Scenario: Broken config is missing user and interactive input is empty. Given a file named "../home/.config/hub" with: """ github.com: - oauth_token: OTOKEN protocol: https """ And the "origin" remote has url "git://github.com/mislav/coral.git" When I run `hub browse -u` interactively And I type "" Then the output should contain "github.com username:" And the output should contain "missing user" And the file "../home/.config/hub" should not contain "user" Scenario: Config file is not writeable, should exit before asking for credentials Given $HUB_CONFIG is "/InvalidConfigFile" When I run `hub create` interactively Then the output should contain exactly: """ open /InvalidConfigFile: permission denied\n """ And the exit status should be 1 And the file "../home/.config/hub" should not exist Scenario: Config file is not writeable on default location, should exit before asking for credentials Given a directory named "../home/.config" with mode "600" When I run `hub create` interactively Then the output with expanded variables should contain: """ <$HOME>/.config/hub: permission denied\n """ And the exit status should be 1 And the file "../home/.config/hub" should not exist Scenario: GitHub SSO challenge Given I am "monalisa" on github.com with OAuth token "OTOKEN" And I am in "git://github.com/acme/playground.git" git repo Given the GitHub API server: """ get('/repos/acme/playground/releases') { response.headers['X-GitHub-SSO'] = 'required; url=http://example.com?auth=HASH' status 403 } """ When I run `hub release show v1.2.0` Then the stderr should contain exactly: """ Error fetching releases: Forbidden (HTTP 403) You must authorize your token to access this organization: http://example.com?auth=HASH """